Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

+269 -53
View File
@@ -54,12 +54,22 @@ abstract class _Item extends \IPS\Content
* @brief [Content\Item] Include the ability to search this content item in global site searches
*/
public static $includeInSiteSearch = TRUE;
/**
* @brief [Content\Item] Include these items in trending content
*/
public static $includeInTrending = TRUE;
/**
* @brief [Content\Item] Sharelink HTML
*/
protected $sharelinks = array();
/**
* @brief [Content\Item] Group Posted cache
*/
public $groupsPosted = [];
/**
* Whether or not to include in site search
*
@@ -387,7 +397,7 @@ abstract class _Item extends \IPS\Content
{
$member->name = $values['guest_name'];
}
/* Create the item */
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
@@ -397,6 +407,12 @@ abstract class _Item extends \IPS\Content
$obj->processForm( $values );
$obj->save();
/* It is possible that $member has changed */
if( $obj->author()->member_id != $member->member_id )
{
$member = $obj->author();
}
/* Create the comment */
$comment = NULL;
if ( isset( static::$commentClass ) and static::$firstCommentRequired )
@@ -418,7 +434,7 @@ abstract class _Item extends \IPS\Content
$obj->save();
}
}
/* Update posts per day limits - don't do this for content items that require a first comment as the comment class will handle that */
if ( $member->member_id AND $member->group['g_ppd_limit'] AND static::$firstCommentRequired === FALSE )
{
@@ -433,12 +449,11 @@ abstract class _Item extends \IPS\Content
$member->members_day_posts = $current;
$member->save();
}
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) )
{
$obj->setAnonymous( $values[ 'post_anonymously' ] );
$obj->setAnonymous( $values[ 'post_anonymously' ], $member );
}
/* Do any processing */
@@ -770,7 +785,7 @@ abstract class _Item extends \IPS\Content
}
/* Post Anonymously */
if ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) )
if ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) and ( $item === NULL or ( $item->author() and $item->author()->group['gbw_can_post_anonymously'] ) or $item->isAnonymous() ) )
{
$return['post_anonymously'] = new \IPS\Helpers\Form\YesNo( 'post_anonymously', ( $item ) ? $item->isAnonymous() : FALSE , FALSE, array( 'label' => \IPS\Member::loggedIn()->language()->addToStack( 'post_anonymously_suffix' ) ), NULL, NULL, NULL, 'post_anonymously' );
}
@@ -997,7 +1012,15 @@ abstract class _Item extends \IPS\Content
* @param array $values Values from form
* @return void
*/
protected function processBeforeCreate( $values ) {}
protected function processBeforeCreate( $values ) {
/* Check for banned IP - The banned ip addresses are only checked inside the register and login controller, so people are able to bypass them when PBR is used */
if( !\IPS\Member::loggedIn()->member_id AND \IPS\Request::i()->ipAddressIsBanned() )
{
\IPS\Output::i()->showBanned();
}
}
/**
* Process created object AFTER the object has been created
@@ -1022,6 +1045,32 @@ abstract class _Item extends \IPS\Content
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_create', $this, $this->webhookFilters() );
}
/* Data Layer Event */
if ( \IPS\Settings::i()->core_datalayer_enabled )
{
/* Is it a status update? */
if ( isset( static::$title ) and static::$title === \IPS\core\Statuses\Status::$title )
{
$member = \IPS\Member::load( $this->member_id );
$properties = array(
'profile_id' => $member->member_id,
'profile_name' => $member->real_name ?: null,
);
\IPS\core\DataLayer::i()->addEvent( 'social_update', $properties );
}
/* Otherwise, make sure it isn't a PM Conversation */
elseif ( !isset( static::$title ) or static::$title !== \IPS\core\Messenger\Conversation::$title )
{
\IPS\core\DataLayer::i()->addEvent( 'content_create', $this->getDataLayerProperties() );
}
}
/* Send this URL to IndexNow if the guest can view it */
if ( $this->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
}
/**
@@ -1125,10 +1174,12 @@ abstract class _Item extends \IPS\Content
{
$this->logEdit( $values );
}
$container = $this->containerWrapper();
if ( $this instanceof \IPS\Content\FuturePublishing AND isset( $values[ static::$formLangPrefix . 'date' ] ) )
{
$container = $this->containerWrapper();
$column = static::$databaseColumnMap['is_future_entry'];
if ( $container AND $this->$column )
@@ -1150,10 +1201,17 @@ abstract class _Item extends \IPS\Content
}
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) and ( isset( $container ) and $container->canPostAnonymously( $container::ANON_ITEMS ) ) )
if( isset( $values[ 'post_anonymously' ] ) and ( $container and $container->canPostAnonymously( $container::ANON_ITEMS ) ) )
{
$this->setAnonymous( $values[ 'post_anonymously' ] );
$this->setAnonymous( $values[ 'post_anonymously' ], $this->author() );
}
/* Send this URL to IndexNow if the guest can view it */
if( $this->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
}
/* Holds the old content for edit logging */
@@ -1354,7 +1412,7 @@ abstract class _Item extends \IPS\Content
*/
public function url( $action=NULL )
{
if( $action == 'getPrefComment' )
if( $action === 'getPrefComment' AND \IPS\Member::loggedIn()->member_id )
{
$pref = \IPS\Member::loggedIn()->linkPref() ?: \IPS\Settings::i()->link_default;
@@ -1373,7 +1431,7 @@ abstract class _Item extends \IPS\Content
break;
}
}
elseif( !\IPS\Member::loggedIn()->member_id AND $action == 'getNewComment' )
elseif( ( $action == 'getPrefComment' OR $action == 'getNewComment' OR $action == 'getLastComment' ) AND !\IPS\Member::loggedIn()->member_id )
{
$action = NULL;
}
@@ -1625,7 +1683,7 @@ abstract class _Item extends \IPS\Content
}
}
return \count( $attachments ) ? $attachments : NULL;
return \count( $attachments ) ? \array_slice( $attachments, 0, $limit ) : NULL;
}
/**
@@ -2263,7 +2321,7 @@ abstract class _Item extends \IPS\Content
$hasPrevious = TRUE;
try
{
$previousFirstComment = $commentClass::constructFromData( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=? AND ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['first'] . '=?', $this->$idColumn, TRUE ) )->first() );
$previousFirstComment = $commentClass::constructFromData( \IPS\Db::i()->select( '*', $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=? AND ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['first'] . '=?', $this->$idColumn, TRUE ), NULL, 1 )->first() );
}
catch( \UnderflowException $e )
{
@@ -2717,6 +2775,14 @@ abstract class _Item extends \IPS\Content
{
\IPS\Member::load( $member )->recountNotifications();
}
/** Item::url() can throw a LogicException exception in specific cases like when a Pages Record has no valid page */
try
{
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
}
catch( \LogicException $e ) {}
}
/**
@@ -4343,39 +4409,47 @@ abstract class _Item extends \IPS\Content
/* Solved count */
$commentClass = static::$commentClass;
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Solvable' ) )
$showSolvedStats = FALSE;
if ( method_exists( $this, 'isQuestion' ) and $this->isQuestion() )
{
if ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() )
$showSolvedStats = TRUE;
}
else if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Solvable' ) and ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() ) )
{
$showSolvedStats = TRUE;
}
if ( $commentClass AND $showSolvedStats )
{
$memberIds = array();
$solvedCounts = array();
$authorField = $commentClass::$databaseColumnMap['author'];
foreach( $results as $id => $data )
{
$memberIds = array();
$solvedCounts = array();
$authorField = $commentClass::$databaseColumnMap['author'];
$memberIds[ $data->$authorField ] = $data->$authorField;
}
if ( \count( $memberIds ) )
{
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, member_id', 'core_solved_index', array( \IPS\Db::i()->in( 'member_id', $memberIds ) ), NULL, NULL, 'member_id' ) as $member )
{
$solvedCounts[ $member['member_id'] ] = $member['count'];
}
foreach( $results as $id => $data )
{
$memberIds[ $data->$authorField ] = $data->$authorField;
}
if ( \count( $memberIds ) )
{
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, member_id', 'core_solved_index', array( \IPS\Db::i()->in( 'member_id', $memberIds ) ), NULL, NULL, 'member_id' ) as $member )
if ( isset( $solvedCounts[ $data->$authorField ] ) )
{
$solvedCounts[ $member['member_id'] ] = $member['count'];
}
foreach( $results as $id => $data )
{
if ( isset( $solvedCounts[ $data->$authorField ] ) )
{
$results[ $id ]->author_solved_count = $solvedCounts[ $data->$authorField ];
}
$results[ $id ]->author_solved_count = $solvedCounts[ $data->$authorField ];
}
}
}
}
/* Recognized content */
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Recognizable' ) )
if ( $commentClass AND \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Recognizable' ) )
{
foreach( \IPS\Db::i()->select( '*', 'core_member_recognize', [ [ 'r_content_class=?', $commentClass ], [ \IPS\Db::i()->in('r_content_id', array_keys( $results ) ) ] ] ) as $row )
{
@@ -4402,7 +4476,7 @@ abstract class _Item extends \IPS\Content
*/
public function mergeConcurrentComment()
{
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->merge_concurrent_posts and $this->lastCommenter()->member_id == \IPS\Member::loggedIn()->member_id )
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->merge_concurrent_posts and $this->lastCommenter()->member_id == \IPS\Member::loggedIn()->member_id and !\IPS\Member::loggedIn()->moderateNewContent() )
{
$lastComment = $this->comments( 1, 0, 'date', 'desc', NULL, TRUE );
@@ -4523,6 +4597,12 @@ abstract class _Item extends \IPS\Content
\IPS\Output::i()->error( 'posts_per_day_error', '2S177/2', 403, '' );
}
}
/* Check for banned IP - The banned ip addresses are only checked inside the register and login controller, so people are able to bypass them when PBR is used */
if( !\IPS\Member::loggedIn()->member_id AND \IPS\Request::i()->ipAddressIsBanned() )
{
\IPS\Output::i()->showBanned();
}
$currentPageCount = \IPS\Request::i()->currentPage;
@@ -4607,8 +4687,23 @@ abstract class _Item extends \IPS\Content
{
$message = \IPS\Member::loggedIn()->language()->addToStack( 'mod_queue_message' );
}
\IPS\Output::i()->json( array( 'type' => 'add', 'id' => $comment->$commentIdColumn, 'page' => $newPageCount, 'total' => $this->mapped('num_comments'), 'content' => \IPS\Output::i()->replaceEmojiWithImages( $output ), 'message' => $message ) );
/* Data Layer stuff for comments here, not in Comment class. We track user actions, and the user action is handled here */
$dataLayer = \IPS\Settings::i()->core_datalayer_enabled ? $this->getDataLayerProperties( $comment ) : array();
\IPS\Output::i()->json(
array(
'type' => 'add',
'id' => $comment->$commentIdColumn,
'page' => $newPageCount,
'total' => $this->mapped('num_comments'),
'content' => \IPS\Output::i()->replaceEmojiWithImages( $output ),
'message' => $message,
'postedByLoggedInMember' => true,
/* This is used on the front end */
"dataLayer" => $dataLayer,
)
);
}
return;
}
@@ -4667,6 +4762,102 @@ abstract class _Item extends \IPS\Content
return $this->_commentFormHtml;
}
protected $_dataLayerProperties = array();
/**
* Most, if not all of these are the same for different events, so we can just have one method
*
* @param IPS\Content\Comment $comment A comment item, leave null for these keys to be omitted
*
* @return array
*/
public function getDataLayerProperties( ?\IPS\Content\Comment $comment = null )
{
$commentIdColumn = $comment ? $comment::$databaseColumnId : null;
$index = $commentIdColumn ? ( $comment->$commentIdColumn ?: 0 ) : 0;
if ( !isset( $this->_dataLayerProperties[$index] ) )
{
$app = static::$application ?? null;
$idColumn = static::$databaseColumnId;
$dataLayer = array(
'author_id' => \IPS\core\DataLayer::i()->getSsoId( $this->author()->member_id ),
'author_name' => $this->author()->real_name ?: null,
'content_age' => $this->mapped( 'date' ) ? \intval( floor( ( time() - $this->mapped( 'date' ) ) / 86400 ) ) : null,
'content_container_id' => null,
'content_container_name' => null,
'content_container_type' => null,
'content_container_url' => null,
'content_id' => $this->$idColumn,
'content_title' => $this->mapped( 'title' ),
'content_type' => static::$contentType ?? null,
'content_url' => (string) $this->url(),
);
if ( $comment AND ( $commentIdColumn = $comment::$databaseColumnId ?? null ) )
{
$dataLayer = array_replace( $dataLayer, array(
'comment_id' => $comment->$commentIdColumn,
'comment_type' => $comment::$commentType ?? null,
'comment_url' => (string) $comment->url()
) );
}
/* For QA forums, the comment_type and content_type is an exception */
if ( \IPS\Settings::i()->core_datalayer_distinguish_qa AND
$this instanceof \IPS\forums\Topic AND
$this->container()->_forum_type === 'qa' )
{
$dataLayer['content_type'] = 'question';
if ( isset( $comment ) )
{
$dataLayer['comment_type'] = 'answer';
}
}
/* If we didn't find a Comment or Content type, try pulling that info from the static title fields */
elseif ( ( !isset( $dataLayer['content_type'] ) AND isset( static::$title ) ) OR ( !isset( $dataLayer['comment_type'] ) AND isset( $comment, $comment::$title ) ) )
{
$contentType = ( $app ? preg_replace( "/[{$app}_]?(.*)/", '$1', static::$title ?? "" ) : ( static::$title ?? null ) ) ?: null;
if ( $contentType AND !isset( $dataLayer['content_type'] ) )
{
$dataLayer['content_type'] = $contentType;
}
if ( $comment AND isset( $comment::$title ) AND !isset( $dataLayer['comment_type'] ) )
{
$commentType = ($app or $contentType) ? preg_replace( "/" . ( $app ? "[{$app}_]?" : '' ) . ( $contentType ? "[{$contentType}_]?" : '' ) . "(.*)/", "$1", $comment::$title ) : $comment::$title;
$dataLayer['comment_type'] = $commentType;
}
}
/* Use either comment or review if there still is no comment type (note this should never happen as of IPS v4.6 unless 3rd party things are going on) */
if ( $comment AND !isset( $dataLayer['comment_type'] ) )
{
$dataLayer['comment_type'] = $comment instanceof \IPS\Content\Review ? 'review' : 'comment';
}
try
{
$container = $this->container();
$dataLayer = array_replace( $dataLayer, $container->getDataLayerProperties() );
}
catch ( \OutOfRangeException | \BadMethodCallException $e ) {}
if ( !isset( $dataLayer['content_area'] ) AND $app )
{
$lang = \IPS\Lang::load( \IPS\Lang::defaultLanguage() );
if ( $lang->checkKeyExists( '__app_' . static::$application ) )
{
$dataLayer['content_area'] = $lang->addToStack( '__app_' . static::$application );
}
}
$this->_dataLayerProperties[$index] = \IPS\core\DataLayer::i()->filterProperties( $dataLayer );
}
return $this->_dataLayerProperties[$index];
}
/**
* Add the comment form elements
@@ -4760,16 +4951,7 @@ abstract class _Item extends \IPS\Content
$hidden = NULL;
}
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, isset( $values['guest_name'] ) ? $values['guest_name'] : NULL, NULL, NULL, NULL, NULL, $hidden );
$parameters = array_merge( array( 'reply-' . static::$application . '/' . static::$module . '-' . $this->$idColumn ), $comment->attachmentIds() );
\IPS\File::claimAttachments( ...$parameters );
/* Post anonymously */
if( isset( $values[ 'post_anonymously' ] ) )
{
$comment->setAnonymous( $values[ 'post_anonymously' ] );
}
$comment = $commentClass::create( $this, $values[ static::$formLangPrefix . 'comment' . '_' . $this->$idColumn ], FALSE, isset( $values['guest_name'] ) ? $values['guest_name'] : NULL, NULL, NULL, NULL, NULL, $hidden, ( isset( $values[ 'post_anonymously' ] ) ? (bool) $values[ 'post_anonymously' ] : NULL ) );
/* Auto-follow - If posted anonymously we should set follow to anonymous as well */
if( isset( $values[ static::$formLangPrefix . 'auto_follow' ] ) )
@@ -5606,7 +5788,7 @@ abstract class _Item extends \IPS\Content
if ( isset( static::$databaseColumnMap['last_comment_anon'] ) )
{
$lastCommentAnonField = static::$databaseColumnMap['last_comment_anon'];
$this->$lastCommentAnonField = $comment->isAnonymous();
$this->$lastCommentAnonField = (int) $comment->isAnonymous();
}
}
catch ( \UnderflowException $e )
@@ -5634,6 +5816,11 @@ abstract class _Item extends \IPS\Content
$field = static::$databaseColumnMap[$c];
$this->$field = 0;
}
else if( $c === 'last_comment_anon' AND isset( static::$databaseColumnMap['last_comment_anon'] ) )
{
$field = static::$databaseColumnMap[$c];
$this->$field = 0;
}
else
{
if ( isset( static::$databaseColumnMap[$c] ) )
@@ -6310,7 +6497,7 @@ abstract class _Item extends \IPS\Content
{
$member = $member ?: \IPS\Member::loggedIn();
/* If the member is banned they can't do antyhing? */
/* If the member is banned they can't do anything? */
if ( ! ( $member instanceof \IPS\Member\Group ) and ! $member->group['g_view_board'] )
{
return FALSE;
@@ -7771,11 +7958,11 @@ abstract class _Item extends \IPS\Content
}
}
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : NULL);
return ( isset ( $this->tags['tags'] ) ? $this->tags['tags'] : [] );
}
else
{
return NULL;
return [];
}
}
@@ -8755,7 +8942,7 @@ abstract class _Item extends \IPS\Content
'meta_item_id' => $this->$idColumn,
'meta_type' => $type,
'meta_data' => $data,
'meta_item_author' => $this->author()->member_id,
'meta_item_author' => (int) $this->author()->member_id,
'meta_added' => time()
) );
@@ -9212,4 +9399,33 @@ abstract class _Item extends \IPS\Content
{
return array( );
}
/**
* Webhook filters
*
* @return array
*/
public function webhookFilters()
{
$filters = parent::webhookFilters();
if ( \in_array( 'IPS\Content\Lockable', class_implements( $this ) ) )
{
$filters['locked'] = $this->locked();
}
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $this ) ) )
{
$filters['pinned'] = (bool) $this->mapped('pinned');
}
if ( \in_array( 'IPS\Content\Featurable', class_implements( $this ) ) )
{
$filters['featured'] = (bool) $this->mapped('featured');
}
if ( \in_array( 'IPS\Content\Polls', class_implements( $this ) ) )
{
$filters['hasPoll'] = (bool) $this->mapped('poll');
}
return $filters;
}
}