Version 4.7.8

This commit is contained in:
Neo committed 2025-12-19 16:10:42 -08:00
1 parent 7b7404e741
commit fd22d99e69
688 files changed
+19814 -10890

No files matched your search

@@ -10,6 +10,7 @@
namespace IPS\Content\Api\GraphQL;
use \IPS\Api\GraphQL\TypeRegistry;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -22,6 +23,18 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
abstract class _ItemMutator extends ContentMutator
{
public function args(): array
{
return [
'category' => TypeRegistry::nonNull( TypeRegistry::id() ),
'title' => TypeRegistry::nonNull( TypeRegistry::string() ),
'content' => TypeRegistry::nonNull( TypeRegistry::string() ),
'tags' => TypeRegistry::listOf( TypeRegistry::string() ),
'state' => TypeRegistry::itemState(),
'postKey' => TypeRegistry::string()
];
}
/**
* Mark as read
*
+8
View File
@@ -40,6 +40,13 @@ class _ItemController extends \IPS\Api\Controller
{
$where[] = array( \IPS\Db::i()->in( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['container'], array_map( 'intval', array_filter( explode( ',', \IPS\Request::i()->$containerParam ) ) ) ) );
}
/* IDs */
if ( isset( \IPS\Request::i()->ids ) )
{
$idField = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId;
$where[] = array(\IPS\Db::i()->in( $idField, array_map( 'intval', explode(',', \IPS\Request::i()->ids ) ) ) );
}
/* Authors */
if ( isset( \IPS\Request::i()->authors ) )
@@ -488,6 +495,7 @@ class _ItemController extends \IPS\Api\Controller
if( !$item->getPoll() )
{
$poll->starter_id = $item->author()->member_id;
$poll->poll_item_class = \get_class( $item );
}
$formatted = array(
+7 -6
View File
@@ -140,7 +140,7 @@ trait ClubContainer
}
$this->save();
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'], 'public' => $values['club_node_public'] ?? 0), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'], 'public' => $values['club_node_public'] ? $values['club_node_public'] : 0), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
}
\IPS\Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id, $values['club_node_name'] );
@@ -220,9 +220,10 @@ trait ClubContainer
case $club::TYPE_PUBLIC:
$insert[ 'perm_' . $v ] = 'ca';
break;
case $club::TYPE_OPEN:
case $club::TYPE_CLOSED:
$insert[ 'perm_' . $v ] = ( $this->isPublic() == 2 ) ? "*" : "cm,c{$club->id}";
break;
case $club::TYPE_OPEN:
case $club::TYPE_PRIVATE:
case $club::TYPE_READONLY:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
@@ -410,17 +411,17 @@ trait ClubContainer
/**
* Is public
*
* @return Bool
* @return int 0 = not public, 1 = anyone can view, 2 = anyone can participate
*/
public function isPublic()
{
try
{
return (bool) \IPS\Db::i()->select( 'public', 'core_clubs_node_map', array( 'club_id=? AND node_class=? AND node_id=?', $this->club()->id, \get_called_class(), $this->_id ) )->first();
return (int) \IPS\Db::i()->select( 'public', 'core_clubs_node_map', array( 'club_id=? AND node_class=? AND node_id=?', $this->club()->id, \get_called_class(), $this->_id ) )->first();
}
catch ( \UnderflowException $e )
{
return FALSE;
return 0;
}
}
}
+6 -4
View File
@@ -326,7 +326,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
if( $this->isAnonymous() )
{
$guest = new \IPS\Member;
$guest->name = \IPS\Member::loggedIn()->language()->addToStack( "post_anonymously_placename" );
$guest->name = \IPS\Member::loggedIn()->language()->get( "post_anonymously_placename" );
return $guest;
}
elseif ( $this->mapped('author') or !isset( static::$databaseColumnMap['author_name'] ) or !$this->mapped('author_name') )
@@ -3158,10 +3158,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Get edit history
*
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the author and private edits
* @param bool $staff Set true for moderators who have permission to view the full log which will show edits not made by the author and private edits
* @param int|NULL $limit
* @return \IPS\Db\Select
*/
public function editHistory( $staff=FALSE )
public function editHistory( $staff=FALSE, $limit=NULL )
{
$idColumn = static::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', \get_called_class(), $this->$idColumn ) );
@@ -3169,7 +3170,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$where[] = array( '`member`=? AND public=1', $this->author()->member_id );
}
return \IPS\Db::i()->select( '*', 'core_edit_history', $where, 'time DESC' );
return \IPS\Db::i()->select( '*', 'core_edit_history', $where, 'time DESC', $limit );
}
/**
+77 -2
View File
@@ -382,6 +382,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
return;
}
/* If we're filtering the topic (live topic questions) then the max commentID in the source will not be the max commentID possible
as not all comments will be shown */
if ( isset( \IPS\Request::i()->ltqid ) )
{
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
return;
}
/* If guest page caching is on, and we're a guest, disable polling */
if ( !\IPS\Member::loggedIn()->member_id and \IPS\CACHE_PAGE_TIMEOUT )
{
@@ -1271,10 +1279,40 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->error( 'node_error', '2S136/1Q', 404, '' );
}
$idColumn = $class::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', $class, $item->$idColumn ) );
if ( !\IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) )
{
$where[] = array( '`member`=? AND public=1', $item->author()->member_id );
}
$table = new \IPS\Helpers\Table\Db( 'core_edit_history', $item->url()->setQueryString( array( 'do' => 'editlog' ) ), $where );
$table->sortBy = 'time';
$table->sortDirection = 'desc';
$table->limit = 10;
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryRows' );
$table->parsers = array(
'new' => function( $val )
{
return $val;
},
'old' => function( $val )
{
return $val;
}
);
$table->extra = $item;
$pageParam = $table->getPaginationKey();
if( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->$pageParam ) )
{
\IPS\Output::i()->sendOutput( (string) $table );
}
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( $item->editHistory( \IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) ), $item );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( (string) $table, $item );
}
/**
@@ -2963,6 +3001,37 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
throw new \DomainException;
}
$idColumn = $commentClass::$databaseColumnId;
$where = array( array( 'class=? AND comment_id=?', $commentClass, $comment->$idColumn ) );
if ( !\IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) )
{
$where[] = array( '`member`=? AND public=1', $comment->author()->member_id );
}
$table = new \IPS\Helpers\Table\Db( 'core_edit_history', $item->url()->setQueryString( array( 'do' => 'editlogComment', 'comment' => $comment->$idColumn ) ), $where );
$table->sortBy = 'time';
$table->sortDirection = 'desc';
$table->limit = 10;
$table->tableTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryTable' );
$table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' ), 'commentEditHistoryRows' );
$table->parsers = array(
'new' => function( $val )
{
return $val;
},
'old' => function( $val )
{
return $val;
}
);
$table->extra = $comment;
$pageParam = $table->getPaginationKey();
if( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->$pageParam ) )
{
\IPS\Output::i()->sendOutput( (string) $table );
}
/* Display */
$container = NULL;
@@ -2979,7 +3048,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Output::i()->breadcrumb[] = array( $comment->url(), $item->mapped( 'title' ) );
\IPS\Output::i()->breadcrumb[] = array( NULL, \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' ) );
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'edit_history_title' );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( $comment->editHistory( \IPS\Member::loggedIn()->modPermission( 'can_view_editlog' ) ), $comment );
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->commentEditHistory( (string) $table, $comment );
}
/**
@@ -3529,6 +3598,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$oldItem->rebuildPopularTime();
}
/* Option to do some post split stuff */
if ( method_exists( $item, 'splitComplete' ) )
{
$item->splitComplete( $oldItem, $item, $comments );
}
/* Now reindex all of the comments - we do this last to prevent the "is last comment" flag continously bouncing around while the comments are still being moved */
foreach( $comments as $comment )
{
+95 -26
View File
@@ -320,7 +320,7 @@ abstract class _Item extends \IPS\Content
case 'hidden_reviews':
$val = 0;
break;
case 'is_future_entry':
$val = ( $time->getTimestamp() > time() ) ? 1 : 0;
break;
@@ -425,7 +425,7 @@ abstract class _Item extends \IPS\Content
{
$commentClass = static::$commentClass;
$comment = $commentClass::create( $obj, $values[ static::$formLangPrefix . 'content' ], TRUE, ( !$member->real_name ) ? NULL : $member->real_name, $obj->hidden() ? FALSE : NULL, $member );
$comment = $commentClass::create( $obj, $values[ static::$formLangPrefix . 'content' ], TRUE, ( !$member->real_name ) ? NULL : $member->real_name, $obj->hidden() ? FALSE : NULL, $member, $time );
$idColumn = static::$databaseColumnId;
$commentIdColumn = $commentClass::$databaseColumnId;
@@ -560,6 +560,7 @@ abstract class _Item extends \IPS\Content
{
$val = $val ? $val->pid : NULL;
}
foreach ( \is_array( static::$databaseColumnMap[ $k ] ) ? static::$databaseColumnMap[ $k ] : array( static::$databaseColumnMap[ $k ] ) as $column )
{
$this->$column = $val;
@@ -579,6 +580,12 @@ abstract class _Item extends \IPS\Content
$this->setTags( $values[ static::$formLangPrefix . 'tags' ] ?: array() );
}
/* Future Publishing */
if( static::canFuturePublish( NULL, $this->containerWrapper() ) and isset( static::$databaseColumnMap['future_date'] ) and isset( $values[ static::$formLangPrefix . 'date'] ) )
{
$this->setFuturePublishingDates( $values );
}
/* Post before registering */
if ( isset( $values['guest_email'] ) and ( !$this->containerWrapper() or !$this->containerWrapper()->can( 'add', \IPS\Member::loggedIn(), FALSE ) ) )
{
@@ -591,6 +598,29 @@ abstract class _Item extends \IPS\Content
\IPS\Request::i()->setCookie( 'post_before_register', $this->_logPostBeforeRegistering( $values['guest_email'], isset( \IPS\Request::i()->cookie['post_before_register'] ) ? \IPS\Request::i()->cookie['post_before_register'] : NULL ) );
}
}
/**
* Set future publishing dates
*
* @param array $values Values from form
* @return void
*/
protected function setFuturePublishingDates( array $values )
{
$time = $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
if( isset( static::$databaseColumnMap['date'] ) )
{
$column = static::$databaseColumnMap['date'];
$this->$column = ( $time->getTimestamp() > time() ) ? $time->getTimestamp() : time();
}
if( isset( static::$databaseColumnMap['future_date'] ) )
{
$column = static::$databaseColumnMap['future_date'];
$this->$column = $time->getTimestamp();
}
}
/**
* Can a given member create this type of content?
@@ -688,7 +718,7 @@ abstract class _Item extends \IPS\Content
/* Title */
if ( isset( static::$databaseColumnMap['title'] ) )
{
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', $item ? $item->mapped('title') : ( isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : NULL ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
$return['title'] = new \IPS\Helpers\Form\Text( static::$formLangPrefix . 'title', isset( \IPS\Request::i()->title ) ? \IPS\Request::i()->title : $item?->mapped( 'title' ), TRUE, array( 'maxLength' => \IPS\Settings::i()->max_title_length ?: 255, 'bypassProfanity' => \IPS\Helpers\Form\Text::BYPASS_PROFANITY_SWAP ) );
$return['title']->rowClasses[] = 'ipsFieldRow_primary';
$return['title']->rowClasses[] = 'ipsFieldRow_fullWidth';
}
@@ -856,20 +886,69 @@ abstract class _Item extends \IPS\Content
/* Create form element */
if ( $canCreatePoll )
{
$return['poll'] = new \IPS\Helpers\Form\Poll( static::$formLangPrefix . 'poll', $existingPoll, FALSE, array( 'allowPollOnly' => TRUE ) );
$return['poll'] = new \IPS\Helpers\Form\Poll( static::$formLangPrefix . 'poll', $existingPoll, FALSE, array( 'allowPollOnly' => TRUE, 'itemClass' => \get_called_class() ) );
}
}
/* Future date */
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) )
/* Show the future date field for new items or while editing an item, but only if the item wasn't published yet */
if ( static::supportsPublishDate( $item ) and static::canFuturePublish( NULL, $container ) )
{
$column = static::$databaseColumnMap['future_date'];
$return['date'] = new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => 0, 'unlimitedLang' => 'immediately'), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
$return['date'] = static::getPublishDateField( $item );
}
return $return;
}
/**
* Returns the Date Field for the publish date
*
* @param \IPS\Content|NULL $item
* @return \IPS\Helpers\Form\Date
*/
protected static function getPublishDateField( \IPS\Content $item = NULL ): \IPS\Helpers\Form\Date
{
/* If it's not published, we don't want to allow any past times */
$column = static::$databaseColumnMap['future_date'];
$minFutureTime = static::getMinimumPublishDate();
$unlimited = 0;
$unlimitedLang = 'immediately';
if( $item )
{
$unlimited = NULL;
$unlimitedLang = NULL;
}
return new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => $unlimited, 'unlimitedLang' => $unlimitedLang, 'min' => $minFutureTime ), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
}
/**
* Can the publish date be changed while editing the item?
*
* @var bool
*/
public static bool $allowPublishDateWhileEditing = FALSE;
/**
* Whether this content supports future publish dates
*
* @return bool
*/
protected static function supportsPublishDate( ?\IPS\Content\Item $item ): bool
{
return \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and isset( static::$databaseColumnMap['future_date'] ) AND ( ( $item AND ( $item->isFutureDate() OR static::$allowPublishDateWhileEditing ) OR !$item ) );
}
/**
* Returns the earliest publish date for the new content item , should be the current timestamp for most content types
*
* @return \IPS\DateTime|null
*/
protected static function getMinimumPublishDate(): ?\IPS\DateTime
{
return \IPS\DateTime::create();
}
/**
* Generate the tags form element
*
@@ -1150,6 +1229,13 @@ abstract class _Item extends \IPS\Content
$column = $commentClass::$databaseColumnMap['content'];
$idField = $commentClass::$databaseColumnId;
/* Update the comment date, in case the topic scheduled publish date has changed */
if( isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime and isset( $commentClass::$databaseColumnMap['date'] ) )
{
$dateColumn = $commentClass::$databaseColumnMap['date'];
$commentObj->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
}
if ( $commentObj instanceof \IPS\Content\EditHistory and \IPS\Settings::i()->edit_log )
{
$editIsPublic = \IPS\Member::loggedIn()->group['g_append_edit'] ? $values['comment_log_edit'] : TRUE;
@@ -1244,16 +1330,6 @@ abstract class _Item extends \IPS\Content
$this->publish();
}
}
elseif ( $container AND ! $this->$column )
{
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
{
/* Was not future, but now is */
$dateColumn = static::$databaseColumnMap['future_date'];
$this->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
$this->unPublish();
}
}
}
/* Post anonymously */
@@ -8503,7 +8579,7 @@ abstract class _Item extends \IPS\Content
$this->$future = 1;
}
$this->save();
$this->onUnpublish( $member );
@@ -8541,12 +8617,6 @@ abstract class _Item extends \IPS\Content
$this->$future = 0;
}
if ( isset( static::$databaseColumnMap['date'] ) )
{
$dateField = \is_array( static::$databaseColumnMap['date'] ) ? static::$databaseColumnMap['date'][0] : static::$databaseColumnMap['date'];
$this->$dateField = time();
}
$this->save();
$this->onPublish( $member );
@@ -8649,7 +8719,6 @@ abstract class _Item extends \IPS\Content
*/
public function onUnpublish( $member )
{
$container = NULL;
if ( method_exists( $this, 'container' ) )
{
try
+12 -8
View File
@@ -1265,16 +1265,20 @@ class _Query extends \IPS\Content\Search\Query
/* This forces MySQL to strongly hint a better index to the optimiser for MySQL 5.7+, even though sorting will not be affected */
if ( !\IPS\CIC )
{
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
try
{
$order .= ", index_date_updated";
}
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
{
$this->forceIndex = 'index_date_updated';
if ( $this->filterByUnread !== FALSE and ( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
{
$order .= ", index_date_updated";
}
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
if ( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and $this->filterByUnread === TRUE )
{
$this->forceIndex = 'index_date_updated';
}
}
catch( \Throwable $e ) { /* Do nothing if server_info access fails */ }
}
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
+1 -1
View File
@@ -51,7 +51,7 @@ class _Pinterest extends \IPS\Content\ShareServices
{
if ( $this->item )
{
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->pinterest( \IPS\Http\Url::external( 'http://pinterest.com/pin/create/button/' )->setQueryString( 'url', (string) $this->url )->setQueryString( 'media', (string) $this->item->shareImage() ) );
return \IPS\Theme::i()->getTemplate( 'sharelinks', 'core' )->pinterest( \IPS\Http\Url::external( 'https://pinterest.com/pin/create/button/' )->setQueryString( 'url', (string) $this->url )->setQueryString( 'media', (string) $this->item->shareImage() ) );
}
else
{
+6
View File
@@ -222,6 +222,12 @@ trait Solvable
{
return FALSE;
}
/* If we have no replies, it's not solvable yet */
if( $this->commentCount() <= 0 OR ( static::$firstCommentRequired AND $this->commentCount() == 1 ) )
{
return false;
}
if ( $this->containerAllowsSolvable() )
{