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

+155 -3
View File
@@ -239,13 +239,13 @@ class _CommentController extends \IPS\Api\Controller
$class = $this->class;
if ( \in_array( 'IPS\Content\Review', class_parents( $class ) ) )
{
$comment = $class::create( $item, $content, FALSE, \intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden );
$comment = $class::create( $item, $content, FALSE, \intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
}
else
{
$comment = $class::create( $item, $content, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden );
$comment = $class::create( $item, $content, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $hidden, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
}
/* Index */
if ( $item instanceof \IPS\Content\Searchable )
{
@@ -369,6 +369,158 @@ class _CommentController extends \IPS\Api\Controller
/* Return */
return new \IPS\Api\Response( 200, $comment->apiOutput( $this->member ) );
}
/**
* Delete a reaction to a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _reactRemove( $id ): \IPS\Api\Response
{
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/6', 404 );
}
try
{
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
$object->removeReaction( $member );
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \DomainException $e )
{
throw new \IPS\Api\Exception( $e->getMessage(), '1S425/7', 403 );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '1S425/8', 404 );
}
}
/**
* React to a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _reactAdd( $id ): \IPS\Api\Response
{
try
{
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->id );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_REACTION', '1S425/2', 404 );
}
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/3', 404 );
}
try
{
$class = $this->class;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
$object->react( $reaction, $member );
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
catch ( \DomainException $e )
{
throw new \IPS\Api\Exception( 'REACT_ERROR_' . $e->getMessage(), '1S425/4', 403 );
}
catch ( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'INVALID_ID', '1S425/5', 404 );
}
}
/**
* Report a comment
*
* @param $id
* @return \IPS\Api\Response
* @throws \IPS\Api\Exception
*/
public function _report( $id ): \IPS\Api\Response
{
try
{
$member = \IPS\Member::load( \IPS\Request::i()->author );
}
catch( \OutOfRangeException $e )
{
throw new \IPS\Api\Exception( 'NO_AUTHOR', '1S425/B', 404 );
}
$class = $this->class;
$idColumn = $class::$databaseColumnId;
if ( $this->member )
{
$object = $class::loadAndCheckPerms( $id, $this->member );
}
else
{
$object = $class::load( $id );
}
/* Has this member already reported this in the past 24 hours */
try
{
$index = \IPS\core\Reports\Report::loadByClassAndId( \get_class( $object ), $object->$idColumn );
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? and report_by=? and date_reported > ?', $index->id, $member, time() - ( \IPS\Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
/* They have aleady reported, so do nothing */
throw new \IPS\Api\Exception( 'REPORTED_ALREADY', '1S425/C', 404 );
}
catch( \Exception $e )
{
/* No issues here */
}
try
{
$object->report( ( isset( \IPS\Request::i()->message ) ? \IPS\Request::i()->message : '' ), ( isset(\IPS\Request::i()->report_type) ? \IPS\Request::i()->report_type : 0 ), $member );
}
catch( \UnexpectedValueException $e )
{
throw new \IPS\Api\Exception( 'REPORT_ERROR_' . $e->getMessage(), '1S425/B', 403 );
}
return new \IPS\Api\Response( 200, $object->apiOutput( $this->member ) );
}
}
+30 -9
View File
@@ -339,6 +339,23 @@ class _ItemController extends \IPS\Api\Controller
}
}
/** We intentionally allow anonymous content in nodes where it's possible (but where it's disabled via the settings */
if( isset( \IPS\Request::i()->anonymous ) AND \in_array( 'IPS\Content\Anonymous', class_implements( \get_class( $item ) ) ) )
{
/* we need to save the item before we set the anonymous data */
$idColumn = $item::$databaseColumnId;
if ( !$item->$idColumn )
{
$item->save();
}
try
{
$item->setAnonymous( (bool) \IPS\Request::i()->anonymous, $item->author() );
}
catch ( \BadMethodCallException $e ){}
}
/* Update first comment if required, and it's not a new item */
$field = isset( $item::$databaseColumnMap['first_comment_id'] ) ? $item::$databaseColumnMap['first_comment_id'] : NULL;
$commentClass = $item::$commentClass;
@@ -392,9 +409,10 @@ class _ItemController extends \IPS\Api\Controller
/* Create item */
$item = $class::createItem( $author, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $date, $container );
$this->_createOrUpdate( $item, 'add' );
$item->save();
/* Create post */
if ( $class::$firstCommentRequired )
{
@@ -404,9 +422,9 @@ class _ItemController extends \IPS\Api\Controller
{
$postContents = \IPS\Text\Parser::parseStatic( $postContents, TRUE, NULL, $this->member, $class::$application . '_' . mb_ucfirst( $class::$module ) );
}
$commentClass = $item::$commentClass;
$post = $commentClass::create( $item, $postContents, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress() );
$post = $commentClass::create( $item, $postContents, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), NULL, ( isset( \IPS\Request::i()->anonymous ) ? (bool) \IPS\Request::i()->anonymous : NULL ) );
if ( isset( $class::$databaseColumnMap['first_comment_id'] ) )
{
@@ -516,21 +534,24 @@ class _ItemController extends \IPS\Api\Controller
}
$itemIdColumn = $itemClass::$databaseColumnId;
$where [] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$itemIdColumn );
/* Hidden? */
if ( isset( \IPS\Request::i()->hidden ) AND \in_array( 'IPS\Content\Hideable', class_implements( $commentClass ) ) )
/* Hideable? */
if ( \in_array( 'IPS\Content\Hideable', class_implements( $commentClass ) ) )
{
if ( \IPS\Request::i()->hidden )
/* If request for hidden comments, only show if request is via API key or by a moderator */
if ( isset( \IPS\Request::i()->hidden ) AND \IPS\Request::i()->hidden
AND ( $this->member === NULL OR $commentClass::modPermission( 'view_hidden', $this->member ) ) )
{
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
{
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . '<>0' );
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'] . ' NOT IN( -3, -2 )' );
}
else
{
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'] . '<>1' );
$where[] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'] . ' NOT IN( -3, -2 )' );
}
}
/* Do not show hidden comments */
else
{
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
+31 -6
View File
@@ -60,7 +60,7 @@ trait ClubContainer
*/
public function clubCheckRules()
{
if ( $club = $this->club() AND !$club->rulesAcknowledged() )
if ( $club = $this->club() AND !$club->rulesAcknowledged() AND !\IPS\Member::loggedIn()->modPermission( 'can_access_all_clubs' ) )
{
\IPS\Output::i()->redirect( $club->url()->setQueryString( 'do', 'rules' )->addRef( \IPS\Request::i()->url() ) );
}
@@ -94,7 +94,7 @@ trait ClubContainer
* @param \IPS\Helpers\Form $form Form object
* @return void
*/
public function clubForm( \IPS\Helpers\Form $form )
public function clubForm( \IPS\Helpers\Form $form, \IPS\Member\Club $club )
{
$itemClass = static::$contentItemClass;
$form->add( new \IPS\Helpers\Form\Text( 'club_node_name', $this->_id ? $this->_title : \IPS\Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
@@ -116,6 +116,7 @@ trait ClubContainer
$this->_saveClubForm( $club, $values );
$needToUpdatePermissions = FALSE;
if ( !$haveId )
{
$clubIdColumn = $this->clubIdColumn();
@@ -125,15 +126,21 @@ trait ClubContainer
'club_id' => $club->id,
'node_class' => $nodeClass,
'node_id' => $this->_id,
'name' => $values['club_node_name']
'name' => $values['club_node_name'],
'public' => ( isset( $values['club_node_public'] ) ) ? $values['club_node_public'] : 0
) );
$needToUpdatePermissions = TRUE;
}
else
{
if( isset( $values['club_node_public'] ) AND $values['club_node_public'] != $this->isPublic() )
{
$needToUpdatePermissions = TRUE;
}
$this->save();
\IPS\Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'] ), 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'] ?? 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'] );
@@ -197,9 +204,10 @@ trait ClubContainer
case $club::TYPE_OPEN:
case $club::TYPE_READONLY:
$insert[ 'perm_' . $v ] = '*';
break;
break;
case $club::TYPE_CLOSED:
$insert[ 'perm_' . $v ] = ( $this->isPublic() ) ? "*" : "cm,c{$club->id}";
break;
case $club::TYPE_PRIVATE:
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
break;
@@ -398,4 +406,21 @@ trait ClubContainer
return parent::parents();
}
}
/**
* Is public
*
* @return Bool
*/
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();
}
catch ( \UnderflowException $e )
{
return FALSE;
}
}
}
+210 -20
View File
@@ -59,15 +59,18 @@ abstract class _Comment extends \IPS\Content
* @param \IPS\DateTime|NULL $time The time
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
* @return static
*/
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
{
if ( $member === NULL )
{
$member = \IPS\Member::loggedIn();
}
$idColumn = $item::$databaseColumnId;
/* Create the object */
$obj = new static;
foreach ( array( 'item', 'date', 'author', 'author_name', 'content', 'ip_address', 'first', 'approved', 'hidden' ) as $k )
@@ -78,7 +81,6 @@ abstract class _Comment extends \IPS\Content
switch ( $k )
{
case 'item':
$idColumn = $item::$databaseColumnId;
$val = $item->$idColumn;
break;
@@ -184,42 +186,52 @@ abstract class _Comment extends \IPS\Content
}
/* Check if profanity filters should mod-queue this comment */
$obj->checkProfanityFilters( $first );
$obj->checkProfanityFilters( $first, FALSE );
/* Save the comment */
$obj->save();
/* Set anonymous status */
if( $anonymous !== NULL )
{
try
{
$obj->setAnonymous( (bool) $anonymous, $member );
}
catch ( \Exception $e ) { }
}
/* Increment post count */
try
{
if ( !$obj->isAnonymous() and !$obj->hidden() and ( $incrementPostCount === TRUE or ( $incrementPostCount === NULL and static::incrementPostCount( $item->container() ) ) ) )
{
$obj->author()->member_posts++;
$member->member_posts++;
}
}
catch( \BadMethodCallException $e ) { }
/* Update member's last post and daily post limits */
if( $obj->author()->member_id AND $obj::incrementPostCount() )
if( $member->member_id AND $obj::incrementPostCount() )
{
$obj->author()->member_last_post = time();
$member->member_last_post = time();
/* Update posts per day limits */
if ( $obj->author()->group['g_ppd_limit'] )
if ( $member->group['g_ppd_limit'] )
{
$current = $obj->author()->members_day_posts;
$current = $member->members_day_posts;
$current[0] += 1;
if ( $current[1] == 0 )
{
$current[1] = \IPS\DateTime::create()->getTimestamp();
}
$obj->author()->members_day_posts = $current;
$member->members_day_posts = $current;
}
$obj->author()->save();
$member->save();
}
/* Send notifications and dish out points */
@@ -228,7 +240,7 @@ abstract class _Comment extends \IPS\Content
if ( !$obj->hidden() and ( !$first or !$item::$firstCommentRequired ) )
{
$obj->sendNotifications();
$obj->author()->achievementAction( 'core', 'Comment', $obj );
$member->achievementAction( 'core', 'Comment', $obj );
}
else if( $obj->hidden() === 1 )
{
@@ -241,10 +253,45 @@ abstract class _Comment extends \IPS\Content
{
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_create', $obj, $obj->webhookFilters() );
}
/* DataLayer Event */
if ( \IPS\Settings::i()->core_datalayer_enabled and !( $item::$firstCommentRequired and $obj->isFirst() ) )
{
if ( isset( $item::$title ) and $item::$title === \IPS\core\Statuses\Status::$title )
{
$event = 'social_reply';
$member = \IPS\Member::load( $item->member_id );
$properties = array(
'profile_id' => $member->member_id,
'profile_name' => $member->name,
);
}
else
{
$event = 'comment_create';
$properties = $obj->getDataLayerProperties();
}
if ( $properties['content_area'] === 'personal_messages' )
{
return;
}
$properties = array_merge( $properties, array(
'author_id' => $obj->author()->member_id,
'author_name' => $obj->author()->name ?? 'Guest',
) );
\IPS\core\DataLayer::i()->addEvent( $event, $properties );
}
/* Update item */
$obj->postCreate();
/* This needs to happen before the index is added */
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
\IPS\File::claimAttachments( ...$parameters );
/* Add to search index */
if ( $obj instanceof \IPS\Content\Searchable )
{
@@ -254,6 +301,32 @@ abstract class _Comment extends \IPS\Content
/* Return */
return $obj;
}
/**
* @brief Field cache for getDataLayerProperties
*/
protected $_dataLayerProperties = array();
/**
* Get the properties that can be added to the datalayer for this key
*
* @return array
*/
public function getDataLayerProperties()
{
if ( empty( $this->_dataLayerProperties ) )
{
$item = $this->item();
$this->_dataLayerProperties = $item->getDataLayerProperties( $this );
if ( $this->_dataLayerProperties['content_type'] === 'question' )
{
$this->_dataLayerProperties['comment_type'] = 'answer';
}
$this->_dataLayerProperties = \IPS\core\DataLayer::i()->filterProperties( $this->_dataLayerProperties );
}
return $this->_dataLayerProperties;
}
/**
* Join profile fields when loading comments?
@@ -376,6 +449,18 @@ abstract class _Comment extends \IPS\Content
{
$item->container()->rebuildPostedIn( array( $item->$itemIdColumn ), array( $this->author() ) );
}
/* Update trending */
$class = \get_class( $item );
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
/* Score by timestamp for trending */
try
{
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
}
catch( \Exception $e ) { }
}
}
/**
@@ -827,6 +912,12 @@ abstract class _Comment extends \IPS\Content
{
$item->clearCachedStatistics();
}
/* Send the content items URL to IndexNow if the guest can view it */
if( $item->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlToQueue( $item->url() );
}
}
/**
@@ -844,6 +935,48 @@ abstract class _Comment extends \IPS\Content
$itemColumn = static::$databaseColumnMap['item'];
$commentIdColumn = static::$databaseColumnId;
$this->$itemColumn = $item->$idColumn;
/* Does this comment's hidden status align with its item's hidden status? */
if ( $this instanceof \IPS\Content\Hideable AND $this->hidden() !== 1 )
{
if ( $item->hidden() === -1 )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
/* if this comment was directly hidden, we don't want to inherit its status from the item */
if ( \intval( $this->$column ) !== -1 )
{
$this->$column = 2;
}
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = -1;
}
}
elseif ( $this->hidden() === -1 )
{
if ( isset( static::$databaseColumnMap['hidden'] ) )
{
$column = static::$databaseColumnMap['hidden'];
/* if this comment was directly hidden, we don't want to inherit its status from the item */
if ( \intval( $this->$column ) !== -1 )
{
$this->$column = 0;
}
}
elseif ( isset( static::$databaseColumnMap['approved'] ) )
{
$column = static::$databaseColumnMap['approved'];
$this->$column = 1;
}
}
}
$this->save();
/* The new item needs to re-claim any attachments associated with this comment */
@@ -865,7 +998,16 @@ abstract class _Comment extends \IPS\Content
}
/* Update member map */
\IPS\Db::i()->update( 'core_item_member_map', array( 'map_item_id' => $item->$idColumn ), array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
try
{
\IPS\Db::i()->update( 'core_item_member_map', array( 'map_item_id' => $item->$idColumn ), array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
}
catch( \IPS\Db\Exception $e )
{
/* If we are splitting to a topic that this member has already posted in, this will cause a duplicate key issue. Best to remove rows and let the next page load rebuild */
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $item ), $item->$idColumn ) );
}
if( $skip === FALSE )
{
@@ -883,6 +1025,23 @@ abstract class _Comment extends \IPS\Content
$item->clearCachedStatistics();
}
}
/* Send the content items URL to IndexNow */
$noIndexUrls = [];
$guest = new \IPS\Member;
if( $oldItem->canView( $guest ) )
{
$noIndexUrls[] = $oldItem->url();
}
if( $item->canView( $guest ) )
{
$noIndexUrls[] = $item->url();
}
if( $oldItem->canView( new \IPS\Member ) )
{
\IPS\core\IndexNow::addUrlsToQueue( $noIndexUrls );
}
}
/**
@@ -1505,7 +1664,7 @@ abstract class _Comment extends \IPS\Content
$attachments[] = array( 'core_Clubs' => $club->cover_photo );
}
return \count( $attachments ) ? $attachments : NULL;
return \count( $attachments ) ? \array_slice( $attachments, 0, $limit ) : NULL;
}
/**
@@ -1593,12 +1752,13 @@ abstract class _Comment extends \IPS\Content
* @apiresponse string content The content
* @apiresponse bool hidden Is hidden?
* @apiresponse string url URL to content
* @apiresponse array reactions Array of reactions given, array key is member_id of reaction giver ([member_id => [ title, id, value, icon ]])
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
$idColumn = static::$databaseColumnId;
$itemColumn = static::$databaseColumnMap['item'];
return array(
$return = array(
'id' => $this->$idColumn,
'item_id' => $this->$itemColumn,
'author' => $this->author()->apiOutput( $authorizedMember ),
@@ -1607,6 +1767,35 @@ abstract class _Comment extends \IPS\Content
'hidden' => (bool) $this->hidden(),
'url' => (string) $this->url()
);
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
{
if ( $reactions = $this->reactions() )
{
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
$finalReactions = [];
foreach( $reactions as $memberId => $array )
{
foreach( $array as $reaction )
{
$finalReactions[ $memberId ][] = [
'title' => $enabledReactions[ $reaction ]->_title,
'id' => $reaction,
'value' => $enabledReactions[ $reaction ]->value,
'icon' => (string) $enabledReactions[ $reaction ]->_icon->url
];
}
}
$return['reactions'] = $finalReactions;
}
else
{
$return['reactions'] = [];
}
}
return $return;
}
/**
@@ -1618,9 +1807,10 @@ abstract class _Comment extends \IPS\Content
{
$filters = parent::webhookFilters();
$item = $this->item();
/* This is kept for legacy compatibility, but starting with 4.6.13 the Content and Item class handle this inside their own webhookFilter methods */
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item->hidden() ) );
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
{
$filters['locked'] = $item->locked();
+418 -105
View File
@@ -402,7 +402,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_delete', $this, $this->webhookFilters() );
parent::delete();
$this->expireWidgetCaches();
@@ -444,19 +446,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
/**
* Check comment against profanity filters
* Check comment against profanity filters but do not act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @return bool Whether to send unapproved notifications
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param string|NULL $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL )
{
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE )
{
/* Set our content */
$content = $content ?: $this->content();
/* We need our item */
$item = $this;
if ( $this instanceof \IPS\Content\Comment )
@@ -482,31 +487,78 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
return FALSE;
}
/* And the author */
/* Check the author cannot bypass */
$member = $this->author();
/* Pass this through our profanity filters to see if it needs to be mod queued */
$sendNotifications = FALSE;
/* Do we need to recount item comments and container comments? */
$recount = FALSE;
try
if ( $member->group['g_bypass_badwords'] )
{
$hiddenByFilter = FALSE;
/* If this is a content item that doesn't require a first comment, or a comment that isn't the first comment, this just pass the content as it stands. */
if ( !$this->hidden() AND ( ( $this instanceof \IPS\Content\Item ) OR ( $this instanceof \IPS\Content\Comment AND ( !$item::$firstCommentRequired OR ( $item::$firstCommentRequired AND !$first ) ) ) ) AND !$member->group['g_bypass_badwords'] )
return FALSE;
}
/* First, check the image scanner */
$idColumn = static::$databaseColumnId;
$itemIdColumn = $item::$databaseColumnId;
if ( \IPS\Settings::i()->ips_imagescanner_enable )
{
if ( $edit )
{
$autoSaveLocation = $autoSaveLocation ?: ( $item::$application . '_' . mb_ucfirst( $item::$module ) );
if ( !$autoSaveKeys )
{
if ( $this instanceof \IPS\Content\Comment and ( !( $this instanceof \IPS\Content\Review ) or !$first or !$item::$firstCommentRequired ) )
{
$autoSaveKeys = [ $this->attachmentIds() ];
}
else
{
$autoSaveKeys = [ [ $item->$itemIdColumn ] ];
}
}
$clauses = [];
$binds = [];
foreach ( $autoSaveKeys as $_autoSaveKeys )
{
$idLookups = [];
foreach ( range( 1, \count( $_autoSaveKeys ) ) as $i )
{
$idLookups[]= "id{$i}=?";
}
$clauses[] = '( ' . implode( ' AND ', $idLookups ) . ' )';
$binds = array_merge( $binds, $_autoSaveKeys );
}
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$hiddenByFilter )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
$attachmentsMapWhere = array_merge( array( 'location_key=? AND ( ' . implode( ' OR ', $clauses ) . ' )', $autoSaveLocation ), $binds );
}
/* If the comment is hidden (probably requires moderator approval) and this is the first post in the item that requires a comment (i.e. a post in a topic), then we need to set the item as requiring approval instead. */
elseif ( $item instanceof \IPS\Content\Hideable AND !$member->group['g_bypass_badwords'] AND ( $item::$firstCommentRequired and $first ) )
else
{
if ( $autoSaveKeys === NULL )
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$container = $item->containerWrapper();
$autoSaveKeys[] = 'newContentItem-' . $item::$application . '/' . $item::$module . '-' . ( $container ? $container->_id : 0 );
}
else
{
if ( $this instanceof \IPS\Content\Review )
{
$autoSaveKeys[] = 'review-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
else
{
$autoSaveKeys[] = 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$itemIdColumn;
}
}
}
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
if ( $title AND !$itemHiddenByFilter )
@@ -514,54 +566,142 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
}
if ( isset( $item::$databaseColumnMap['approved'] ) and $itemHiddenByFilter )
$attachmentsMapWhere = \IPS\Db::i()->in( 'temp', array_map( 'md5', $autoSaveKeys ) );
}
foreach ( \IPS\Db::i()->select( 'attach_moderation_status', 'core_attachments', [ 'attach_id IN (?)', \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', $attachmentsMapWhere ) ] ) as $attachModerationStatus )
{
if ( $attachModerationStatus === 'pending' )
{
/* 'approved' is easy, clear and concise */
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
$recount = TRUE;
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
elseif ( isset( $item::$databaseColumnMap['hidden'] ) and $itemHiddenByFilter )
}
if ( !$hiddenByFilter and !$itemHiddenByFilter )
{
foreach ( $imageUploads as $file )
{
/* 'hidden' is backwards */
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
$recount = TRUE;
if ( $file->requiresModeration )
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
$itemHiddenByFilter = TRUE;
}
else
{
$hiddenByFilter = TRUE;
}
break;
}
}
}
}
if( $edit === TRUE )
/* Then pass this through our profanity and link filters */
try
{
if ( $this instanceof \IPS\Content\Item or ( $first and $item::$firstCommentRequired ) )
{
if ( !$itemHiddenByFilter )
{
$sendNotifications = TRUE;
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
}
if ( !$itemHiddenByFilter and $title )
{
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
}
}
else
{
if ( !$hiddenByFilter )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
}
if ( !$hiddenByFilter and $title )
{
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
}
}
}
catch( \BadMethodCallException $e ) { }
if ( $hiddenByFilter === TRUE )
/* Return */
return ( $hiddenByFilter or $itemHiddenByFilter );
}
/**
* Check comment against profanity filters AND act on it
*
* @param bool $first Is this the first comment?
* @param bool $edit Are we editing or merging (true) or is this a new comment (false)?
* @param string|NULL $content The content to check - useful for if the content needs to be checked first, before it gets saved to the database.
* @param string|NULL|bool $title The title of the content to check, or NULL to check the current title, or FALSE to not check at all.
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
* @param string|NULL $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
* @param array $imageUploads Images that have been uploaded that may require moderation
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
*/
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[] )
{
/* Check it */
$hiddenByFilter = FALSE;
$itemHiddenByFilter = FALSE;
$sendNotifications = FALSE;
$item = ( $this instanceof \IPS\Content\Item ) ? $this : $this->item();
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter );
/* If we need to hide the item, then do that */
if ( $itemHiddenByFilter )
{
$recount = TRUE;
$sendNotifications = $edit;
/* 'approved' is easy, clear and concise */
if ( isset( $item::$databaseColumnMap['approved'] ) )
{
$column = $item::$databaseColumnMap['approved'];
$item->$column = 0;
$item->save();
}
/* 'hidden' is backwards */
elseif ( isset( $item::$databaseColumnMap['hidden'] ) )
{
$column = $item::$databaseColumnMap['hidden'];
$item->$column = 1;
$item->save();
}
}
/* If we need to hide this, then do that */
if ( $hiddenByFilter === TRUE or $itemHiddenByFilter === TRUE )
{
$sendNotifications = TRUE;
/* 'approved' is easy, clear and concise */
if ( isset( static::$databaseColumnMap['approved'] ) )
{
/* 'approved' is easy, clear and concise */
$column = static::$databaseColumnMap['approved'];
$this->$column = 0;
$this->save();
}
/* 'hidden' is backwards */
else if ( isset( static::$databaseColumnMap['hidden'] ) )
{
/* 'hidden' is backwards */
$column = static::$databaseColumnMap['hidden'];
$this->$column = 1;
$this->$column = ( $itemHiddenByFilter and $this instanceof \IPS\Content\Comment ) ? 2 : 1; # We use the special 2 flag to note it is only hidden because the parent is
$this->save();
}
/* We have to save here because the status change is relevant to resyncCommentCounts() and resyncLastComment(), and our comment object changes we just set can be overwritten when those methods are called. */
$this->save();
$sendNotifications = TRUE;
}
if ( $recount === TRUE )
/* If we did either, then recount number of comments */
if ( $itemHiddenByFilter or $hiddenByFilter )
{
$item->resyncCommentCounts();
$item->resyncLastComment();
@@ -574,6 +714,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
/* Return */
return $sendNotifications;
}
@@ -646,7 +787,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Return a list of groups that cannot see this item
*
* @return NULL|Array
* @return NULL|array
*/
public function cannotViewGroups()
{
@@ -1235,7 +1376,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$this->sendApprovedNotification();
}
/* Award points */
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) ? static::$itemClass : \get_called_class();
if ( $this instanceof \IPS\Content\Item )
@@ -1246,7 +1387,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$this->author()->achievementAction( 'core', 'Review', $this );
}
elseif ( $this instanceof \IPS\Content\Comment and ( !$itemClass::$firstCommentRequired or $this->isFirst() ) )
elseif ( $this instanceof \IPS\Content\Comment and ( ( $itemClass::$firstCommentRequired AND ! $this->isFirst() ) OR ( ! $itemClass::$firstCommentRequired ) ) )
{
$this->author()->achievementAction( 'core', 'Comment', $this );
}
@@ -1455,19 +1596,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
/**
* Report
*
* @param string $reportContent Report content message from member
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report
* @param string $reportContent Report content message from member
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report)
* @param \IPS\Member|NULL $member Member making the report (or NULL for loggedIn())
* @return \IPS\core\Reports\Report
* @throws \UnexpectedValueException If there is a permission error - you should only call this method after checking canReport
*/
public function report( $reportContent, $reportType=1 )
public function report( $reportContent, $reportType=1, $member=NULL )
{
$member = ( $member ) ? $member : \IPS\Member::loggedIn();
/* Permission check */
if ( $this->canReport() !== TRUE )
$permCheck = $this->canReport( $member );
if ( $permCheck !== TRUE )
{
throw new \UnexpectedValueException;
throw new \UnexpectedValueException( $permCheck );
}
/* Find or create an index */
$idColumn = static::$databaseColumnId;
try
@@ -1481,7 +1626,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$index->class = \get_called_class();
$index->content_id = $this->$idColumn;
$index->perm_id = $this->permId();
$index->first_report_by = (int) \IPS\Member::loggedIn()->member_id;
$index->first_report_by = (int) $member->member_id;
$index->first_report_date = time();
$index->last_updated = time();
$index->author = (int) $this->author()->member_id;
@@ -1502,10 +1647,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$reportInsert = array(
'rid' => $index->id,
'report' => $reportContent,
'report_by' => (int) \IPS\Member::loggedIn()->member_id,
'report_by' => (int) $member->member_id,
'date_reported' => time(),
'ip_address' => \IPS\Request::i()->ipAddress(),
'report_type' => \IPS\Member::loggedIn()->member_id ? $reportType : 0
'report_type' => $member->member_id ? $reportType : 0
);
$insertID = \IPS\Db::i()->insert( 'core_rc_reports', $reportInsert );
@@ -1596,12 +1741,19 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
case 'author':
$this->$col = $newAuthor->member_id ? $newAuthor->member_id : 0;
break;
case 'author_name':
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not */
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
break;
case 'edit_member_name':
/* Real name will contain the custom guest name if available or '' if not.
But we only want to update the user if oldName is the same as the current edit_member_name
So if "Bob" edited his own post, you want that to change if Bob becomes Bob2.
But if a moderator edited it, we don't want to change that name. */
if ( $oldAuthor->name == $this->$col )
{
$this->$col = $newAuthor->member_id ? $newAuthor->name : $newAuthor->real_name;
}
break;
case 'is_anon':
/* We are specifying an author so turn off is_anon column */
if ( $newAuthor->member_id )
@@ -1621,6 +1773,29 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
}
/**
* Get language articles for the given container
*
* @param string $itemClass The classname of this content
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
*/
public static function articlesFromIndexData( $itemClass, ?array $containerData ): array
{
/* Object URL */
$indefiniteArticle = static::_indefiniteArticle( $containerData );
$definiteArticle = static::_definiteArticle( $containerData );
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
if ( \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) )
{
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
$definiteArticle = $itemClass::_definiteArticle( $containerData );
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
}
return array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
}
/**
* Get HTML for search result display
*
@@ -1654,9 +1829,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$itemUrl = $itemClass::urlFromIndexData( $indexData, $itemData, 'getPrefComment' );
/* Object URL */
$indefiniteArticle = static::_indefiniteArticle( $containerData );
$definiteArticle = static::_definiteArticle( $containerData );
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
if ( \in_array( 'IPS\Content\Comment', class_parents( \get_called_class() ) ) )
{
if ( \in_array( 'IPS\Content\Review', class_parents( \get_called_class() ) ) )
@@ -1669,17 +1841,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
}
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
$definiteArticle = $itemClass::_definiteArticle( $containerData );
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
}
else
{
$objectUrl = $itemUrl;
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
}
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
/* Articles language */
$articles = static::articlesFromIndexData( $itemClass, $containerData );
/* Container details */
$containerUrl = NULL;
@@ -1740,7 +1910,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
*/
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, ?array $containerData, array $reputationData, $reviewRating, $view )
{
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData, $itemData ) : '';
}
/**
@@ -1752,17 +1922,32 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
* @param array $articles Articles language strings
* @param array $indexData Search index data
* @param array $itemData Data about the item
* @param bool $includeLinks Include links to member profile
* @return string
*/
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData )
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData, $includeLinks = TRUE )
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
if( $includeLinks )
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
else
{
$authorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $authorData['name'], $authorData['member_group_id'] ?? \IPS\Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
if( \in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
{
if( isset( $itemData['author'] ) )
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $itemData['author']['member_id'], $itemData['author']['name'], $itemData['author']['members_seo_name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
if( $includeLinks )
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $itemData['author']['member_id'], $itemData['author']['name'], $itemData['author']['members_seo_name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
}
else
{
$itemAuthorTemplate = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $itemData['author']['name'], $itemData['author']['member_group_id'] ?? \IPS\Settings::i()->guest_group );
}
}
if( \in_array( 'IPS\Content\Review', class_parents( $indexData['index_class'] ) ) )
@@ -1930,6 +2115,75 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return \IPS\Email::template( 'core', '_genericContent', $type, array( $this, $includeLinks, $includeAuthor, $email ) );
}
/**
* Returns "Foo posted {{indefart}} in {{container}}, {{date}}
*
* @param \IPS\Lang|NULL $plaintextLanguage If specified, will return plaintext (not linking the user or the container in the language specified). If NULL, returns with links based on logged in user's theme and language
* @note This function was extracted from IPS\Promote
* @return string
*/
public function objectMetaDescription( $plaintextLanguage=NULL )
{
$object = $this;
$author = $this->author();
if ( $object instanceof \IPS\Content\Item )
{
$container = $object->containerWrapper();
if ( $container )
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_container', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle(), $container->url(), $container->_title ),
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_container_nolink', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $container->getTitleForLanguage( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) ),
) );
}
}
else
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle() )
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) )
) );
}
}
}
else if ( $object instanceof \IPS\Content\Comment )
{
if ( !$plaintextLanguage )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'htmlsprintf' => array( $author->link(), \IPS\DateTime::ts( $this->mapped('date') )->html( FALSE ) ),
'sprintf' => array( $object->indefiniteArticle() )
) );
}
else
{
return $plaintextLanguage->addToStack( 'promote_metadescription_nocontainer', FALSE, array(
'sprintf' => array( $object->indefiniteArticle( $plaintextLanguage ), $author->name, \IPS\DateTime::ts( $this->mapped('date') ) )
) );
}
}
throw new \OutofRangeException('object_not_valid');
}
/**
* Get a count of the database table
@@ -2046,8 +2300,17 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
}
$followIds = array();
$followers = iterator_to_array( $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra ) );
$followers = $this->notificationRecipients( array( $offset, static::NOTIFICATIONS_PER_BATCH ), $extra );
/* If $followers is NULL (which can be the case if the follows are just people following the author), just return as there is nothing to do */
if ( $followers === NULL )
{
return NULL;
}
/* If we're still here, our iterator may not necessarily be one that implements Countable so we need to convert it to an array. */
$followers = iterator_to_array( $followers );
if( !\count( $followers ) )
{
return NULL;
@@ -2351,21 +2614,28 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
$item = $this;
}
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE ) as $session )
/** Item::url() can throw a LogicException exception in specific cases like when a Pages Record has no valid page */
try
{
if( mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE ) as $session )
{
$sessionData = $session;
$sessionData['location_url'] = NULL;
$sessionData['location_lang'] = NULL;
$sessionData['location_data'] = json_encode( array() );
$sessionData['current_id'] = 0;
$sessionData['location_permissions'] = 0;
if( mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
{
$sessionData = $session;
$sessionData['location_url'] = NULL;
$sessionData['location_lang'] = NULL;
$sessionData['location_data'] = json_encode( array() );
$sessionData['current_id'] = 0;
$sessionData['location_permissions'] = 0;
\IPS\Session\Store::i()->updateSession( $sessionData );
\IPS\Session\Store::i()->updateSession( $sessionData );
}
}
}
catch( \LogicException $e ){}
}
/**
@@ -2709,58 +2979,90 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
$filters = array();
$filters['author'] = $this->author()->member_id;
/* All our Zapier Triggers for content have a hidden setting, so we're using this one global for items and comments */
$filters['hidden'] = (bool) $this->hidden();
return $filters;
}
/**
* Set anonymous state
*
* @param bool $state The state, TRUE for anonymous FALSE for not
* @param \IPS\Member|NULL $member The member posting anonymously or NULL for logged in member
* @param bool $state The state, TRUE for anonymous FALSE for not
* @param \IPS\Member|NULL $member The member posting anonymously or NULL for logged in member
* @return void
*/
public function setAnonymous( $state = TRUE, $member = NULL )
public function setAnonymous( bool $state = TRUE, \IPS\Member $member = NULL)
{
if( !$this instanceof \IPS\Content\Anonymous or !$this->container()->canPostAnonymously() )
if( !$this instanceof \IPS\Content\Anonymous )
{
throw new \BadMethodCallException;
throw new \BadMethodCallException();
}
$member = $member ?: \IPS\Member::loggedIn();
if( $state == $this->isAnonymous() )
{
return;
}
$class = \get_class( $this );
$idColumn = static::$databaseColumnId;
$anonColumn = static::$databaseColumnMap['is_anon'];
if( !$state )
{
try
{
$originalAuthor = \IPS\DB::i()->select( 'anonymous_member_id', 'core_anonymous_posts', array( 'anonymous_object_class=? and anonymous_object_id=?', $class, $this->$idColumn ) )->first();
$member = \IPS\Member::load( $originalAuthor );
}
catch (\UnderflowException $e )
{
$member = NULL;
}
}
$member = $member ?: \IPS\Member::loggedIn();
if( $state and !$this->container()->canPostAnonymously( 0, $member ) )
{
throw new \BadMethodCallException();
}
if( $state )
{
{
/* Insert the anonymous map */
$save = array(
'anonymous_member_id' => $member->member_id,
'anonymous_member_id' => ( $this->author()->member_id ) ? $this->author()->member_id : $member->member_id,
'anonymous_object_class' => $class,
'anonymous_object_id' => $this->$idColumn
);
\IPS\Db::i()->replace( 'core_anonymous_posts', $save );
$member = new \IPS\Member;
$this->$anonColumn = 1;
}
else
{
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_member_id=? and anonymous_object_class=? and anonymous_object_id=?', $member->member_id, $class, $this->$idColumn ) );
\IPS\Db::i()->delete( 'core_anonymous_posts', array( 'anonymous_object_class=? and anonymous_object_id=?', $class, $this->$idColumn ) );
$this->$anonColumn = 0;
}
/* @todo only run the rest of the code here if the $anonColumn state is different from original. Waste of processing otherwise */
$this->save();
$this->changeAuthor( $member, FALSE );
if ( \in_array( 'IPS\Content\Comment', class_parents( $this ) ) )
{
$this->item()->rebuildFirstAndLastCommentData();
}
$this->expireWidgetCaches();
}
/**
* Is this an anonymous entry?
*
@@ -2770,4 +3072,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
{
return $this->mapped('is_anon');
}
/**
* Returns the content images
*
* @return array|NULL
* @throws \BadMethodCallException
*/
public function imageLabelsForSearch()
{
return [];
}
}
+201 -74
View File
@@ -47,7 +47,25 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Ensure JS loaded for forms/content functions such as moderation */
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_core.js', 'core' ) );
return parent::execute();
parent::execute();
/* Do this to prevent non-existent and non-accessible items from returning a status of 200 */
if ( empty( \IPS\Output::i()->output ) AND !\IPS\Request::i()->_bypassItemIdCheck AND isset( \IPS\Request::i()->id ) )
{
$contentModel = static::$contentModel;
try
{
$item = $contentModel::load( \IPS\Request::i()->id );
if ( !$item->can( 'read' ) )
{
\IPS\Output::i()->error( 'node_error', '2S136/1X', 403, '' );
}
}
catch ( \OutOfRangeException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/1Y', 404, '' );
}
}
}
/**
@@ -132,7 +150,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
\IPS\Output::i()->redirect( $redirectToUrl );
}
/* Get ready to store data layer info */
$dataLayer = \IPS\Settings::i()->core_datalayer_enabled;
$dataLayerProperties = array();
if ( $dataLayer )
{
$dataLayerProperties = $item->getDataLayerProperties();
}
/* Check we're on a valid page */
$paginationType = 'comment';
if ( isset( \IPS\Request::i()->tab ) and \IPS\Request::i()->tab === 'reviews' )
@@ -168,6 +194,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$lastPageMethod = 'last' . mb_ucfirst( $paginationType ) . 'PageUrl';
\IPS\Output::i()->redirect( $item->$lastPageMethod(), NULL, 303 );
}
$dataLayerProperties['page_number'] = \intval( \IPS\Request::i()->page );
}
/* Add rel tags */
@@ -211,10 +239,21 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
try
{
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
$countUpdated = true;
}
catch( \Exception $e ) {}
}
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') and $class::$includeInTrending )
{
try
{
/* Score by timestamp for trending */
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
}
catch( \Exception $e ) {}
}
if ( ! $countUpdated )
{
@@ -262,7 +301,21 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
'item_type' => isset( $class::$contentType ) ? $class::$contentType : $class::$title,
'category_name' => isset( $item::$databaseColumnMap['container'] ) ? $item->container()->_title : NULL
);
/* Data Layer */
if ( $dataLayer )
{
if ( !\IPS\Request::i()->isAjax() )
{
\IPS\core\DataLayer::i()->addEvent( 'content_view', $dataLayerProperties );
}
unset( $dataLayerProperties['ips_key'] );
foreach ( $dataLayerProperties as $key => $value )
{
\IPS\core\DataLayer::i()->addContextProperty( $key, $value );
}
}
if( $item->mapped( 'updated' ) OR $item->mapped( 'last_comment' ) OR $item->mapped( 'last_review' ) )
{
\IPS\Output::i()->metaTags['og:updated_time'] = \IPS\DateTime::ts( $item->mapped( 'updated' ) ? $item->mapped( 'updated' ) : ( $item->mapped( 'last_comment' ) ? $item->mapped( 'last_comment' ) : $item->mapped( 'last_review' ) ) )->rfc3339();
@@ -356,6 +409,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$commentClass = $class::$commentClass;
/* Don't auto-poll archived content, it never gets updated */
if( isset( $item::$archiveClass ) AND method_exists( $item, 'isArchived' ) AND $item->isArchived() )
{
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
return;
}
$commentIdColumn = $commentClass::$databaseColumnId;
$commentDateColumn = $commentClass::$databaseColumnMap['date'];
@@ -514,7 +575,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$item->processAfterEdit( $values );
/* Moderator log */
$toLog = array( $item::$title => FALSE, $item->url()->__toString() => FALSE, $item::$title => TRUE, $item->mapped( 'title' ) => FALSE );
$toLog = array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item::$title => TRUE, $item->mapped( 'title' ) => FALSE );
if ( $oldTitle != $item->$titleField )
{
@@ -723,12 +784,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$newTitle = new \IPS\Helpers\Form\Text( 'newTitle', \IPS\Request::i()->newTitle );
if( $newTitle->validate() !== TRUE or $newTitle->value !== \IPS\Request::i()->newTitle )
if( $newTitle->validate() !== TRUE )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'form_tags_not_allowed', FALSE, array( 'sprintf' => array( \IPS\Request::i()->newTitle ) ) ) );
}
$item->$titleField = \IPS\Request::i()->newTitle;
$item->$titleField = $newTitle->value;
$idField = $item::$databaseColumnId;
$item->save();
@@ -761,7 +822,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Session::i()->modLog( 'modlog__comment_edit_title', array( (string) $item->url() => FALSE, $item->$titleField => FALSE, $oldTitle => FALSE ), $item );
}
\IPS\Output::i()->json( 'OK' );
\IPS\Output::i()->json( $item->$titleField );
}
catch( \LogicException $e )
{
@@ -1557,7 +1618,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* If we do not have a time last read set for this content, fallback to the reset time */
$resetTimes = \IPS\Member::loggedIn()->markersResetTimes( $class::$application );
if ( array_key_exists( $item->container()->_id, $resetTimes ) and $item->mapped('date') < $resetTimes[ $item->container()->_id ] )
if ( ( \is_array( $resetTimes ) AND array_key_exists( $item->container()->_id, $resetTimes ) ) and $item->mapped('date') < $resetTimes[ $item->container()->_id ] )
{
$comment = $item->comments( 1, NULL, 'date', 'asc', NULL, NULL, \IPS\DateTime::ts( $resetTimes[ $item->container()->_id ] ) );
@@ -1597,7 +1658,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$error = 'node_error';
}
\IPS\Output::i()->error( $error, '2S136/V', 404, '' );
\IPS\Output::i()->error( $error, '2S136/V', 403, '' );
}
catch( \LogicException $e )
{
@@ -1666,7 +1727,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$error = 'node_error';
}
\IPS\Output::i()->error( $error, '2S136/Q', 404, '' );
\IPS\Output::i()->error( $error, '2S136/Q', 403, '' );
}
}
@@ -1708,7 +1769,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
catch( \LogicException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/X', 404, '' );
\IPS\Output::i()->error( 'node_error', '2S136/X', 403, '' );
}
}
@@ -1909,7 +1970,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
$form = new \IPS\Helpers\Form;
$form->add( new \IPS\Helpers\Form\YesNo( 'message_is_public', isset( $current['is_public'] ) ? $current['is_public'] : TRUE ) );
$form->attributes['data-controller'] = 'core.front.core.contentMessage';
$form->add( new \IPS\Helpers\Form\YesNo( 'message_is_public', isset( $current['is_public'] ) ? $current['is_public'] : FALSE ) );
$form->add( new \IPS\Helpers\Form\Editor( 'message', $current ? $current['message'] : NULL , TRUE, array( 'app' => 'core', 'key' => 'Meta', 'autoSaveKey' => $current ? "meta-message-" . \IPS\Request::i()->meta_id : "meta-message-new", 'attachIds' => $current ? array( \IPS\Request::i()->meta_id, NULL, 'core_ContentMessages' ) : NULL ) ) );
$form->add( new \IPS\Helpers\Form\Custom( 'message_color', isset( $current['color'] ) ? $current['color'] : 'none', FALSE, array( 'getHtml' => function( $element )
{
@@ -2083,7 +2145,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
if ( \IPS\Member::loggedIn()->modPermission('can_set_best_answer') )
{
\IPS\Session::i()->modLog( 'modlog__best_answer_unset', array( $item->$solvedField => FALSE ), $item );
\IPS\Session::i()->modLog( 'modlog__best_answer_unset', array( $comment->$idField => FALSE ), $item );
}
}
catch ( \Exception $e ) {}
@@ -2248,7 +2310,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
}
}
$commentPosition = \IPS\Db::i()->select( 'COUNT(*) AS position', $commentClass::$databaseTable, $where )->first();
$commentPosition = $commentClass::db()->select( 'COUNT(*) AS position', $commentClass::$databaseTable, $where )->first();
/* Now work out what page that makes it */
$url = $item->url();
@@ -2381,7 +2443,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
if ( isset( \IPS\Request::i()->restoreAsHidden ) )
{
\IPS\Session::i()->modLog( 'modlog__action_restore_hidden', array(
$comment::$title => FALSE,
$comment::$title => TRUE,
$comment->url()->__toString() => FALSE
) );
@@ -2390,7 +2452,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
else
{
\IPS\Session::i()->modLog( 'modlog__action_restore', array(
$comment::$title => FALSE,
$comment::$title => TRUE,
$comment->url()->__toString() => FALSE
) );
@@ -2452,7 +2514,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Post Anonymously */
$container = $item->containerWrapper();
if ( $container and $container->canPostAnonymously( $container::ANON_COMMENTS ) and $comment instanceof \IPS\Content\Anonymous and \IPS\Member::loggedIn()->member_id and \IPS\Member::loggedIn()->group['gbw_can_post_anonymously'] )
if ( $container and $container->canPostAnonymously( $container::ANON_COMMENTS ) and $comment instanceof \IPS\Content\Anonymous and ( $comment->author() and $comment->author()->group['gbw_can_post_anonymously'] or $comment->isAnonymous() ) )
{
$form->add ( new \IPS\Helpers\Form\YesNo( 'post_anonymously', $comment->isAnonymous(), FALSE, array( 'label' => \IPS\Member::loggedIn()->language()->addToStack( 'post_anonymously_suffix' ) ), NULL, NULL, NULL, 'post_anonymously' ) );
}
@@ -2545,11 +2607,17 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
$comment->editRating( $values['rating_value'] );
}
/* Anonymous posting */
if( isset( $values[ 'post_anonymously' ] ) )
{
$comment->setAnonymous( $values[ 'post_anonymously' ] );
/* The anon changes to the user details (photo, name, etc) is outside the JS scope to change when editing a comment */
if ( (bool) $comment->isAnonymous() !== (bool) $values['post_anonymously'] )
{
$reloadPage = true;
}
$comment->setAnonymous( $values[ 'post_anonymously' ], $comment->author() );
}
/* Moderator log */
@@ -2770,12 +2838,18 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $oldItem, $comment );
}
/* Remove solved associations */
if ( $oldItem and \IPS\IPS::classUsesTrait( $oldItem, 'IPS\Content\Solvable' ) and $oldItem->isSolved() and ( $oldItem->mapped('solved_comment_id') === $comment->$commentIdColumn ) )
{
$oldItem->toggleSolveComment( $comment->$commentIdColumn, FALSE );
}
$comment->move( $item );
$oldItem->rebuildFirstAndLastCommentData();
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_split', array(
$item::$title => FALSE,
$item::$title => TRUE,
$item->url()->__toString() => FALSE,
$item->mapped( 'title' ) => FALSE,
$oldItem->url()->__toString() => FALSE,
@@ -3239,7 +3313,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Create a copy of the old item for logging */
$oldItem = $item;
$comments = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select(
$comments = iterator_to_array( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select(
'*',
$commentClass::$databaseTable,
array(
@@ -3247,7 +3321,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Db::i()->in( $commentClass::$databasePrefix . $commentClass::$databaseColumnId, array_keys( \IPS\Request::i()->multimod ) )
),
$commentClass::$databasePrefix . $commentClass::$databaseColumnMap['date']
), $commentClass );
), $commentClass ) );
foreach ( $comments as $comment )
{
@@ -3322,6 +3396,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $oldItem, $comment );
}
/* Remove solved associations */
if ( $oldItem and \IPS\IPS::classUsesTrait( $oldItem, 'IPS\Content\Solvable' ) and $oldItem->isSolved() and ( $oldItem->mapped('solved_comment_id') === $comment->$commentIdColumn ) )
{
$oldItem->toggleSolveComment( $comment->$commentIdColumn, FALSE );
}
if( $comment == $firstComment AND $comment->hidden() !== 0 )
{
if ( isset( $comment::$databaseColumnMap['hidden'] ) )
@@ -3364,7 +3444,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_split', array(
$item::$title => FALSE,
$item::$title => TRUE,
$item->url()->__toString() => FALSE,
$item->mapped( 'title' ) => FALSE,
$oldItem->url()->__toString() => FALSE,
@@ -3460,7 +3540,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
/* Log it */
\IPS\Session::i()->modLog( 'modlog__action_merge_comments', array(
$firstComment::$title => FALSE,
$firstComment::$title => TRUE,
$firstComment->url()->__toString() => FALSE,
$firstComment->$commentIdColumn => FALSE,
), $item );
@@ -3751,64 +3831,96 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
$class = static::$contentModel;
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
$items = [];
if( !$item instanceof \IPS\Content\Tags OR ( $item->tags() === NULL AND $item->prefix( FALSE ) === NULL ) )
/* Are we using elasticsearch? */
if ( \IPS\Settings::i()->search_method == 'elastic' )
{
return NULL;
}
$query = \IPS\Content\Search\Query::init( \IPS\Member::loggedIn() );
$query->resultsToGet = $limit;
$query->filterByMoreLikeThis( $item );
$query->filterByLastUpdatedDate( \IPS\DateTime::create()->sub( new \DateInterval( 'P2Y' ) ) );
$results = $query->search();
$results->init();
/* Store tags in array, so that we can add a prefix if set */
$tags = $item->tags() ?: array();
$tags[] = $item->prefix( FALSE );
/* Build the where clause */
$where = array(
array( '(' . \IPS\Db::i()->in( 'tag_text', $tags ) . ')' ),
array( '!(tag_meta_app=? and tag_meta_area=? and tag_meta_id=?)', $class::$application, $class::$module, \IPS\Request::i()->id ),
array( '(' . \IPS\Db::i()->findInSet( 'tag_perm_text', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'tag_perm_text=? )', '*' ),
array( 'tag_perm_visible=1' )
);
/* Allow the item to manipulate the query if needed */
if( $item->similarContentFilter() )
{
$where = array_merge( $where, $item->similarContentFilter() );
}
$select = \IPS\Db::i()->select(
'tag_meta_app,tag_meta_area,tag_meta_id',
'core_tags',
$where,
'tag_added DESC',
array( 0, $limit ),
array( 'tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_added' )
)->join(
'core_tags_perms',
array( 'tag_perm_aai_lookup=tag_aai_lookup' )
);
$items = array();
foreach( $select as $result )
{
foreach( \IPS\Application::load( $result['tag_meta_app'] )->extensions( 'core', 'ContentRouter' ) as $key => $router )
foreach( $results as $result )
{
foreach( $router->classes AS $itemClass )
$object = $result->asArray()['indexData'];
$class = $object['index_class'];
$loaded = $class::loadAndCheckPerms( $object['index_object_id'] );
if ( $loaded instanceof \IPS\Content\Item )
{
if( $itemClass::$module == $result['tag_meta_area'] )
{
try
{
$items[ $result['tag_meta_id'] ] = $itemClass::loadAndCheckPerms( $result['tag_meta_id'] );
break;
}
catch( \Exception $e ){}
}
$obj = $loaded;
}
else
{
$obj = $loaded->item();
}
$items[] = $obj;
}
}
else
{
if ( !$item instanceof \IPS\Content\Tags or ( $item->tags() === NULL and $item->prefix( FALSE ) === NULL ) )
{
return NULL;
}
/* Store tags in array, so that we can add a prefix if set */
$tags = $item->tags() ?: array();
$tags[] = $item->prefix( FALSE );
/* Build the where clause */
$where = array(
array('(' . \IPS\Db::i()->in( 'tag_text', $tags ) . ')'),
array('!(tag_meta_app=? and tag_meta_area=? and tag_meta_id=?)', $class::$application, $class::$module, \IPS\Request::i()->id),
array('(' . \IPS\Db::i()->findInSet( 'tag_perm_text', \IPS\Member::loggedIn()->groups ) . ' OR ' . 'tag_perm_text=? )', '*'),
array('tag_perm_visible=1')
);
/* Allow the item to manipulate the query if needed */
if ( $item->similarContentFilter() )
{
$where = array_merge( $where, $item->similarContentFilter() );
}
$select = \IPS\Db::i()->select(
'tag_meta_app,tag_meta_area,tag_meta_id',
'core_tags',
$where,
'tag_added DESC',
array(0, $limit),
array('tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_added')
)->join(
'core_tags_perms',
array('tag_perm_aai_lookup=tag_aai_lookup')
);
foreach ( $select as $result )
{
foreach ( \IPS\Application::load( $result['tag_meta_app'] )->extensions( 'core', 'ContentRouter' ) as $key => $router )
{
foreach ( $router->classes as $itemClass )
{
if ( $itemClass::$module == $result['tag_meta_area'] )
{
try
{
$items[$result['tag_meta_id']] = $itemClass::loadAndCheckPerms( $result['tag_meta_id'] );
break;
}
catch ( \Exception $e )
{
}
}
}
}
}
}
return $items;
}
catch ( \Exception $e )
@@ -3905,7 +4017,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
}
catch ( \UnderflowException $e )
{
\IPS\Output::i()->error( 'node_error', '2S136/1W', 403, '' );
if( \IPS\Request::i()->isAjax() )
{
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicHover( \IPS\Member::loggedIn()->language()->addToStack( 'anon_user_deleted' ) );
return;
}
\IPS\Output::i()->error( 'anon_user_deleted', '2S136/1W', 403, '' );
}
$author = \IPS\Member::load( $anonymousAuthor );
@@ -3935,4 +4052,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
{
$this->reveal();
}
/**
* A convenient hook point to finish any set up in manage()
*
* @param \IPS\Content\Item $item The item that is being set up in manage()
* @return void
*/
protected function finishManage( $item )
{
}
}
+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;
}
}
+13 -3
View File
@@ -96,6 +96,11 @@ class _Facebook extends PromoteAbstract
{
$facebook = static::getLoginHandler();
if ( $facebook === NULL )
{
return FALSE;
}
$authorizedScopes = $facebook->authorizedScopes( $this->member );
if( $authorizedScopes === NULL )
@@ -165,7 +170,7 @@ class _Facebook extends PromoteAbstract
{
foreach( $this->settings['shareable']['pages'] as $id => $data )
{
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
$names[] = $data['name'];
}
}
@@ -174,7 +179,7 @@ class _Facebook extends PromoteAbstract
{
foreach( $this->settings['shareable']['groups'] as $id => $data )
{
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
$names[] = $data['name'];
}
}
@@ -277,7 +282,7 @@ class _Facebook extends PromoteAbstract
\IPS\Log::log( $e, 'facebook' );
}
if ( ! $photos or \count( $photos ) === 1 )
if ( ! $photos or ( \is_array( $photos ) and \count( $photos ) === 1 ) )
{
/* Simple message */
try
@@ -450,6 +455,11 @@ class _Facebook extends PromoteAbstract
{
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
if ( $facebook === NULL )
{
return NULL;
}
/* We are using the default app here */
if ( ! \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
{
+7 -2
View File
@@ -106,6 +106,11 @@ class _Twitter extends PromoteAbstract
public function canPostToPage()
{
$twitter = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth1\Twitter');
if ( $twitter === NULL )
{
return FALSE;
}
return $twitter->hasWritePermissions( $this->settings['token'], $this->settings['secret'] );
}
@@ -194,8 +199,8 @@ class _Twitter extends PromoteAbstract
{
$photos = $promote->imageObjects();
$mediaIds = array();
if ( \count( $photos ) )
if ( \is_array( $photos ) and \count( $photos ) )
{
try
{
+18 -2
View File
@@ -152,6 +152,22 @@ trait Reactable
$this->_reactions = NULL;
$this->hasReacted[ $member->member_id ] = $reaction;
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('realtime') )
{
$this->reactions();
\IPS\cloud\Realtime::i()->publishEvent( 'reaction-count', array('count' => $this->_reactionCount) );
}
if( \IPS\Application::appIsEnabled( 'cloud' ) and \IPS\cloud\Realtime::i()->isEnabled('trending') )
{
/* We need to make sure we're using the item */
$item = ( ! $this instanceof \IPS\Content\Item ) ? $this->item() : $this;
$itemIdColumn = $item::$databaseColumnId;
$itemId = $item->$itemIdColumn;
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
}
}
/**
@@ -426,9 +442,9 @@ trait Reactable
try
{
$class = $row['rep_class'];
$comment = $class::load( $row['type_id'] );
$item = $class::load( $row['type_id'] );
return \IPS\Member::loggedIn()->language()->addToStack( '_defart_from_date', NULL, array( 'htmlsprintf' => array( $comment->url(), $comment::$title, \IPS\DateTime::ts( $date )->html() ) ) );
return \IPS\Member::loggedIn()->language()->addToStack( '_defart_from_date', FALSE, array( 'htmlsprintf' => array( $item->url(), $item->mapped('title'), \IPS\DateTime::ts( $date )->html() ) ) );
}
catch( \Exception $e )
{
+13 -7
View File
@@ -72,6 +72,12 @@ trait Recognizable
*/
public function canRecognize( ?\IPS\Member $member=NULL ): bool
{
/* Are achievements enabled? */
if ( !\IPS\Settings::i()->achievements_enabled )
{
return FALSE;
}
/* Already recognized? */
if ( isset( $this->recognized ) )
{
@@ -161,38 +167,38 @@ trait Recognizable
public function recognizedBlurb(): array
{
$return = [ 'message' => NULL ];
$return['main'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_main', NULL, [ 'htmlsprintf' => [ $this::$title, $this->recognized->_given_by->link() ] ] );
$return['main'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_main', FALSE, [ 'htmlsprintf' => [ $this::$title, $this->recognized->_given_by->link() ] ] );
if ( $this->recognized->badge and $this->recognized->points )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge', NULL, ['htmlsprintf' => [$this->recognized->badge()->_title, $this->recognized->points]] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge', FALSE, ['htmlsprintf' => [$this->recognized->badge()->_title, $this->recognized->points]] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge_third', NULL, ['htmlsprintf' => [$this->author()->name, $this->recognized->badge()->_title, $this->recognized->points]] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_and_badge_third', FALSE, ['htmlsprintf' => [$this->author()->name, $this->recognized->badge()->_title, $this->recognized->points]] );
}
}
else if ( $this->recognized->points )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_points', NULL, [ 'sprintf' => [ $this->recognized->points ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points', FALSE, [ 'sprintf' => [ $this->recognized->points ] ] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_points_third', NULL, [ 'sprintf' => [ $this->author()->name, $this->recognized->points ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_points_third', FALSE, [ 'sprintf' => [ $this->author()->name, $this->recognized->points ] ] );
}
}
else if ( $this->recognized->badge )
{
if ( $this->author()->member_id === \IPS\Member::loggedIn()->member_id )
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_badge', NULL, [ 'htmlsprintf' => [ $this->recognized->badge()->_title ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_badge', FALSE, [ 'htmlsprintf' => [ $this->recognized->badge()->_title ] ] );
}
else
{
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack('recognize_blurb_awarded_badge_third', NULL, [ 'htmlsprintf' => [ $this->author()->name, $this->recognized->badge()->_title ] ] );
$return['awards'] = \IPS\Member::loggedIn()->language()->addToStack( 'recognize_blurb_awarded_badge_third', FALSE, [ 'htmlsprintf' => [ $this->author()->name, $this->recognized->badge()->_title ] ] );
}
}
+2 -1
View File
@@ -44,10 +44,11 @@ abstract class _Review extends \IPS\Content\Comment
* @param \IPS\DateTime|NULL $time The time
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
* @return static
* @throws \InvalidArgumentException
*/
public static function create( $item, $comment, $first=FALSE, $rating=NULL, $guestName=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
public static function create( $item, $comment, $first=FALSE, $rating=NULL, $guestName=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
{
if ( !\is_int( $rating ) or $rating < 1 or $rating > \intval( \IPS\Settings::i()->reviews_rating_out_of ) )
{
+71 -57
View File
@@ -25,8 +25,8 @@ class _Index extends \IPS\Content\Search\Index
/**
* @brief Elasticsearch version requirements
*/
const MINIMUM_VERSION = '6.0.0';
const UNSUPPORTED_VERSION = '7.7.0';
const MINIMUM_VERSION = '7.2.0';
const UNSUPPORTED_VERSION = '8.0.0';
/**
* @brief The server URL
@@ -61,7 +61,7 @@ class _Index extends \IPS\Content\Search\Index
$analyzer = key( $settings['analysis']['analyzer'] );
}
$this->url->request()->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
$definition = array(
'settings' => $settings,
@@ -106,6 +106,8 @@ class _Index extends \IPS\Content\Search\Index
'index_comments' => array( 'type' => 'long' ),
'index_reviews' => array( 'type' => 'long' ),
'index_participants' => array( 'type' => 'long' ),
'index_is_anon' => array( 'type' => 'byte' ),
'index_item_solved' => array( 'type' => 'byte' )
)
)
)
@@ -113,7 +115,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$response = $this->url->setQueryString( 'include_type_name', 'true' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
if( $response->httpResponseCode != 200 )
{
@@ -122,7 +124,7 @@ class _Index extends \IPS\Content\Search\Index
}
catch( \Exception $e )
{
$response = $this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
}
}
@@ -137,16 +139,15 @@ class _Index extends \IPS\Content\Search\Index
if ( $indexData = parent::indexData( $object ) )
{
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
$indexData['index_tags'] = $indexData['index_tags'] ? explode( ',', $indexData['index_tags'] ) : array();
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
if ( $object instanceof \IPS\Content\Item )
{
$indexData += $this->metaCounts( $object );
$indexData = array_merge( $indexData, $this->metaData( $object ) );
}
else
{
$indexData += $this->metaCounts( $object->item() );
$indexData = array_merge( $indexData, $this->metaData( $object->item() ) );
}
return $indexData;
@@ -181,7 +182,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
if ( $r['found'] )
{
$existingData = $r['_source'];
@@ -204,9 +205,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Insert into index */
$r = $this->url
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )
->request( \IPS\LONG_REQUEST_TIMEOUT )
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->put( json_encode( $indexData ) );
@@ -215,6 +214,11 @@ class _Index extends \IPS\Content\Search\Index
throw new \IPS\Content\Search\Elastic\Exception( $error['type'] . ': ' . $error['reason'] );
}
if ( $resetLastComment )
{
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
}
/* Views / Comments / Reviews */
if ( $object instanceof \IPS\Content\Item )
{
@@ -224,7 +228,8 @@ class _Index extends \IPS\Content\Search\Index
{
$item = $object->item();
}
$this->rebuildMetaCounts( $item, $indexData['index_is_last_comment'] ? $this->getIndexId( $object ) : NULL );
$this->rebuildMetaData( $item );
}
catch ( \IPS\Http\Request\Exception $e )
{
@@ -234,11 +239,6 @@ class _Index extends \IPS\Content\Search\Index
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
}
if ( $resetLastComment )
{
$this->resetLastComment( array( $indexData['index_class'] ), $indexData['index_item_id'] );
}
}
}
}
@@ -253,7 +253,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
/* This request *intentionally* goes to _tasks and not (ourpath)/_tasks */
$response = $this->url->setPath( '/_tasks' )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_tasks' ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
if( $error = $this->getResponseError( $response ) )
{
@@ -278,7 +278,7 @@ class _Index extends \IPS\Content\Search\Index
continue;
}
$this->url->setPath( '/.tasks/task' . $taskId )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
}
}
}
@@ -298,7 +298,7 @@ class _Index extends \IPS\Content\Search\Index
* @param \IPS\Content\Searchable $item The content item
* @return void
*/
protected function metaCounts( $item )
protected function metaData( $item )
{
$databaseColumnId = $item::$databaseColumnId;
@@ -318,7 +318,9 @@ class _Index extends \IPS\Content\Search\Index
return array(
'index_comments' => $item->mapped('num_comments'),
'index_reviews' => $item->mapped('num_reviews'),
'index_participants' => $participants
'index_participants' => $participants,
'index_tags' => $item->tags(),
'index_prefix' => $item->prefix(),
);
}
@@ -326,10 +328,9 @@ class _Index extends \IPS\Content\Search\Index
* Rebuild the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @param string $lastCommentIndexId The index ID of the last comment, if you want to reset others
* @return void
*/
protected function rebuildMetaCounts( $item, $lastCommentIndexId = NULL )
protected function rebuildMetaData( $item )
{
$databaseColumnId = $item::$databaseColumnId;
$class = \get_class( $item );
@@ -347,12 +348,12 @@ class _Index extends \IPS\Content\Search\Index
{
$updates = array();
$params = array();
foreach ( $this->metaCounts( $item ) as $k => $v )
foreach ( $this->metaData( $item ) as $k => $v )
{
if ( \is_array( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params[ 'param_' . $k ] = array_map( 'intval', $v );
$params[ 'param_' . $k ] = ( $k !== 'index_tags' ? array_map( 'intval', $v ) : $v );
}
elseif ( \is_null( $v ) )
{
@@ -365,12 +366,8 @@ class _Index extends \IPS\Content\Search\Index
$params['param_' . $k ] = \intval( $v );
}
}
if ( !\is_null( $lastCommentIndexId ) )
{
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == params.param_lastcommentid );";
$params['param_lastcommentid'] = $lastCommentIndexId;
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -429,7 +426,7 @@ class _Index extends \IPS\Content\Search\Index
if ( !( $object instanceof \IPS\Content\Item ) )
{
$this->rebuildMetaCounts( $object->item() );
$this->rebuildMetaData( $object->item() );
$itemClass = \get_class( $object->item() );
$itemIdColumn = $itemClass::$databaseColumnId;
@@ -465,14 +462,14 @@ class _Index extends \IPS\Content\Search\Index
try
{
$indexId = \strtolower( str_replace( '\\', '_', \substr( $class, 4 ) ) ) . '-' . $id;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId )->request()->delete();
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -495,7 +492,7 @@ class _Index extends \IPS\Content\Search\Index
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -565,15 +562,15 @@ class _Index extends \IPS\Content\Search\Index
'index_author' => $authorId
)
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => $query
) ) );
}
else
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'term' => array(
'index_class' => $class
@@ -677,7 +674,7 @@ class _Index extends \IPS\Content\Search\Index
if ( \count( $updates ) )
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -697,8 +694,8 @@ class _Index extends \IPS\Content\Search\Index
'index_author' => 0
)
);
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_permissions.add( 'm' + ctx._source.index_author );",
'lang' => 'painless'
@@ -727,8 +724,8 @@ class _Index extends \IPS\Content\Search\Index
)
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_hidden = params.newHiddenStatus;",
'lang' => 'painless',
@@ -766,7 +763,7 @@ class _Index extends \IPS\Content\Search\Index
{
return array(
'bool' => array(
'must' => array( $conditions )
'must' => $conditions
)
);
}
@@ -795,7 +792,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => $update
) ) );
@@ -827,7 +824,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'range' => array(
'index_date_updated' => array(
@@ -861,7 +858,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
/* Remove the flag */
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_is_last_comment = false;",
'lang' => 'painless'
@@ -921,15 +918,15 @@ class _Index extends \IPS\Content\Search\Index
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND $lastComment instanceof \IPS\Content\Searchable )
{
/* Set that it is the latest comment */
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $lastComment ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update/' . $this->getIndexId( $lastComment ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => array(
'index_is_last_comment' => true
)
) ) );
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
$indexDataForLastComment = $this->indexData( $lastComment );
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => "ctx._source.index_date_updated = params.dateUpdated; ctx._source.index_date_commented = params.dateCommented;",
'lang' => 'painless',
@@ -1001,7 +998,7 @@ class _Index extends \IPS\Content\Search\Index
);
/* Get the count */
$count = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$count = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 0,
'query' => $query
) ) )->decodeJson();
@@ -1012,7 +1009,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Now get the unique item ids */
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'aggs' => array(
'itemIds' => array(
'terms' => array(
@@ -1054,7 +1051,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'query' => array(
'bool' => array(
'filter' => array(
@@ -1103,7 +1100,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 1,
'sort' => array( array( 'index_date_updated' => 'asc' ) )
) ) )->decodeJson();
@@ -1139,7 +1136,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$results = $this->url->setPath( '/_analyze' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'analyzer' => $analyzer,
'text' => implode( ' ', $terms )
) ) )->decodeJson();
@@ -1172,6 +1169,23 @@ class _Index extends \IPS\Content\Search\Index
return FALSE;
}
/**
* Wrapper to account for log in where needed
*
* @param $url
* @param null $timeout
* @return \IPS\Http\Request\Curl|Socket
*/
public static function request( $url, $timeout=NULL )
{
if ( \IPS\ELASTICSEARCH_USER or \IPS\ELASTICSEARCH_PASSWORD )
{
return $url->request( $timeout )->login( \IPS\ELASTICSEARCH_USER, \IPS\ELASTICSEARCH_PASSWORD );
}
return $url->request( $timeout );
}
/**
* Check response to see if an error was produced
*
@@ -73,7 +73,7 @@ class _MassIndexer extends \IPS\Content\Search\Elastic\Index
}
$json = implode( "\n", $json );
$this->url->setPath( '/_bulk' )->request()->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
}
}
}
+173 -15
View File
@@ -69,7 +69,12 @@ class _Query extends \IPS\Content\Search\Query
parent::__construct( $member );
$this->url = $url;
}
/**
* @var bool Stores whether or not any content type needs to be limited by the latest comment
*/
protected $lastCommentMustBeTrue = false;
/**
* Filter by multiple content types
*
@@ -198,11 +203,14 @@ class _Query extends \IPS\Content\Search\Query
/* Only last comment? */
if ( $filter->onlyLastComment )
{
/* Set this for when the search JSON is built */
$this->lastCommentMustBeTrue = true;
$conditions[] = array(
'term' => array( 'index_is_last_comment' => true )
);
}
/* Put it together */
if( \count( $conditions ) )
{
@@ -215,7 +223,7 @@ class _Query extends \IPS\Content\Search\Query
{
$this->filters[] = array(
'bool' => array(
( $type ? 'should' : 'must_not' ) => array( $contentFilterConditions )
( $type ? 'should' : 'must_not' ) => $contentFilterConditions
)
);
}
@@ -363,7 +371,79 @@ class _Query extends \IPS\Content\Search\Query
/* Return for daisy chaining */
return $this;
}
/**
* Stores the "more like this" Content object for search()
* @param null
*/
protected $moreLikeThis = NULL;
/**
* Filter by more like this
* @param \IPS\Content\Searchable $object
*/
public function filterByMoreLikeThis( \IPS\Content\Searchable $object )
{
$index = new \IPS\Content\Search\Elastic\Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof \IPS\Content\Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
/* Some container types cannot be cached */
$classes = [];
$conditions = [];
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
/* Some nodes have complex permissions, so avoid any permission issues by not selecting these */
foreach( $classes as $itemClass )
{
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
$blockIds = [];
if ( $customNodes = $containerClass::customPermissionNodes() )
{
foreach ( $customNodes as $key => $ids )
{
if ( $key !== 'count' )
{
$blockIds = array_merge( $blockIds, $ids );
}
}
}
if ( \count( $blockIds ) )
{
$conditions[] = array(
'bool' => array(
'filter' => array(
array(
'terms' => array( 'index_container_class' => [ $containerClass ] )
),
array(
'terms' => array( 'index_container_id' => $blockIds )
),
)
)
);
}
if ( \count( $conditions ) )
{
foreach( $conditions as $condition )
{
$this->mustNot[] = $condition;
}
}
}
}
/* Only show non-hidden and approved items */
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
}
/**
* Filter by container class
*
@@ -558,9 +638,7 @@ class _Query extends \IPS\Content\Search\Query
if ( $includeMembers and $followed = iterator_to_array( \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_member_id=?', 'core', 'member', $this->member->member_id ), 'follow_rel_id asc' ) ) )
{
$conditions[] = array(
array(
'terms' => array( 'index_author' => $followed )
)
'terms' => array( 'index_author' => $followed )
);
}
@@ -799,7 +877,35 @@ class _Query extends \IPS\Content\Search\Query
return $this;
}
/**
* Filter by solved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterBySolved()
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 1 )
);
return $this;
}
/**
* Filter by Unsolved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByUnsolved()
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 0 )
);
return $this;
}
/**
* Filter by start date
*
@@ -922,7 +1028,35 @@ class _Query extends \IPS\Content\Search\Query
return $this;
}
/**
* Debug by itemId, used for debugging purposes only
* @note No permission checks run, do not use in production
* @param $itemId
* @return void
*/
public function debugByItemId( $itemId )
{
$array = array(
'query' => [
'term' => [
'index_item_id' => [
'value' => \intval( $itemId )
]
]
],
'sort' => $this->sort ?: array(),
'from' => 0,
'size' => 50,
);
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
return \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
}
/**
* Search
*
@@ -1017,6 +1151,12 @@ class _Query extends \IPS\Content\Search\Query
)
)
);
/* If we're not searching with a term, then just show the title record, not comments */
if ( $term === NULL and ! $this->lastCommentMustBeTrue )
{
$must[] = ['exists' => ['field' => 'index_title']];
}
}
/* Put that with the rest of the conditions */
@@ -1050,7 +1190,25 @@ class _Query extends \IPS\Content\Search\Query
$filters[] = array( 'term' => array( 'index_hidden' => $this->hiddenStatuses ) );
}
}
$searchUrl = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' );
if ( $this->moreLikeThis )
{
$must[] = [
'more_like_this' => [
'fields' => [ 'index_title' ],
'like' => [ '_index' => \IPS\Settings::i()->search_elastic_index, '_id' => $this->moreLikeThis ],
'min_term_freq' => 1,
'max_query_terms' => 12,
'min_word_length' => 3,
]
];
/* Let's enforce a relatively short timeout to prevent slow ES server from dragging down topics */
$searchUrl = $searchUrl->setQueryString( 'timeout', '3s' );
}
/* Peform the search */
try
{
@@ -1062,9 +1220,9 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $filters
)
);
/* Add the time decay */
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort )
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort and ! $this->sort )
{
$query = array(
'function_score' => array(
@@ -1081,7 +1239,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Add the self boost */
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort )
if ( \IPS\Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort and ! $this->moreLikeThis )
{
$query = array(
'function_score' => array(
@@ -1117,10 +1275,10 @@ class _Query extends \IPS\Content\Search\Query
}
/* Make the call! */
$return = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
$return = \IPS\Content\Search\Elastic\Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
{
\IPS\Log::log( print_r( $return['error'], TRUE ), 'elasticsearch' );
\IPS\Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
}
+12 -1
View File
@@ -309,6 +309,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
/* Get the item index ID */
$itemIndexId = NULL;
$itemClass = NULL;
if ( $object instanceof \IPS\Content\Comment )
{
$itemClass = $object::$itemClass;
@@ -390,6 +391,15 @@ abstract class _Index extends \IPS\Patterns\Singleton
$containerClass = ( $object->item()->searchIndexContainerClass() ) ? \get_class( $object->item()->searchIndexContainerClass() ) : NULL;
}
/* Do we have an extension to modify this? */
foreach( \IPS\Application::enabledApplications() as $app )
{
foreach ( $app->extensions( 'core', 'SearchIndex' ) as $extension )
{
$content = $extension->content( $object, $content );
}
}
/* Return */
return array(
'index_class' => $class,
@@ -412,7 +422,8 @@ abstract class _Index extends \IPS\Patterns\Singleton
'index_is_last_comment' => $isLastComment,
'index_club_id' => $clubId,
'index_class_type_id_hash' => md5( $class . ':' . $object->$idColumn ),
'index_is_anon' => $object->isAnonymous()
'index_is_anon' => (int) $object->isAnonymous(),
'index_item_solved' => ( $itemClass and \IPS\IPS::classUsesTrait( $itemClass, 'IPS\Content\Solvable' ) ) ? ( $object->item()->mapped('solved_comment_id') == $object->$idColumn ) ? 1 : 0 : NULL
);
}
+73 -65
View File
@@ -609,7 +609,31 @@ class _Query extends \IPS\Content\Search\Query
$this->filterByUnread = TRUE;
}
/**
* Filter only solved content
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterBySolved()
{
$this->where[] = array( 'index_item_solved=?', 1 );
return $this;
}
/**
* Filter only unsolved content
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByUnsolved()
{
$this->where[] = array( 'index_item_solved=?', 0 );
return $this;
}
/**
* Get the 'unread' where SQL
*
@@ -629,54 +653,61 @@ class _Query extends \IPS\Content\Search\Query
$unreadWheres = array();
$containerIds = array();
$containerClass = isset( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
foreach( $resetTimes as $containerId => $timestamp )
{
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
{
if ( ! $containerClass::isValidContainerId( $containerId ) )
{
continue;
}
}
$timestamp = $timestamp ?: $this->member->marked_site_read;
$containerIds[] = $containerId;
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
if ( \is_array( $resetTimes ) )
{
foreach( $resetTimes as $containerId => $timestamp )
{
foreach( $items as $mid => $mtime )
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
{
if ( $mtime > $timestamp )
if ( ! $containerClass::isValidContainerId( $containerId ) )
{
/* If an item has been moved from one container to another, the user may have a marker
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( \in_array( $mid, $markers ) )
continue;
}
}
$timestamp = $timestamp ?: $this->member->marked_site_read;
$containerIds[] = $containerId;
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
{
foreach( $items as $mid => $mtime )
{
if ( $mtime > $timestamp )
{
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
/* If an item has been moved from one container to another, the user may have a marker
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( \in_array( $mid, $markers ) )
{
unset( $markers[ $_key ] );
}
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
else
{
$mtime = $_mtime;
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
{
unset( $markers[ $_key ] );
}
/* If the existing timestamp is higher, retain that since we reset the $markers array below */
else
{
$mtime = $_mtime;
}
}
$markers[ $mtime . '.' . $mid ] = $mid;
}
$markers[ $mtime . '.' . $mid ] = $mid;
}
}
}
}
else
{
$unreadWheres[] = "( index_date_updated > " . \intval( $resetTimes ) . ")";
}
if( \count( $containerIds ) )
@@ -972,37 +1003,13 @@ class _Query extends \IPS\Content\Search\Query
$results = iterator_to_array( \IPS\Db::i()->select( 'index_item_id, index_class', array( 'core_search_index_item_map', 'sub' ), array( array( 'index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . \IPS\Db::i()->in( 'index_class', $classes ) ) ), 'index_item_id desc', array( 0, 501 ) ) );
if ( \count( $results ) == 501 )
if ( \count( $results ) )
{
/* Assume we have loads of matches, so do a full query */
$subClause = array( array( 'sub.index_item_id=main.index_item_id AND index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . $this->makeEmbeddedIfClauseForSearchMapTable('sub.index_class') ) );
$query = \IPS\Db::i()->select( 'index_item_id', array( 'core_search_index_item_map', 'sub' ), $subClause );
$where[] = array( 'EXISTS(?)', $query );
}
else if ( \count( $results ) )
{
$classIds = array();
foreach( $results as $result )
{
$classIds[ $result['index_class'] ][] = $result['index_item_id'];
}
$subWhere = array();
foreach( $classIds as $class => $ids )
{
$stack = array( $class );
if ( isset( $class::$commentClass ) )
{
$stack[] = $class::$commentClass;
}
$subWhere[] = '(' . \IPS\Db::i()->in( 'index_class', $stack ) . ' and ' . \IPS\Db::i()->in( 'index_item_id', $ids ) . ')';
}
/* IN is not a SIN. It's been a long day */
$where[] = '(' . implode( ' OR ', $subWhere ) . ')';
}
else
{
/* There are no results */
@@ -1260,8 +1267,9 @@ class _Query extends \IPS\Content\Search\Query
{
$order .= ", index_date_updated";
}
if( $this->filterByUnread === TRUE )
/* 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';
}
+14
View File
@@ -174,6 +174,20 @@ abstract class _Query
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByUnread();
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterBySolved();
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByUnsolved();
/**
* Filter by start date
+1 -1
View File
@@ -138,7 +138,7 @@ class _Content extends \IPS\Content\Search\Result
catch( \Exception $ex ) { }
}
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), \IPS\Content\Search\Result::preDisplay( $this->indexData['index_content'] ), \IPS\DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
}
/* If the search result was orphaned, let us continue */
catch( \OutOfRangeException $e ){}
+3 -5
View File
@@ -38,12 +38,10 @@ abstract class _Result
if ( \IPS\Settings::i()->emoji_style == 'twemoji' )
{
return preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
}
else
{
return $content;
$content = preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
}
return $content;
}
/**
+56 -3
View File
@@ -56,6 +56,11 @@ class _Results extends \ArrayIterator
* @brief Reaction Data
*/
protected $reactions = array();
/**
* @brief Attached Images
*/
protected $attachedImages = array();
/**
* @brief Review Ratings
@@ -100,7 +105,8 @@ class _Results extends \ArrayIterator
$reviewRatings = array();
$itemIndexIds = array();
$indexIds = array();
$attachedImages = array();
/* Loop */
foreach ( $this->getArrayCopy() as $result )
{
@@ -162,8 +168,10 @@ class _Results extends \ArrayIterator
{
$containersToLoad[ 'IPS\Member\Club' ][ $result['index_club_id'] ] = $result['index_club_id'];
}
$attachedImages[ $result['index_class'] ][ $result['index_item_id'] ][] = $result['index_object_id'];
}
/* Load item data */
foreach ( $itemsToLoad as $itemClass => $itemIds )
{
@@ -245,7 +253,47 @@ class _Results extends \ArrayIterator
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag) as index_tags', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
$this->prefixes = iterator_to_array( \IPS\Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
}
/* Load attached images */
$clause = [];
$binds = [];
$locationKeyMap = [];
foreach ( $attachedImages as $class => $ids )
{
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
foreach( $ids as $indexId => $commentIds )
{
foreach( $commentIds as $commentId )
{
$locationKey = $itemClass::$application . '_' . mb_ucfirst( $itemClass::$module );
$locationKeyMap[$locationKey] = $class;
$clause[] = "( location_key=? AND id1=? AND id2=?)";
$binds[] = $locationKey;
$binds[] = $indexId;
$binds[] = $commentId;
}
}
}
if ( \count( $attachedImages ) and \count( $clause ) )
{
/* As we need to join back onto the map table, 2 queries are much more efficient */
$ids = iterator_to_array( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) ) ) );
foreach ( \IPS\Db::i()->select( '*', 'core_attachments', array( array( \IPS\Db::i()->in( 'attach_id', $ids ) ), array('attach_is_image=1') ), 'attach_id ASC' )
->join( 'core_attachments_map', 'core_attachments_map.attachment_id=core_attachments.attach_id' ) as $row )
{
if ( isset( $locationKeyMap[ $row['location_key'] ] ) and isset( $row['id2'] ) )
{
$this->attachedImages[ $locationKeyMap[ $row['location_key'] ] ][ $row['id2'] ][] = [
'extension' => 'core_Attachment',
'location' => $row['attach_location'],
'thumb_location' => $row['attach_thumb_location'],
'labels' => implode( ', ', \IPS\Text\Parser::getAttachmentLabels( $row ) )
];
}
}
}
/* Set that we're initiated */
$this->initated = TRUE;
}
@@ -303,6 +351,11 @@ class _Results extends \ArrayIterator
}
}
if ( \is_array( $this->attachedImages ) and isset( $this->attachedImages[ $class ][ $data['index_object_id'] ] ) )
{
$itemData['attachedImages'] = $this->attachedImages[ $class ][ $data['index_object_id'] ];
}
if ( isset( $this->tags[ $data['index_item_index_id'] ] ) )
{
$data['index_tags'] = $this->tags[ $data['index_item_index_id'] ]['index_tags'];
+5
View File
@@ -81,6 +81,8 @@ trait Solvable
$oldComment = $commentClass::load( $this->$solvedField );
$oldComment->setSolved( FALSE );
$oldComment->save();
\IPS\Db::i()->delete( 'core_solved_index', array( 'comment_class=? AND comment_id=?', $commentClass, $oldComment->$commentIdField ) );
}
catch( \Exception $e ) { }
}
@@ -134,6 +136,9 @@ trait Solvable
\IPS\Member::load( $memberToRecount )->recountNotifications();
}
}
/* Update search index */
\IPS\Content\Search\Index::i()->index( $comment );
}
/**
+33 -20
View File
@@ -83,6 +83,7 @@ trait Statistics
catch( \OutOfRangeException $e )
{
$where = $this->_getVisibleWhere();
$where[] = [ $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '!=?', 0];
$members = iterator_to_array( \IPS\Db::i()->select( "count(*) as sum, {$authorColumn}", $commentClass::$databaseTable, $where, 'sum DESC', array( 0, $count ), array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] ) ) );
@@ -108,6 +109,11 @@ trait Statistics
$return[] = array( 'member' => $member, 'count' => $counts[ $member->member_id ] );
}
usort($return, function ( $member, $member2 )
{
return $member2['count'] <=> $member['count'];
});
return $return;
}
@@ -247,7 +253,7 @@ trait Statistics
public function clearCachedStatistics()
{
$idField = static::$databaseColumnId;
\IPS\Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) );
\IPS\Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) );
}
/**
@@ -378,17 +384,19 @@ trait Statistics
*/
protected function _getCached( $key )
{
if ( static::$cachedActivity === NULL )
$idField = static::$databaseColumnId;
$class = \get_class( $this );
$arrayKey = $class.'.'.$this->$idField;
if ( !isset( static::$cachedActivity[$arrayKey] ) )
{
$idField = static::$databaseColumnId;
try
{
$cache = \IPS\Db::i()->select( '*', 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ) )->first();
$cache = \IPS\Db::i()->select( '*', 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', $class, $this->$idField ) )->first();
if ( $cache['cache_added'] > time() - 86400 )
{
static::$cachedActivity = json_decode( $cache['cache_contents'], TRUE );
static::$cachedActivity[ $arrayKey ] = json_decode( $cache['cache_contents'], TRUE );
}
else
{
@@ -398,18 +406,18 @@ trait Statistics
catch( \UnderflowException $e ) { }
}
if ( static::$cachedActivity !== NULL and isset( static::$cachedActivity[ $key ] ) )
if ( isset(static::$cachedActivity[ $arrayKey ] ) and isset( static::$cachedActivity[ $arrayKey ][ $key ] ) )
{
return static::$cachedActivity[ $key ];
return static::$cachedActivity[ $arrayKey ][ $key ];
}
else
{
if( static::$cachedActivity === NULL )
if( !isset( static::$cachedActivity[ $arrayKey ] ) )
{
static::$cachedActivity = array();
static::$cachedActivity[ $arrayKey ] = array();
}
static::$cachedActivity[ $key ] = array();
static::$cachedActivity[ $arrayKey ][ $key ] = array();
throw new \OutOfRangeException;
}
}
@@ -435,10 +443,12 @@ trait Statistics
catch( \Exception $e ) { }
$idField = static::$databaseColumnId;
static::$cachedActivity[ $key ] = $value;
$class = \get_class( $this );
$arrayKey = $class.'.'.$this->$idField;
$this->storeCache = $this->$idField;
static::$cachedActivity[ $arrayKey ][ $key ] = $value;
$this->storeCache[ $arrayKey ][ $key ] = $this->$idField;
}
/**
@@ -450,12 +460,15 @@ trait Statistics
{
if( $this->storeCache )
{
\IPS\Db::i()->insert( 'core_item_statistics_cache', array(
'cache_class' => \get_class( $this ),
'cache_item_id' => $this->storeCache,
'cache_contents' => json_encode( static::$cachedActivity ),
'cache_added' => time()
), TRUE );
foreach( $this->storeCache as $key => $data )
{
\IPS\Db::i()->insert( 'core_item_statistics_cache', array(
'cache_class' => \get_class( $this ),
'cache_item_id' => str_replace( \get_class($this) . '.', '', $key),
'cache_contents' => json_encode( static::$cachedActivity[$key] ),
'cache_added' => time()
), TRUE );
}
}
if( \is_callable( 'parent::__destruct' ) )
+3 -2
View File
@@ -201,7 +201,8 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
/* Sort */
$sortOptions = $class::getWidgetSortOptions();
$return['sort'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_on', isset( $this->configuration['widget_feed_sort_on'] ) ? $this->configuration['widget_feed_sort_on'] : $class::$databaseColumnMap['updated'], FALSE, array( 'options' => $sortOptions ), NULL, NULL, NULL, 'widget_feed_sort_on' );
$dateColumn = $class::$databasePrefix . ( isset( $class::$databaseColumnMap['updated'] ) ? $class::$databaseColumnMap['updated'] : $class::$databaseColumnMap['date'] );
$return['sort'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_on', $this->configuration['widget_feed_sort_on'] ?? $dateColumn, FALSE, array( 'options' => $sortOptions ), NULL, NULL, NULL, 'widget_feed_sort_on' );
$return['direction'] = new \IPS\Helpers\Form\Select( 'widget_feed_sort_dir', isset( $this->configuration['widget_feed_sort_dir'] ) ? $this->configuration['widget_feed_sort_dir'] : 'desc', FALSE, array(
'options' => array(
@@ -439,7 +440,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
$binds = array( $class::$application, $class::$module );
foreach( $this->configuration['widget_feed_tags'] as $tag )
{
$tagWhere[] = "( tag_text LIKE CONCAT( ?, '%') )";
$tagWhere[] = "( tag_text=? )";
$binds[] = $tag;
}