Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+34 -21
View File
@@ -11,23 +11,36 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Api\PaginatedResponse;
use IPS\Content\Reaction;
use IPS\Db;
use IPS\Http\Url;
use IPS\Member;
use IPS\Request;
use IPS\Settings;
use OutOfRangeException;
use function count;
use function defined;
use function in_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Abstract Search Query
*/
class _ApiResponse extends \IPS\Api\PaginatedResponse
class ApiResponse extends PaginatedResponse
{
/**
* Data to output
*
* @return array
*/
public function getOutput()
public function getOutput(): array
{
/* Get the query object from the stored select */
$queryObject = $this->iterator[0];
@@ -40,20 +53,20 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
$searchResults = $queryObject->search(
$this->iterator[2],
$this->iterator[3],
$this->iterator[1] + \IPS\Content\Search\Query::TAGS_MATCH_ITEMS_ONLY,
$this->iterator[1] + Query::TAGS_MATCH_ITEMS_ONLY,
$this->iterator[4]
);
$this->count = $searchResults->count( TRUE );
/* Search tracking, only if there's a term for a non-member search and we're on the first page */
if( \IPS\Request::i()->q AND $this->page == 1 AND ( !isset( \IPS\Request::i()->doNotTrack ) OR \IPS\Request::i()->doNotTrack != 1 ) )
if( Request::i()->q AND $this->page == 1 AND ( !isset( Request::i()->doNotTrack ) OR Request::i()->doNotTrack != 1 ) )
{
$memberPermissions = $this->authorizedMember;
if( !$this->authorizedMember AND isset( \IPS\Request::i()->search_as ) )
if( !$this->authorizedMember AND isset( Request::i()->search_as ) )
{
$memberPermissions = \IPS\Member::load( \IPS\Request::i()->search_as );
$memberPermissions = Member::load( Request::i()->search_as );
if( !$memberPermissions->member_id )
{
@@ -61,10 +74,10 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
}
}
\IPS\Db::i()->insert( 'core_statistics', array(
Db::i()->insert( 'core_statistics', array(
'type' => 'search',
'time' => time(),
'value_4' => \IPS\Request::i()->q,
'value_4' => Request::i()->q,
'value_2' => $this->count,
'extra_data' => $memberPermissions ? md5( $memberPermissions->email . $memberPermissions->joined ) : md5( 'rest-api' ) // Intentionally anonymized
) );
@@ -75,7 +88,7 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
{
$resultData = $result->asArray();
$indexClass = $resultData['indexData']['index_class'];
$itemClass = ( \in_array( 'IPS\Content\Comment', class_parents( $indexClass ) ) ) ? $indexClass::$itemClass : $indexClass;
$itemClass = ( in_array( 'IPS\Content\Comment', class_parents( $indexClass ) ) ) ? $indexClass::$itemClass : $indexClass;
/* Container details */
$containerUrl = NULL;
@@ -83,26 +96,26 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
$containerTitle = \IPS\Member::loggedIn()->language()->addToStack( $containerClass::$titleLangPrefix . $resultData['indexData']['index_container_id'], 'NULL', array( 'escape' => true ) );
$containerTitle = Member::loggedIn()->language()->addToStack( $containerClass::$titleLangPrefix . $resultData['indexData']['index_container_id'], 'NULL', array( 'escape' => true ) );
$containerUrl = $containerClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'], $resultData['containerData'] );
}
/* Reputation - if we are showing the total value, then we need to load them up and total up all of the values */
if ( \IPS\Settings::i()->reaction_count_display == 'count' )
if ( Settings::i()->reaction_count_display == 'count' )
{
$repCount = 0;
foreach( $resultData['reputationData'] AS $memberId => $reactionId )
{
try
{
$repCount += \IPS\Content\Reaction::load( $reactionId )->value;
$repCount += Reaction::load( $reactionId )->value;
}
catch( \OutOfRangeException $e ) {}
catch( OutOfRangeException $e ) {}
}
}
else
{
$repCount = \count( $resultData['reputationData'] );
$repCount = count( $resultData['reputationData'] );
}
$commentCount = NULL;
@@ -133,17 +146,17 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
'itemId' => $resultData['indexData']['index_item_id'],
'started' => $result->createdDate->rfc3339(),
'updated' => $result->lastUpdatedDate->rfc3339(),
'itemUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] ),
'objectUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] )->setQueryString( array( 'do' => 'findComment', 'comment' => $resultData['indexData']['index_object_id'] ) ),
'itemUrl' => (string) $resultData['url'],
'objectUrl' => (string) $resultData['url']->setQueryString( array( 'do' => 'findComment', 'comment' => $resultData['indexData']['index_object_id'] ) ),
'reputation' => $repCount,
'comments' => $commentCount,
'reviews' => $reviewCount,
'container' => $containerTitle,
'containerUrl' => (string) $containerUrl,
'author' => $resultData['authorData']['name'],
'authorUrl' => $resultData['authorData']['member_id'] ? (string) \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$resultData['authorData']['member_id']}", 'front', 'profile', $resultData['authorData']['members_seo_name'] ) : NULL,
'authorPhoto' => \IPS\Member::photoUrl( $resultData['authorData'], FALSE ),
'authorPhotoThumbnail' => \IPS\Member::photoUrl( $resultData['authorData'] ),
'authorUrl' => $resultData['authorData']['member_id'] ? (string) Url::internal( "app=core&module=members&controller=profile&id={$resultData['authorData']['member_id']}", 'front', 'profile', $resultData['authorData']['members_seo_name'] ) : NULL,
'authorPhoto' => Member::photoUrl( $resultData['authorData'], FALSE ),
'authorPhotoThumbnail' => Member::photoUrl( $resultData['authorData'] ),
'tags' => ( !empty( $resultData['indexData']['index_tags'] ) ) ? explode( ',', $resultData['indexData']['index_tags'] ) : array()
);
}
+33 -30
View File
@@ -11,93 +11,96 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Content Filter for Search Queries
*/
class _ContentFilter
class ContentFilter
{
/**
* @brief Item class
*/
public $itemClass;
public string $itemClass;
/**
* @brief Classes to include
*/
public $classes = array();
public array $classes = array();
/**
* @brief Container classes to include
* @note NULL means not to filter by container classes
*/
public $containerClasses = NULL;
public array|null $containerClasses = NULL;
/**
* @brief Classes to include when filtering by container classes
* @note NULL means no special overrides
*/
public $containerClassExclusions = NULL;
public array|null $containerClassExclusions = NULL;
/**
* @brief Container ID filter
*/
public $containerIdFilter = NULL;
public bool|null $containerIdFilter = NULL;
/**
* @brief Container IDs
*/
public $containerIds = array();
public array $containerIds = array();
/**
* @brief Item ID filter
*/
public $itemIdFilter = NULL;
public bool|null $itemIdFilter = NULL;
/**
* @brief Item IDs
*/
public $itemIds = array();
public array $itemIds = array();
/**
* @brief Object ID filter
*/
public $objectIdFilter = NULL;
public bool|null $objectIdFilter = NULL;
/**
* @brief Object IDs
*/
public $objectIds = array();
public array $objectIds = array();
/**
* @brief Minimum comments
*/
public $minimumComments = 0;
public int $minimumComments = 0;
/**
* @brief Minimum reviews
*/
public $minimumReviews = 0;
public int $minimumReviews = 0;
/**
* @brief Minimum views
*/
public $minimumViews = 0;
public int $minimumViews = 0;
/**
* @brief Only include results which are the first comment on an item requiring a comment
*/
public $onlyFirstComment = FALSE;
public bool $onlyFirstComment = FALSE;
/**
* @brief Only include results which are the last comment on an item requiring a comment
*/
public $onlyLastComment = FALSE;
public bool $onlyLastComment = FALSE;
/**
* Constructor
@@ -108,7 +111,7 @@ class _ContentFilter
* @param bool $includeReviews Include reviews in results?
* @return static
*/
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
public static function init( string $itemClass, bool $includeItems=TRUE, bool $includeComments=TRUE, bool $includeReviews=TRUE ): static
{
$obj = new static;
$obj->itemClass = $itemClass;
@@ -138,7 +141,7 @@ class _ContentFilter
* @param string $class The class
* @return static
*/
public static function initWithSpecificClass( $class )
public static function initWithSpecificClass( string $class ): static
{
$obj = new static;
$obj->classes = array( $class );
@@ -152,7 +155,7 @@ class _ContentFilter
* @param array $ids Acceptable container IDs
* @return static (for daisy chaining)
*/
public function onlyInContainers( array $ids )
public function onlyInContainers( array $ids ): static
{
$this->containerIdFilter = TRUE;
$this->containerIds = $ids;
@@ -166,7 +169,7 @@ class _ContentFilter
* @param array $ids Acceptable container IDs
* @return static (for daisy chaining)
*/
public function excludeInContainers( array $ids )
public function excludeInContainers( array $ids ): static
{
$this->containerIdFilter = FALSE;
$this->containerIds = $ids;
@@ -180,7 +183,7 @@ class _ContentFilter
* @param array $ids Acceptable item IDs
* @return static (for daisy chaining)
*/
public function onlyInItems( array $ids )
public function onlyInItems( array $ids ): static
{
$this->itemIdFilter = TRUE;
$this->itemIds = $ids;
@@ -194,7 +197,7 @@ class _ContentFilter
* @param array $ids Acceptable object IDs
* @return static (for daisy chaining)
*/
public function onlyInIds( array $ids )
public function onlyInIds( array $ids ): static
{
$this->objectIdFilter = TRUE;
$this->objectIds = $ids;
@@ -208,7 +211,7 @@ class _ContentFilter
* @param array $ids Acceptable container IDs
* @return static (for daisy chaining)
*/
public function excludeInItems( array $ids )
public function excludeInItems( array $ids ): static
{
$this->itemIdFilter = FALSE;
$this->itemIds = $ids;
@@ -222,7 +225,7 @@ class _ContentFilter
* @param int $minimumComments The minimum number of comments
* @return static (for daisy chaining)
*/
public function minimumComments( $minimumComments )
public function minimumComments( int $minimumComments ): static
{
$this->minimumComments = $minimumComments;
@@ -235,7 +238,7 @@ class _ContentFilter
* @param int $minimumReviews The minimum number of reviews
* @return static (for daisy chaining)
*/
public function minimumReviews( $minimumReviews )
public function minimumReviews( int $minimumReviews ): static
{
$this->minimumReviews = $minimumReviews;
@@ -248,7 +251,7 @@ class _ContentFilter
* @param int $minimumViews The minimum number of views
* @return static (for daisy chaining)
*/
public function minimumViews( $minimumViews )
public function minimumViews( int $minimumViews ): static
{
$this->minimumViews = $minimumViews;
@@ -260,7 +263,7 @@ class _ContentFilter
*
* @return static (for daisy chaining)
*/
public function onlyFirstComment()
public function onlyFirstComment(): static
{
$this->onlyFirstComment = TRUE;
@@ -272,7 +275,7 @@ class _ContentFilter
*
* @return static (for daisy chaining)
*/
public function onlyLastComment()
public function onlyLastComment(): static
{
$this->onlyLastComment = TRUE;
+7 -3
View File
@@ -11,15 +11,19 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DomainException;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Exception Class
*/
class _Exception extends \DomainException
class Exception extends DomainException
{
}
+200 -160
View File
@@ -11,16 +11,49 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Search\Index as SearchIndex;
use IPS\Content\Search\Elastic\Exception as ElasticException;
use IPS\Content;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Request\Exception as RequestException;
use IPS\Http\Request\Curl;
use IPS\Http\Response;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use IPS\Member;
use IPS\Settings;
use OutOfRangeException;
use RuntimeException;
use StdClass;
use function count;
use function defined;
use function get_class;
use function in_array;
use function intval;
use function is_array;
use function is_null;
use function strtolower;
use function substr;
use const IPS\ELASTICSEARCH_PASSWORD;
use const IPS\ELASTICSEARCH_USER;
use const IPS\LONG_REQUEST_TIMEOUT;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Index
*/
class _Index extends \IPS\Content\Search\Index
class Index extends SearchIndex
{
/**
* @brief Elasticsearch version requirements
@@ -31,15 +64,15 @@ class _Index extends \IPS\Content\Search\Index
/**
* @brief The server URL
*/
protected $url;
protected Url $url;
/**
* Constructor
*
* @param \IPS\Http\Url $url The server URL
* @param Url $url The server URL
* @return void
*/
public function __construct( \IPS\Http\Url $url )
public function __construct( Url $url )
{
$this->url = $url;
}
@@ -49,21 +82,21 @@ class _Index extends \IPS\Content\Search\Index
*
* @return void
*/
public function init()
public function init(): void
{
try
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
$analyzer = Settings::i()->search_elastic_analyzer;
$settings = array(
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
'max_result_window' => Settings::i()->search_index_maxresults
);
if ( $analyzer === 'custom' )
{
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$settings['analysis'] = json_decode( '{' . Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $settings['analysis']['analyzer'] );
}
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
Index::request( $this->url )->delete();
$definition = array(
'settings' => $settings,
@@ -117,38 +150,38 @@ class _Index extends \IPS\Content\Search\Index
try
{
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
if( $response->httpResponseCode != 200 )
{
throw new \RuntimeException;
throw new RuntimeException;
}
}
catch( \Exception $e )
catch( Exception )
{
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
$response = Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Get index data
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return array|NULL
*/
public function indexData( \IPS\Content\Searchable $object )
public function indexData( Content $object ): array|NULL
{
if ( $indexData = parent::indexData( $object ) )
{
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$indexData = array_merge( $indexData, $this->metaData( $object ) );
}
@@ -166,10 +199,10 @@ class _Index extends \IPS\Content\Search\Index
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
public function index( \IPS\Content\Searchable $object )
public function index( Content $object ): void
{
if ( $indexData = $this->indexData( $object ) )
{
@@ -190,16 +223,16 @@ class _Index extends \IPS\Content\Search\Index
try
{
$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();
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
if ( $r['found'] )
{
$existingData = $r['_source'];
$existingIndexId = $r['_id'];
}
}
catch( \Exception $e ) { }
catch( Exception ) { }
if ( $object instanceof \IPS\Content\Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
if ( $object instanceof Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
{
/* We do not allow hidden or needing approval comments to become flagged as the last comment as this means users without hidden view permission never see the item in an item only stream */
$indexData['index_is_last_comment'] = false;
@@ -213,7 +246,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Insert into index */
$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 )
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->put( json_encode( $indexData ) );
@@ -228,24 +261,24 @@ class _Index extends \IPS\Content\Search\Index
}
/* Views / Comments / Reviews */
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$item = $object;
}
elseif ( $object instanceof \IPS\Content\Comment )
elseif ( $object instanceof Comment )
{
$item = $object->item();
}
$this->rebuildMetaData( $item );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
catch ( ElasticException $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
Log::log( $e, 'elasticsearch_response_error' );
}
}
}
@@ -256,16 +289,16 @@ class _Index extends \IPS\Content\Search\Index
*
* @return void
*/
public function clearTasks()
public function clearTasks(): void
{
try
{
/* This request *intentionally* goes to _tasks and not (ourpath)/_tasks */
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_tasks' ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
$response = Index::request( $this->url->setPath( '/_tasks' ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get();
if( $error = $this->getResponseError( $response ) )
{
throw new \IPS\Content\Search\Elastic\Exception( $error['type'] . ' ' . $error['type'] );
throw new ElasticException( $error['type'] . ' ' . $error['type'] );
}
$response = $response->decodeJson();
@@ -286,45 +319,53 @@ class _Index extends \IPS\Content\Search\Index
continue;
}
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
Index::request( $this->url->setPath( '/.tasks/task' . $taskId ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->delete();
}
}
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
catch ( ElasticException $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
Log::log( $e, 'elasticsearch_response_error' );
}
catch( \Exception $e )
catch( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Get the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @return void
* @param Item $item The content item
* @return array|null
*/
protected function metaData( $item )
protected function metaData( Item $item ): ?array
{
$databaseColumnId = $item::$databaseColumnId;
if( !Content\Search\SearchContent::isSearchable( $item ) )
{
return null;
}
$databaseColumnId = $item::$databaseColumnId;
/* @var array $databaseColumnMap */
$participants = array( $item->mapped('author') );
if ( isset( $item::$commentClass ) )
{
/* @var Comment $commentClass */
$commentClass = $item::$commentClass;
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
$participants += iterator_to_array( Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
}
if ( isset( $item::$reviewClass ) )
{
/* @var Content\Review $reviewClass */
$reviewClass = $item::$reviewClass;
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
$participants += iterator_to_array( Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
}
$participants = array_values( array_unique( $participants ) );
$isSolved = 0;
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
if ( IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
{
$isSolved = 1;
}
@@ -342,13 +383,13 @@ class _Index extends \IPS\Content\Search\Index
/**
* Rebuild the comment / review counts for an item
*
* @param \IPS\Content\Searchable $item The content item
* @param Item $item The content item
* @return void
*/
protected function rebuildMetaData( $item )
protected function rebuildMetaData( Item $item ): void
{
$databaseColumnId = $item::$databaseColumnId;
$class = \get_class( $item );
$class = get_class( $item );
$classes = array( $class );
if ( isset( $class::$commentClass ) )
{
@@ -365,24 +406,22 @@ class _Index extends \IPS\Content\Search\Index
$params = array();
foreach ( $this->metaData( $item ) as $k => $v )
{
if ( \is_array( $v ) )
$updates[] = "ctx._source.{$k} = params.param_{$k};";
if ( is_array( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params[ 'param_' . $k ] = ( $k !== 'index_tags' ? array_map( 'intval', $v ) : $v );
}
elseif ( \is_null( $v ) )
elseif ( is_null( $v ) )
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = null;
}
else
{
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = \intval( $v );
$params['param_' . $k ] = intval( $v );
}
}
$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(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ), LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -406,44 +445,45 @@ class _Index extends \IPS\Content\Search\Index
)
) ) );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
/**
* Retrieve the search ID for an item
*
* @param \IPS\Content\Searchable $object Item to add
* @return string
* @param Content $object Item to add
* @return mixed
*/
public function getIndexId( \IPS\Content\Searchable $object ): string
public function getIndexId( Content $object ): mixed
{
$databaseColumnId = $object::$databaseColumnId;
return \strtolower( str_replace( '\\', '_', \substr( \get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
return strtolower( str_replace( '\\', '_', substr( get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
}
/**
* Remove item
*
* @param \IPS\Content\Searchable $object Item to remove
* @return void
* @param Content $object Item to remove
* @return string
*/
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
public function removeFromSearchIndex( Content $object ): void
{
try
{
$class = \get_class( $object );
$class = get_class( $object );
$idColumn = $class::$databaseColumnId;
$this->directIndexRemoval( $class, $object->$idColumn );
if ( !( $object instanceof \IPS\Content\Item ) )
if ( !( $object instanceof Item ) )
{
$this->rebuildMetaData( $object->item() );
$itemClass = \get_class( $object->item() );
/* @var Item $itemClass */
$itemClass = get_class( $object->item() );
$itemIdColumn = $itemClass::$databaseColumnId;
$classes = array( $itemClass );
@@ -456,12 +496,12 @@ class _Index extends \IPS\Content\Search\Index
$classes[] = $itemClass::$reviewClass;
}
$this->resetLastComment( $classes, $object->item()->$itemIdColumn, $object->$idColumn, $this->getFirstCommentId( $object ) );
$this->resetLastComment( $classes, $object->item()->$itemIdColumn, $object->$idColumn, null, $this->getFirstCommentId( $object ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -472,19 +512,19 @@ class _Index extends \IPS\Content\Search\Index
* @param int $id ID
* @return void
*/
public function directIndexRemoval( $class, $id )
public function directIndexRemoval( string $class, int $id ): void
{
try
{
$indexId = \strtolower( str_replace( '\\', '_', \substr( $class, 4 ) ) ) . '-' . $id;
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
$indexId = strtolower( str_replace( '\\', '_', substr( $class, 4 ) ) ) . '-' . $id;
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$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(
$response = Index::request( $this->url->setPath( $this->url->data[ 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(
@@ -507,7 +547,7 @@ class _Index extends \IPS\Content\Search\Index
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
\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(
Index::request( $this->url->setPath( $this->url->data[ 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(
@@ -529,9 +569,9 @@ class _Index extends \IPS\Content\Search\Index
}
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -543,7 +583,7 @@ class _Index extends \IPS\Content\Search\Index
* @param int|NULL $authorId The author ID to delete, or NULL
* @return void
*/
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
public function removeClassFromSearchIndex( string $class, int|null $containerId=null, int|null $authorId=null ): void
{
try
{
@@ -579,13 +619,13 @@ class _Index extends \IPS\Content\Search\Index
);
}
\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(
Index::request( $this->url->setPath( $this->url->data[ 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
{
\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(
Index::request( $this->url->setPath( $this->url->data[ 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
@@ -594,9 +634,9 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -615,7 +655,7 @@ class _Index extends \IPS\Content\Search\Index
* @param bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
* @return void
*/
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
public function massUpdate( string $class, int|null $containerId = null, int|null $itemId = null, string|null $newPermissions = null, int|null $newHiddenStatus = null, int|null $newContainer = null, int|null $authorId = null, int|null $newItemId = null, int|null $newItemAuthorId = null, bool $addAuthorToPermissions = FALSE ): void
{
try
{
@@ -660,16 +700,17 @@ class _Index extends \IPS\Content\Search\Index
if ( $newContainer )
{
$updates[] = "ctx._source.index_container_id = params.params_indexcontainer;";
$params['params_indexcontainer'] = \intval( $newContainer );
if ( $itemClass = ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
$params['params_indexcontainer'] = $newContainer;
/* @var Item $itemClass */
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
{
try
{
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = \intval( $containerClass::load( $newContainer )->$clubIdColumn );
$params['params_indexclub'] = intval( $containerClass::load( $newContainer )->$clubIdColumn );
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException )
{
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = null;
@@ -679,17 +720,17 @@ class _Index extends \IPS\Content\Search\Index
if ( $newItemId )
{
$updates[] = "ctx._source.index_item_id = params.params_indexitem;";
$params['params_indexitem'] = \intval( $newItemId );
$params['params_indexitem'] = $newItemId;
}
if ( $newItemAuthorId )
{
$updates[] = "ctx._source.index_item_author = params.params_indexauthor;";
$params['params_indexauthor'] = \intval( $newItemAuthorId );
$params['params_indexauthor'] = $newItemAuthorId;
}
if ( \count( $updates ) )
if ( count( $updates ) )
{
\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(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'source' => implode( ' ', $updates ),
'lang' => 'painless',
@@ -710,7 +751,7 @@ class _Index extends \IPS\Content\Search\Index
)
);
\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(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => 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'
@@ -740,11 +781,11 @@ class _Index extends \IPS\Content\Search\Index
);
}
\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(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => 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',
'params' => array( 'newHiddenStatus' => \intval( $newHiddenStatus ) )
'params' => array( 'newHiddenStatus' => $newHiddenStatus )
),
'query' => array(
'bool' => $conditions
@@ -752,9 +793,9 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -764,15 +805,15 @@ class _Index extends \IPS\Content\Search\Index
* @param array $conditions Conditions
* @return array
*/
public static function convertConditionsToQuery( $conditions )
public static function convertConditionsToQuery( array $conditions ): array
{
if ( \count( $conditions ) == 1 )
if ( count( $conditions ) == 1 )
{
return $conditions[0];
}
elseif ( \count( $conditions ) == 0 )
elseif ( count( $conditions ) == 0 )
{
return array( 'match_all' => new \StdClass );
return array( 'match_all' => new StdClass );
}
else
{
@@ -788,17 +829,16 @@ class _Index extends \IPS\Content\Search\Index
* Update data for the first and last comment after a merge
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
*
* @param \IPS\Content\Item $item The item
* @param Item $item The item
* @return void
*/
public function rebuildAfterMerge( \IPS\Content\Item $item )
public function rebuildAfterMerge( Item $item ): void
{
if ( $item::$commentClass )
{
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
$idColumn = $item::$databaseColumnId;
$update = array( 'index_is_last_comment' => false );
if ( $item::$firstCommentRequired )
{
@@ -807,7 +847,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
\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(
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => $update
) ) );
@@ -820,9 +860,9 @@ class _Index extends \IPS\Content\Search\Index
$this->index( $lastComment );
}
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
}
@@ -830,16 +870,16 @@ class _Index extends \IPS\Content\Search\Index
/**
* Prune search index
*
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @param DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @return void
*/
public function prune( \IPS\DateTime $cutoff = NULL )
public function prune( DateTime|null $cutoff = NULL ): void
{
if ( $cutoff )
{
try
{
\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(
Index::request( $this->url->setPath( $this->url->data[ 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(
@@ -849,9 +889,9 @@ class _Index extends \IPS\Content\Search\Index
)
) ) );
}
catch ( \IPS\Http\Request\Exception $e )
catch ( RequestException $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
else
@@ -859,22 +899,22 @@ class _Index extends \IPS\Content\Search\Index
$this->init();
}
}
/**
* Reset the last comment flag in any given class/index_item_id
*
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @return void
*/
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL, $firstCommentId = NULL )
public function resetLastComment( array $classes, int|null $indexItemId, int|null $ignoreId = null, int|null $firstCommentId = null ): void
{
try
{
/* Remove the flag */
$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(
$r = Index::request( $this->url->setPath( $this->url->data[ 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'
@@ -906,7 +946,7 @@ class _Index extends \IPS\Content\Search\Index
$itemClass = NULL;
foreach ( $classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$itemClass = $class;
break;
@@ -931,10 +971,10 @@ class _Index extends \IPS\Content\Search\Index
$where = array( $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentIdColumn . '<>?', $ignoreId );
}
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND $lastComment instanceof \IPS\Content\Searchable )
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, $where ) AND Content\Search\SearchContent::isSearchable( $lastComment ) )
{
/* Set that it is the latest comment */
$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(
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update/' . $this->getIndexId( $lastComment ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => array(
'index_is_last_comment' => true
)
@@ -942,13 +982,13 @@ class _Index extends \IPS\Content\Search\Index
/* 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 );
$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(
$r = Index::request( $this->url->setPath( $this->url->data[ 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',
'params' => array(
'dateUpdated' => \intval( $indexDataForLastComment['index_date_updated'] ),
'dateCommented' => \intval( $indexDataForLastComment['index_date_commented'] )
'dateUpdated' => intval( $indexDataForLastComment['index_date_updated'] ),
'dateCommented' => intval( $indexDataForLastComment['index_date_commented'] )
)
),
'query' => array(
@@ -975,12 +1015,12 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \OutOfRangeException $e ) {}
catch ( OutOfRangeException ) {}
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
}
}
@@ -988,10 +1028,10 @@ class _Index extends \IPS\Content\Search\Index
* Given a list of item index IDs, return the ones that a given member has participated in
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param Member $member The member
* @return array
*/
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
public function iPostedIn( array $itemIndexIds, Member $member ): array
{
try
{
@@ -1014,7 +1054,7 @@ class _Index extends \IPS\Content\Search\Index
);
/* Get the count */
$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(
$count = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 0,
'query' => $query
) ) )->decodeJson();
@@ -1025,7 +1065,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Now get the unique item ids */
$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(
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'aggs' => array(
'itemIds' => array(
'terms' => array(
@@ -1037,9 +1077,9 @@ class _Index extends \IPS\Content\Search\Index
'query' => $query
) ) )->decodeJson();
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return array();
}
@@ -1059,15 +1099,15 @@ class _Index extends \IPS\Content\Search\Index
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
*
* @param array $hashes Item index hashes
* @param \IPS\Member $member The member
* @param Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL )
public function hashesWithPermission( array $hashes, Member $member, int|null $limit = null ): array
{
try
{
$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(
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'query' => array(
'bool' => array(
'filter' => array(
@@ -1092,9 +1132,9 @@ class _Index extends \IPS\Content\Search\Index
'size' => $limit ?: 10 // If we define a limit, use that, otherwise default to 10 which is ElasticSearch's default
) ) )->decodeJson();
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return array();
}
@@ -1112,11 +1152,11 @@ class _Index extends \IPS\Content\Search\Index
*
* @return int|null
*/
public function firstIndexDate()
public function firstIndexDate(): int|NULL
{
try
{
$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(
$results = Index::request( $this->url->setPath( $this->url->data[ 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();
@@ -1128,9 +1168,9 @@ class _Index extends \IPS\Content\Search\Index
return NULL;
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
Log::log( $e, 'elasticsearch' );
return NULL;
}
}
@@ -1141,18 +1181,18 @@ class _Index extends \IPS\Content\Search\Index
* @param array $terms Terms
* @return array
*/
public function stemmedTerms( $terms )
public function stemmedTerms( array $terms ): array
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
$analyzer = Settings::i()->search_elastic_analyzer;
if ( $analyzer === 'custom' )
{
$analysisSettings = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analysisSettings = json_decode( '{' . Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $analysisSettings['analyzer'] );
}
try
{
$results = \IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = Index::request( $this->url->setPath( '/_analyze' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'analyzer' => $analyzer,
'text' => implode( ' ', $terms )
) ) )->decodeJson();
@@ -1169,7 +1209,7 @@ class _Index extends \IPS\Content\Search\Index
return $terms;
}
catch ( \Exception $e )
catch ( Exception )
{
return $terms;
}
@@ -1180,7 +1220,7 @@ class _Index extends \IPS\Content\Search\Index
*
* @return bool
*/
public function supportViewFiltering()
public function supportViewFiltering(): bool
{
return FALSE;
}
@@ -1188,15 +1228,15 @@ class _Index extends \IPS\Content\Search\Index
/**
* Wrapper to account for log in where needed
*
* @param $url
* @param null $timeout
* @return \IPS\Http\Request\Curl|Socket
* @param Url $url
* @param int|null $timeout
* @return Curl
*/
public static function request( $url, $timeout=NULL )
public static function request( Url $url, int|null $timeout=NULL ) : Curl
{
if ( \IPS\ELASTICSEARCH_USER or \IPS\ELASTICSEARCH_PASSWORD )
if ( ELASTICSEARCH_USER or ELASTICSEARCH_PASSWORD )
{
return $url->request( $timeout )->login( \IPS\ELASTICSEARCH_USER, \IPS\ELASTICSEARCH_PASSWORD );
return $url->request( $timeout )->login( ELASTICSEARCH_USER, ELASTICSEARCH_PASSWORD );
}
return $url->request( $timeout );
@@ -1205,10 +1245,10 @@ class _Index extends \IPS\Content\Search\Index
/**
* Check response to see if an error was produced
*
* @param \IPS\Http\Response $response Response object
* @return string|null
* @param Response $response Response object
* @return array|null
*/
protected function getResponseError( \IPS\Http\Response $response )
protected function getResponseError( Response $response ): array|NULL
{
/* Log any errors */
if( $response->httpResponseCode != 200 AND $content = $response->decodeJson() AND isset( $content['error'] ) )
+17 -10
View File
@@ -11,44 +11,51 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Http\Url;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Mass Indexer
*/
class _MassIndexer extends \IPS\Content\Search\Elastic\Index
class MassIndexer extends Index
{
/**
* @brief Data to store in bulk
*/
protected $data = array();
protected ?array $data = array();
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
public function index( \IPS\Content\Searchable $object )
public function index( Content $object ): void
{
if ( $indexData = $this->indexData( $object ) and $indexData['index_permissions'] )
{
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$parent = $object;
}
elseif ( $object instanceof \IPS\Content\Comment )
elseif ( $object instanceof Comment )
{
$parent = $object->item();
}
$this->data[] = array(
'index' => array(
'_index' => trim( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/' ),
'_index' => trim( $this->url->data[ Url::COMPONENT_PATH ], '/' ),
'_type' => '_doc',
'_id' => $this->getIndexId( $object ),
)
@@ -73,7 +80,7 @@ class _MassIndexer extends \IPS\Content\Search\Elastic\Index
}
$json = implode( "\n", $json );
\IPS\Content\Search\Elastic\Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
Index::request( $this->url->setPath( '/_bulk' ) )->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
}
}
}
+155 -178
View File
@@ -11,60 +11,82 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Application;
use IPS\Content;
use IPS\Content\Item;
use IPS\Content\Search\Query as SearchQuery;
use IPS\Content\Search\Results;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Log;
use IPS\Member;
use IPS\Member\Club;
use IPS\Settings;
use StdClass;
use function count;
use function defined;
use function in_array;
use function intval;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Query
*/
class _Query extends \IPS\Content\Search\Query
class Query extends SearchQuery
{
/**
* @brief The server URL
*/
protected $url;
protected Url $url;
/**
* @brief Filters
*/
protected $filters = array();
protected array $filters = array();
/**
* @brief "Must Not" filter
*/
protected $mustNot = array();
protected array $mustNot = array();
/**
* @brief The sort clause
*/
protected $sort = NULL;
protected array|string|null $sort = NULL;
/**
* @brief The offset
*/
protected $offset = 0;
protected int $offset = 0;
/**
* @brief index_hidden statuses
*/
protected $hiddenStatuses = NULL;
protected int|array|null $hiddenStatuses = NULL;
/**
* @brief Item classes included
*/
protected $itemClasses = NULL;
protected array|null $itemClasses = NULL;
/**
* Constructor
*
* @param \IPS\Member $member The member performing the search
* @param \IPS\Http\Url $url The server URL
* @param Member $member The member performing the search
* @param Url $url The server URL
* @return void
*/
public function __construct( \IPS\Member $member, \IPS\Http\Url $url )
public function __construct( Member $member, Url $url )
{
parent::__construct( $member );
$this->url = $url;
@@ -73,16 +95,16 @@ class _Query extends \IPS\Content\Search\Query
/**
* @var bool Stores whether or not any content type needs to be limited by the latest comment
*/
protected $lastCommentMustBeTrue = false;
protected bool $lastCommentMustBeTrue = false;
/**
* Filter by multiple content types
*
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByContent( array $contentFilters, $type = TRUE )
public function filterByContent( array $contentFilters, bool $type = TRUE ): static
{
$contentFilterConditions = array();
if ( $type )
@@ -212,14 +234,14 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it together */
if( \count( $conditions ) )
if( count( $conditions ) )
{
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
}
}
/* Put them together */
if ( \count( $contentFilterConditions ) > 1 )
if ( count( $contentFilterConditions ) > 1 )
{
$this->filters[] = array(
'bool' => array(
@@ -229,14 +251,14 @@ class _Query extends \IPS\Content\Search\Query
}
elseif ( $type )
{
if( \count( $contentFilterConditions ) )
if( count( $contentFilterConditions ) )
{
$this->filters[] = $contentFilterConditions[0];
}
}
else
{
if( \count( $contentFilterConditions ) )
if( count( $contentFilterConditions ) )
{
$this->mustNot[] = $contentFilterConditions[0];
}
@@ -248,18 +270,18 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by author
*
* @param \IPS\Member|int|array $author The author, or an array of author IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member|int|array $author The author, or an array of author IDs
* @return Query (for daisy chaining)
*/
public function filterByAuthor( $author )
public function filterByAuthor( Member|int|array $author ): static
{
if ( \is_array( $author ) )
if ( is_array( $author ) )
{
$this->filters[] = array( 'terms' => array( 'index_author' => $author ) );
}
else
{
$this->filters[] = array( 'term' => array( 'index_author' => $author instanceof \IPS\Member ? $author->member_id : $author ) );
$this->filters[] = array( 'term' => array( 'index_author' => $author instanceof Member ? $author->member_id : $author ) );
}
return $this;
@@ -268,10 +290,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by club
*
* @param \IPS\Member\Club|int|array $club The club, or array of club IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Club|int|array|null $club The club, or array of club IDs
* @return Query (for daisy chaining)
*/
public function filterByClub( $club )
public function filterByClub( Club|int|array|null $club ): static
{
if ( $club === NULL )
{
@@ -279,7 +301,7 @@ class _Query extends \IPS\Content\Search\Query
'exists' => array( 'field' => 'index_club_id' )
);
}
elseif ( \is_array( $club ) )
elseif ( is_array( $club ) )
{
$this->filters[] = array(
'terms' => array( 'index_club_id' => $club )
@@ -288,28 +310,12 @@ class _Query extends \IPS\Content\Search\Query
else
{
$this->filters[] = array(
'term' => array( 'index_club_id' => $club instanceof \IPS\Member\Club ? $club->id : $club )
'term' => array( 'index_club_id' => $club instanceof Club ? $club->id : $club )
);
}
/* Get the list of valid classes */
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
foreach ( $object->classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
$itemClass::searchEngineFiltering( $filters, $this );
}
$this->customFiltering( TRUE );
return $this;
}
@@ -317,10 +323,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter for profile
*
* @param \IPS\Member $member The member whose profile is being viewed
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $member The member whose profile is being viewed
* @return Query (for daisy chaining)
*/
public function filterForProfile( \IPS\Member $member )
public function filterForProfile( Member $member ): static
{
/* Filter by content they've posted or posts on their wall */
$this->filters[] = array(
@@ -328,71 +334,40 @@ class _Query extends \IPS\Content\Search\Query
'should' => array(
array(
'term' => array( 'index_author' => $member->member_id )
),
array(
'bool' => array(
'filter' => array(
array(
'term' => array(
'index_class' => 'IPS\core\Statuses\Status'
)
),
array(
'term' => array(
'index_container_id' => $member->member_id
)
)
)
)
)
)
)
);
/* Get the list of valid classes */
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
foreach ( $object->classes as $class )
{
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
$itemClass::searchEngineFiltering( $filters, $this );
}
$this->customFiltering( TRUE );
/* Return for daisy chaining */
/* Return for daisy-chaining */
return $this;
}
/**
* Stores the "more like this" Content object for search()
* @param null
* @param array|null $moreLikeThis
*/
protected $moreLikeThis = NULL;
protected array|null $moreLikeThis = NULL;
/**
* Filter by more like this
* @param \IPS\Content\Searchable $object
* @param Content $object
* @return Query
*/
public function filterByMoreLikeThis( \IPS\Content\Searchable $object )
public function filterByMoreLikeThis( Content $object ): static
{
$index = new \IPS\Content\Search\Elastic\Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof \IPS\Content\Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
$index = new Index( $this->url );
$this->moreLikeThis = $index->getIndexId( ( $object instanceof Item and $object::$firstCommentRequired ) ? $object->firstComment() : $object );
/* Some container types cannot be cached */
$classes = [];
$conditions = [];
$noSimilarContentClasses = [];
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
@@ -428,7 +403,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
if ( \count( $blockIds ) )
if ( count( $blockIds ) )
{
$conditions[] = array(
'bool' => array(
@@ -444,7 +419,7 @@ class _Query extends \IPS\Content\Search\Query
);
}
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
foreach( $conditions as $condition )
{
@@ -455,7 +430,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Prevent some things from being in similar content widget */
if ( \count( $noSimilarContentClasses ) )
if ( count( $noSimilarContentClasses ) )
{
$this->mustNot[] = array(
'bool' => array(
@@ -470,6 +445,8 @@ class _Query extends \IPS\Content\Search\Query
/* Only show non-hidden and approved items */
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
return $this;
}
/**
@@ -478,9 +455,9 @@ class _Query extends \IPS\Content\Search\Query
* @param array $classes Container classes to exclude from results.
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
* such as Gallery images, comments and reviews.
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByContainerClasses( $classes=array(), $exclude=array() )
public function filterByContainerClasses( array $classes=array(), array $exclude=array() ): static
{
if( empty( $exclude ) )
{
@@ -519,10 +496,10 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by item author
*
* @param \IPS\Member $author The author
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $author The author
* @return Query (for daisy chaining)
*/
public function filterByItemAuthor( \IPS\Member $author )
public function filterByItemAuthor( Member $author ): static
{
$this->filters[] = array(
'term' => array( 'index_item_author' => $author->member_id )
@@ -536,9 +513,9 @@ class _Query extends \IPS\Content\Search\Query
* @param bool $includeContainers Include content in containers the user follows?
* @param bool $includeItems Include items and comments/reviews on items the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
public function filterByFollowed( bool $includeContainers, bool $includeItems, bool $includeMembers ): static
{
$conditions = array();
$followApps = $followAreas = $case = $containerCase = array();
@@ -556,7 +533,7 @@ class _Query extends \IPS\Content\Search\Query
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
@@ -565,7 +542,7 @@ class _Query extends \IPS\Content\Search\Query
/* Loop them */
foreach ( $classes as $class )
{
if( is_subclass_of( $class, 'IPS\Content\Followable' ) )
if( IPS::classUsesTrait( $class, 'IPS\Content\Followable' ) )
{
$followApps[ $class::$application ] = $class::$application;
$followArea = mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) );
@@ -607,13 +584,13 @@ class _Query extends \IPS\Content\Search\Query
}
/* Get the stuff we follow */
foreach( \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_member_id=? AND ' . \IPS\Db::i()->in( 'follow_app', $followApps ) . ' AND ' . \IPS\Db::i()->in( 'follow_area', $followAreas ), $this->member->member_id ) ) as $follow )
foreach( Db::i()->select( '*', 'core_follow', array( 'follow_member_id=? AND ' . Db::i()->in( 'follow_app', $followApps ) . ' AND ' . Db::i()->in( 'follow_area', $followAreas ), $this->member->member_id ) ) as $follow )
{
if( array_key_exists( $follow['follow_area'], $case ) )
{
$followedItems[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
else if( \in_array( $follow['follow_area'], $containerCase ) )
else if( in_array( $follow['follow_area'], $containerCase ) )
{
$followedContainers[ $follow['follow_area'] ][] = $follow['follow_rel_id'];
}
@@ -663,7 +640,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Are we including content posted by followed members? */
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' ) ) )
if ( $includeMembers and $followed = iterator_to_array( 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(
'terms' => array( 'index_author' => $followed )
@@ -671,13 +648,13 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
$this->filters[] = array( 'bool' => array( 'should' => $conditions ) );
}
else
{
$this->filters[] = array( 'match_none' => new \StdClass );
$this->filters[] = array( 'match_none' => new StdClass );
}
/* And return */
@@ -687,9 +664,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by content the user has posted in
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByItemsIPostedIn()
public function filterByItemsIPostedIn(): static
{
$this->filters[] = array(
'term' => array(
@@ -703,9 +680,9 @@ class _Query extends \IPS\Content\Search\Query
* Filter by content the user has not read
*
* @note If applicable, it is more efficient to call filterByContent() before calling this method
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByUnread()
public function filterByUnread(): static
{
/* Work out what classes we need to examine */
if ( $this->itemClasses !== NULL )
@@ -715,7 +692,7 @@ class _Query extends \IPS\Content\Search\Query
else
{
$classes = array();
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
foreach ( Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
{
$classes = array_merge( $object->classes, $classes );
}
@@ -726,9 +703,9 @@ class _Query extends \IPS\Content\Search\Query
$resetTimes = $this->member->markersResetTimes( NULL );
foreach ( $classes as $class )
{
if( is_subclass_of( $class, 'IPS\Content\ReadMarkers' ) )
if( IPS::classUsesTrait( $class, 'IPS\Content\ReadMarkers' ) )
{
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
$containerClass = $class::$containerNodeClass ?? NULL;
$classConditions = array();
/* Work out what classes this applies to - need to specify comment and review classes */
@@ -779,21 +756,21 @@ class _Query extends \IPS\Content\Search\Query
);
/* And get the times each individual item was read for later */
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( \count( $items ) )
$items = $this->member->markersItems( $application, $class::makeMarkerKey( $containerId ) );
if ( count( $items ) )
{
foreach( $items as $mid => $mtime )
{
if ( $mtime > $timestamp )
{
/* 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
in its 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 ) )
if ( in_array( $mid, $markers ) )
{
$_key = array_search( $mid, $markers );
$_mtime = \intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
if ( $_mtime < $mtime )
{
unset( $markers[ $_key ] );
@@ -838,7 +815,7 @@ class _Query extends \IPS\Content\Search\Query
}
$notIn = array();
if ( \count( $markers ) )
if ( count( $markers ) )
{
$useIds = array_flip( $markers );
@@ -847,7 +824,7 @@ class _Query extends \IPS\Content\Search\Query
{
if ( isset( $class::$databaseColumnMap[ $k ] ) )
{
if ( \is_array( $class::$databaseColumnMap[ $k ] ) )
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
{
foreach ( $class::$databaseColumnMap[ $k ] as $v )
{
@@ -860,16 +837,16 @@ class _Query extends \IPS\Content\Search\Query
}
}
}
$dateColumnExpression = \count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
$dateColumnExpression = count( $dateColumns ) > 1 ? ( 'GREATEST(' . implode( ',', $dateColumns ) . ')' ) : array_pop( $dateColumns );
foreach( \IPS\Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
foreach( Db::i()->select( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId. ' as _id, ' . $dateColumnExpression . ' as _date', $class::$databaseTable, Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, array_keys( $useIds ) ) ) as $row )
{
if ( isset( $useIds[ $row['_id'] ] ) )
{
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
{
/* Still read */
$notIn[] = \intval( $row['_id'] );
$notIn[] = intval( $row['_id'] );
}
}
}
@@ -881,7 +858,7 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $classConditions
)
);
if ( \count( $notIn ) )
if ( count( $notIn ) )
{
$_condition['bool']['must_not'] = array(
'terms' => array(
@@ -894,7 +871,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( \count( $conditions ) )
if ( count( $conditions ) )
{
$this->filters[] = array(
'bool' => array(
@@ -909,9 +886,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by solved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterBySolved()
public function filterBySolved(): static
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 1 )
@@ -923,9 +900,9 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by Unsolved
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function filterByUnsolved()
public function filterByUnsolved(): static
{
$this->filters[] = array(
'term' => array( 'index_item_solved' => 0 )
@@ -937,11 +914,11 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by start date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
public function filterByCreateDate( DateTime|null $start = null, DateTime|null $end = null ): static
{
$range = array();
@@ -967,11 +944,11 @@ class _Query extends \IPS\Content\Search\Query
/**
* Filter by last updated date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
public function filterByLastUpdatedDate( DateTime|null $start = null, DateTime|null $end = null ): static
{
$range = array();
@@ -998,9 +975,9 @@ class _Query extends \IPS\Content\Search\Query
* Set hidden status
*
* @param int|array|NULL $statuses The statuses (see HIDDEN_ constants) or NULL for any
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setHiddenFilter( $statuses )
public function setHiddenFilter( int|array|null $statuses ): static
{
$this->hiddenStatuses = $statuses;
return $this;
@@ -1010,9 +987,9 @@ class _Query extends \IPS\Content\Search\Query
* Set page
*
* @param int $page The page number
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setPage( $page )
public function setPage( int $page ): static
{
$this->offset = ( $page - 1 ) * $this->resultsToGet;
@@ -1023,9 +1000,9 @@ class _Query extends \IPS\Content\Search\Query
* Set order
*
* @param int $order Order (see ORDER_ constants)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setOrder( $order )
public function setOrder( int $order ): static
{
switch ( $order )
{
@@ -1062,16 +1039,16 @@ class _Query extends \IPS\Content\Search\Query
/**
* Debug by itemId, used for debugging purposes only
* @note No permission checks run, do not use in production
* @param $itemId
* @return void
* @param int $itemId
* @return array
*/
public function debugByItemId( $itemId )
public function debugByItemId( int $itemId ): array
{
$array = array(
'query' => [
'term' => [
'index_item_id' => [
'value' => \intval( $itemId )
'value' => $itemId
]
]
],
@@ -1082,7 +1059,7 @@ class _Query extends \IPS\Content\Search\Query
$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();
return Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
}
/**
@@ -1092,25 +1069,25 @@ class _Query extends \IPS\Content\Search\Query
* @param array|null $tags The tags to search for
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
* @return \IPS\Content\Search\Results
* @return Results
*/
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
public function search( string|null $term = null, array|null $tags = null, int $method = 1, string|null $operator = null ): Results
{
/* If we're looking for more results than we can fetch, we don't need to ask Elastic Search */
if( ( $this->offset + $this->resultsToGet ) > \IPS\Settings::i()->search_index_maxresults )
/* If we're looking for more results than we can fetch, we don't need to ask ElasticSearch */
if( ( $this->offset + $this->resultsToGet ) > Settings::i()->search_index_maxresults )
{
return new \IPS\Content\Search\Results( array(), 0 );
return new Results( array(), 0 );
}
$operator = $operator ?: \IPS\Settings::i()->search_default_operator;
$operator = $operator ?: Settings::i()->search_default_operator;
$must = array();
$filters = $this->filters;
/* Set our conditions for this search */
if ( $term !== NULL or ( \IPS\Settings::i()->tags_enabled AND $tags !== NULL ) )
if ( $term !== NULL or ( Settings::i()->tags_enabled AND $tags !== NULL ) )
{
$searchConditions = array();
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . \intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
$titleField = Settings::i()->search_title_boost ? ( 'index_title^' . intval( Settings::i()->search_title_boost ) ) : 'index_title';
/* Build the condition for the search term */
if ( $term !== NULL )
@@ -1141,7 +1118,7 @@ class _Query extends \IPS\Content\Search\Query
'bool' => array(
'should' => array(
array(
'wildcard' => array( 'index_title' => ( \IPS\Settings::i()->search_title_boost ? array( 'value' => $term, 'boost' => \intval( \IPS\Settings::i()->search_title_boost ) ) : array( 'value' => $term ) ) )
'wildcard' => array( 'index_title' => ( Settings::i()->search_title_boost ? array( 'value' => $term, 'boost' => intval( Settings::i()->search_title_boost ) ) : array( 'value' => $term ) ) )
),
array(
'wildcard' => array( 'index_content' => array( 'value' => $term ) )
@@ -1165,7 +1142,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
/* Build the condition for the tags */
if ( \IPS\Settings::i()->tags_enabled and $tags !== NULL )
if ( Settings::i()->tags_enabled and $tags !== NULL )
{
$searchConditions[] = array(
'bool' => array(
@@ -1209,7 +1186,7 @@ class _Query extends \IPS\Content\Search\Query
$filters[] = array( 'terms' => array( 'index_permissions' => array_merge( $this->permissionArray(), array( '*' ) ) ) );
if ( $this->hiddenStatuses !== NULL )
{
if ( \is_array( $this->hiddenStatuses ) )
if ( is_array( $this->hiddenStatuses ) )
{
$filters[] = array( 'terms' => array( 'index_hidden' => $this->hiddenStatuses ) );
}
@@ -1219,14 +1196,14 @@ class _Query extends \IPS\Content\Search\Query
}
}
$searchUrl = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' );
$searchUrl = $this->url->setPath( $this->url->data[ 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 ],
'like' => [ '_index' => Settings::i()->search_elastic_index, '_id' => $this->moreLikeThis ],
'min_term_freq' => 1,
'max_query_terms' => 12,
'min_word_length' => 3,
@@ -1250,15 +1227,15 @@ class _Query extends \IPS\Content\Search\Query
);
/* Add the time decay */
if ( \IPS\Settings::i()->search_decay_factor and !$this->sort and ! $this->sort )
if ( Settings::i()->search_decay_factor and !$this->sort )
{
$query = array(
'function_score' => array(
'query' => $query,
'linear' => array(
'index_date_updated' => array(
'scale' => \intval( \IPS\Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' ),
'scale' => intval( Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( Settings::i()->search_decay_factor, 1, '.', '' ),
'origin' => time(),
)
)
@@ -1267,7 +1244,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 and ! $this->moreLikeThis )
if ( Settings::i()->search_elastic_self_boost and $this->member->member_id and !$this->sort and ! $this->moreLikeThis )
{
$query = array(
'function_score' => array(
@@ -1277,8 +1254,8 @@ class _Query extends \IPS\Content\Search\Query
'source' => "doc['index_author'].value == params.param_memberId ? ( _score * Float.parseFloat( params.param_booster ) ) : _score",
'lang' => 'painless',
'params' => array(
'param_memberId' => \intval( $this->member->member_id ),
'param_booster' => number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' )
'param_memberId' => intval( $this->member->member_id ),
'param_booster' => number_format( Settings::i()->search_elastic_self_boost, 1, '.', '' )
)
)
)
@@ -1299,30 +1276,30 @@ class _Query extends \IPS\Content\Search\Query
$json = json_encode( $array, JSON_PARTIAL_OUTPUT_ON_ERROR );
if ( $json === FALSE )
{
return new \IPS\Content\Search\Results( array(), 0 );
return new Results( array(), 0 );
}
/* Make the call! */
$return = \IPS\Content\Search\Elastic\Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
$return = Index::request( $searchUrl )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
{
\IPS\Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
Log::log( print_r( array_merge( $array, ['error' => $return['error'] ] ), TRUE ), 'elasticsearch' );
return new Results( array(), 0 );
}
/* Set results */
$total = $return['hits']['total']['value'] ?? $return['hits']['total'];
return new \IPS\Content\Search\Results( array_map( function( $hit ) {
return new Results( array_map( function( $hit ) {
$indexData = $hit['_source'];
$indexData['index_permissions'] = implode( ',', $indexData['index_permissions'] );
$indexData['index_tags'] = $indexData['index_tags'] ? implode( ',', $indexData['index_tags'] ) : NULL;
return $indexData;
}, $return['hits']['hits'] ), $total <= \IPS\Settings::i()->search_index_maxresults ? $total : (int) \IPS\Settings::i()->search_index_maxresults );
}, $return['hits']['hits'] ), $total <= Settings::i()->search_index_maxresults ? $total : (int) Settings::i()->search_index_maxresults );
}
catch ( \Exception $e )
catch ( Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
Log::log( $e, 'elasticsearch' );
return new Results( array(), 0 );
}
}
}
+172 -150
View File
@@ -11,39 +11,64 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Application;
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Search\Elastic\MassIndexer;
use IPS\DateTime;
use IPS\Db;
use IPS\Http\Url;
use IPS\IPS;
use IPS\Member;
use IPS\Patterns\Singleton;
use IPS\Settings;
use IPS\Task;
use IPS\Text\Parser;
use LogicException;
use OutOfRangeException;
use RuntimeException;
use UnderflowException;
use function chr;
use function defined;
use function get_class;
use function intval;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Abstract Search Index
*/
abstract class _Index extends \IPS\Patterns\Singleton
abstract class Index extends Singleton
{
/**
* @brief Singleton Instances
*/
protected static $instance = NULL;
protected static ?Singleton $instance = NULL;
/**
* Get instance
*
* @param bool $skipCache Do not use the cached instance if one exists
* @param bool $skipCache Do not use the cached instance if one exists
* @return static
*/
public static function i( $skipCache=FALSE )
public static function i( bool $skipCache=FALSE ): static
{
if( static::$instance === NULL OR $skipCache === TRUE )
{
if ( \IPS\Settings::i()->search_method == 'elastic' )
if ( Settings::i()->search_method == 'elastic' )
{
static::$instance = new \IPS\Content\Search\Elastic\Index( \IPS\Http\Url::external( rtrim( \IPS\Settings::i()->search_elastic_server, '/' ) . '/' . \IPS\Settings::i()->search_elastic_index ) );
static::$instance = new Elastic\Index( Url::external( rtrim( Settings::i()->search_elastic_server, '/' ) . '/' . Settings::i()->search_elastic_index ) );
}
else
{
static::$instance = new \IPS\Content\Search\Mysql\Index;
static::$instance = new Mysql\Index;
}
}
@@ -55,11 +80,11 @@ abstract class _Index extends \IPS\Patterns\Singleton
*
* @return static
*/
public static function massIndexer()
public static function massIndexer(): static
{
if ( \IPS\Settings::i()->search_method == 'elastic' )
if ( Settings::i()->search_method == 'elastic' )
{
return new \IPS\Content\Search\Elastic\MassIndexer( \IPS\Http\Url::external( rtrim( \IPS\Settings::i()->search_elastic_server, '/' ) . '/' . \IPS\Settings::i()->search_elastic_index ) );
return new MassIndexer( Url::external( rtrim( Settings::i()->search_elastic_server, '/' ) . '/' . Settings::i()->search_elastic_index ) );
}
else
{
@@ -72,7 +97,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
*
* @return void
*/
public function init()
public function init(): void
{
// Does nothing by default
}
@@ -82,45 +107,47 @@ abstract class _Index extends \IPS\Patterns\Singleton
*
* @return void
*/
public function rebuild()
public function rebuild(): void
{
/* Delete everything currently in it */
$this->prune();
/* If the queue is already running, clear it out */
\IPS\Db::i()->delete( 'core_queue', array( "`key`=?", 'RebuildSearchIndex' ) );
Db::i()->delete( 'core_queue', array( "`key`=?", 'RebuildSearchIndex' ) );
/* And set the queue in motion to rebuild */
foreach ( \IPS\Content::routedClasses( FALSE ) as $class )
foreach( SearchContent::searchableClasses() as $class )
{
try
{
if( is_subclass_of( $class, 'IPS\Content\Searchable' ) )
{
\IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ), 5, TRUE );
}
Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ), 5, array( 'class' ) );
}
catch( \OutOfRangeException $ex ) {}
catch( OutOfRangeException ) {}
}
}
/**
* Index a single items comments and reviews if applicable.
*
* @param \IPS\Content\Searchable $object The item to index
* @param Item $object The item to index
* @return void
*/
public function indexSingleItem( \IPS\Content\Searchable $object )
public function indexSingleItem( Item $object ): void
{
if( !SearchContent::isSearchable( $object ) )
{
return;
}
/* It is possible for some items to not have a valid URL */
try
{
if( !$url = (string) $object->url() )
{
throw new \LogicException;
throw new LogicException;
}
}
catch( \LogicException $e )
catch( LogicException )
{
$url = '';
}
@@ -128,66 +155,77 @@ abstract class _Index extends \IPS\Patterns\Singleton
try
{
$idColumn = $object::$databaseColumnId;
if ( isset( $object::$commentClass ) AND is_subclass_of( $object::$commentClass, 'IPS\Content\Searchable' ) )
if ( isset( $object::$commentClass ) AND SearchContent::isSearchable( $object::$commentClass ) )
{
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$commentClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ), 5, TRUE );
Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$commentClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ) );
}
if ( isset( $object::$reviewClass ) AND is_subclass_of( $object::$reviewClass, 'IPS\Content\Searchable' ) )
if ( isset( $object::$reviewClass ) AND SearchContent::isSearchable( $object::$reviewClass ) )
{
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$reviewClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ), 5, TRUE );
Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$reviewClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ) );
}
}
catch( \OutOfRangeException $e ) {}
catch( OutOfRangeException ) {}
}
/**
* Get index data
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return array|NULL
*/
public function indexData( \IPS\Content\Searchable $object )
public function indexData( Content $object ): array|NULL
{
/* Init */
$class = \get_class( $object );
$class = get_class( $object );
if( !SearchContent::isSearchable( $class ) )
{
return NULL;
}
$extension = SearchContent::extension( $object );
if( $extension === null )
{
return NULL;
}
$extension->setObject( $object );
$idColumn = $class::$databaseColumnId;
$tags = ( $object instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? implode( ',', array_filter( $object->tags() ) ) : NULL;
$prefix = ( $object instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? $object->prefix() : NULL;
$tags = ( IPS::classUsesTrait( $object, 'IPS\Content\Taggable' ) and Settings::i()->tags_enabled ) ? implode( ',', array_filter( $object->tags() ) ) : NULL;
$prefix = ( IPS::classUsesTrait( $object, 'IPS\Content\Taggable' ) and Settings::i()->tags_enabled ) ? $object->prefix() : NULL;
/* If this is an item where the first comment is required, don't index because the comment will serve as both */
if ( $object instanceof \IPS\Content\Item and $class::$firstCommentRequired )
if ( $object instanceof Item and $object::$firstCommentRequired )
{
return NULL;
}
/* Don't index if this is an item to be published in the future */
if ( $object->isFutureDate() )
if ( IPS::classUsesTrait( $object, 'IPS\Content\FuturePublishing' ) AND $object->isFutureDate() )
{
return NULL;
}
/* Or if this *is* the first comment, add the title and replace the tags */
$title = $object->searchIndexTitle();
$title = $extension->searchIndexTitle();
$isForItem = FALSE;
if ( $object instanceof \IPS\Content\Comment )
if ( $object instanceof Comment )
{
$itemClass = $class::$itemClass;
$itemClass = $object::$itemClass;
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
{
try
{
$item = $object->item();
}
catch( \OutOfRangeException $ex )
catch( OutOfRangeException )
{
/* Comment has no working item, return */
return NULL;
}
$title = $item->searchIndexTitle();
$tags = ( $item instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? implode( ',', array_filter( $item->tags() ) ) : NULL;
$prefix = ( $item instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? $item->prefix() : NULL;
$tags = ( IPS::classUsesTrait( $item, 'IPS\Content\Taggable' ) and Settings::i()->tags_enabled ) ? implode( ',', array_filter( $item->tags() ) ) : NULL;
$prefix = ( IPS::classUsesTrait( $item, 'IPS\Content\Taggable' ) and Settings::i()->tags_enabled ) ? $item->prefix() : NULL;
$isForItem = TRUE;
}
}
@@ -202,7 +240,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
{
$dateUpdated = $object->mapped('date');
$dateCommented = $object->mapped('date');
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
foreach ( array( 'last_comment', 'last_review', 'updated' ) as $k )
{
@@ -221,15 +259,15 @@ abstract class _Index extends \IPS\Patterns\Singleton
}
}
/* Is the the latest content? */
/* Is this the latest content? */
$isLastComment = 0;
if ( $object instanceof \IPS\Content\Comment )
if ( $object instanceof Comment )
{
try
{
$item = $object->item();
}
catch( \OutOfRangeException $ex )
catch( OutOfRangeException )
{
/* Comment has no parent item, return */
return NULL;
@@ -248,9 +286,9 @@ abstract class _Index extends \IPS\Patterns\Singleton
{
$isLastComment = 1;
}
/* If this comment is hidden, don't actually mark as the last comment as that will cause this item to be hidden in search if we are getting only the last comment. */
if ( $isLastComment AND $object->hidden() AND !$object->isFirst() )
if ( $isLastComment and $object->hidden() and !$object->isFirst() )
{
$isLastComment = 0;
}
@@ -265,7 +303,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
}
}
}
else if ( $object instanceof \IPS\Content\Item and ! $class::$firstCommentRequired )
else if ( $object instanceof Item and ! $object::$firstCommentRequired )
{
/* If this is item itself and not a comment, then we will store it as the last comment so the activity stream fetches the data correctly */
$isLastComment = 1;
@@ -281,7 +319,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
/* Is the item itself searchable but the comment not? */
$commentClass = $object::$commentClass;
if ( ! ( is_subclass_of( $commentClass, 'IPS\Content\Searchable' ) ) )
if( !$commentClass OR !SearchContent::isSearchable( $commentClass ) )
{
/* Then make this the last comment so it remains searchable */
$isLastComment = 1;
@@ -289,34 +327,49 @@ abstract class _Index extends \IPS\Patterns\Singleton
}
/* Strip spoilers */
$content = $object->searchIndexContent();
$content = $extension->searchIndexContent();
if ( preg_match( '#<div\s+?class=["\']ipsSpoiler["\']#', $content ) )
{
$content = \IPS\Text\Parser::removeElements( $content, array( 'div[class=ipsSpoiler]' ) );
$content = Parser::removeElements( $content, array( 'div[class=ipsSpoiler]' ) );
}
/* Take the HTML out of the content */
$content = trim( str_replace( \chr(0xC2) . \chr(0xA0), ' ', strip_tags( preg_replace( "/(<br(?: \/)?>|<\/p>)/i", ' ', preg_replace( "#<blockquote(?:[^>]+?)>.+?(?<!<blockquote)</blockquote>#s", " ", preg_replace( "#<script(.*?)>(.*)</script>#uis", "", ' ' . $content . ' ' ) ) ) ) ) );
$content = trim( str_replace( chr(0xC2) . chr(0xA0), ' ', strip_tags( preg_replace( "/(<br(?: \/)?>|<\/p>)/i", ' ', preg_replace( "#<blockquote(?:[^>]+?)>.+?(?<!<blockquote)</blockquote>#s", " ", preg_replace( "#<script(.*?)>(.*)</script>#uis", "", ' ' . $content . ' ' ) ) ) ) ) );
/* Work out the hidden status */
$hiddenStatus = $object->hidden();
if ( $hiddenStatus === 0 and method_exists( $object, 'item' ) and $object->item()->hidden() )
if( IPS::classUsesTrait( $object, 'IPS\Content\Hideable' ) )
{
$hiddenStatus = $isForItem ? $object->item()->hidden() : 2;
try
{
$hiddenStatus = $object->hidden();
if ( $hiddenStatus === 0 and method_exists( $object, 'item' ) and $object->item()->hidden() )
{
$hiddenStatus = $isForItem ? $object->item()->hidden() : 2;
}
if ( $hiddenStatus !== 0 and method_exists( $object, 'item' ) and IPS::classUsesTrait( $object, 'IPS\Content\FuturePublishing' ) AND $object->isFutureDate() )
{
$hiddenStatus = 0;
}
if ( $hiddenStatus === -3 )
{
return NULL;
}
}
catch( RuntimeException )
{
/* Some classes implement Hideable for other reasons, but don't actually use it. See \IPS\nexus\Package\Item */
$hiddenStatus = 0;
}
}
if ( $hiddenStatus !== 0 and method_exists( $object, 'item' ) and $object->item()->isFutureDate() )
else
{
$hiddenStatus = 0;
}
if ( $hiddenStatus === -3 )
{
return NULL;
}
/* Get the item index ID */
$itemIndexId = NULL;
$itemClass = NULL;
if ( $object instanceof \IPS\Content\Comment )
if ( $object instanceof Comment )
{
$itemClass = $object::$itemClass;
if ( $itemClass::$firstCommentRequired )
@@ -331,7 +384,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
$itemIndexId = $this->getIndexId( $object->item()->firstComment() );
}
catch ( \Exception $e ) { }
catch ( Exception ) { }
}
else
{
@@ -339,22 +392,22 @@ abstract class _Index extends \IPS\Patterns\Singleton
{
$itemIndexId = $this->getIndexId( $object->item() );
}
catch ( \UnderflowException $e )
catch ( UnderflowException )
{
try
{
/* Try and index parent */
\IPS\Content\Search\Index::i()->index( $object->item() );
Index::i()->index( $object->item() );
$itemIndexId = $this->getIndexId( $object->item() );
}
catch( \Exception $ex )
catch( Exception )
{
return NULL;
}
}
}
}
else if ( $object instanceof \IPS\Content\Item )
else if ( $object instanceof Item )
{
if ( ! $object::$firstCommentRequired )
{
@@ -364,45 +417,35 @@ abstract class _Index extends \IPS\Patterns\Singleton
/* Good, we need the index_item_index_id so this is not wiped on re-index */
$itemIndexId = $this->getIndexId( $object );
}
catch ( \Exception $e ) { }
catch ( Exception ) { }
}
}
/* Club */
$container = NULL;
$containerId = NULL;
$clubId = NULL;
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$containerId = (int) $object->searchIndexContainer();
$container = $object->containerWrapper();
}
else
{
$containerId = (int) $object->item()->mapped('container');
$container = $object->item()->containerWrapper();
}
if ( $container and \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) )
if ( $container and IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) )
{
$clubId = $container->{$container::clubIdColumn()};
}
/* Work out the container class */
if( $object instanceof \IPS\Content\Item )
{
$containerClass = ( $object->searchIndexContainerClass() ) ? \get_class( $object->searchIndexContainerClass() ) : NULL;
}
else
{
$containerClass = ( $object->item()->searchIndexContainerClass() ) ? \get_class( $object->item()->searchIndexContainerClass() ) : NULL;
}
$containerClass = ( $extension->searchIndexContainerClass() ) ? get_class( $extension->searchIndexContainerClass() ) : null;
/* Do we have an extension to modify this? */
foreach( \IPS\Application::enabledApplications() as $app )
foreach( Application::enabledApplications() as $app )
{
foreach ( $app->extensions( 'core', 'SearchIndex' ) as $extension )
foreach ( $app->extensions( 'core', 'SearchIndex' ) as $searchIndexExtension )
{
$content = $extension->content( $object, $content );
$content = $searchIndexExtension->content( $object, $content );
}
}
@@ -410,26 +453,26 @@ abstract class _Index extends \IPS\Patterns\Singleton
return array(
'index_class' => $class,
'index_object_id' => $object->$idColumn,
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
'index_item_id' => ( $object instanceof Item ) ? $object->$idColumn : $object->mapped('item'),
'index_container_class' => $containerClass,
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
'index_container_id' => $extension->searchIndexContainer(),
'index_title' => $title,
'index_content' => $content,
'index_permissions' => $object->searchIndexPermissions(),
'index_date_created' => \intval( $object->mapped('date') ),
'index_date_updated' => \intval( $dateUpdated ),
'index_date_commented' => \intval( $dateCommented ),
'index_permissions' => $extension->searchIndexPermissions(),
'index_date_created' => intval( $object->mapped('date') ),
'index_date_updated' => intval( $dateUpdated ),
'index_date_commented' => intval( $dateCommented ),
'index_author' => (int) $object->mapped('author'),
'index_tags' => $tags,
'index_prefix' => $prefix,
'index_hidden' => $hiddenStatus,
'index_item_index_id' => $itemIndexId,
'index_item_author' => \intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
'index_item_author' => intval( ( $object instanceof Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
'index_is_last_comment' => $isLastComment,
'index_club_id' => $clubId,
'index_class_type_id_hash' => md5( $class . ':' . $object->$idColumn ),
'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
'index_is_anon' => (int) ( IPS::classUsesTrait( $object, 'IPS\Content\Anonymous' ) AND $object->isAnonymous() ),
'index_item_solved' => ( $itemClass and IPS::classUsesTrait( $itemClass, 'IPS\Content\Solvable' ) ) ? ( $object->item()->mapped('solved_comment_id') == $object->$idColumn ) ? 1 : 0 : NULL
);
}
@@ -437,12 +480,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
* Get the object ID of the first comment.
* Used for posts, where the index_object_id is NOT the topic ID
*
* @param \IPS\Content\Searchable $object
* @param Content $object
* @return int|null
*/
protected function getFirstCommentId( \IPS\Content\Searchable $object ) : ?int
protected function getFirstCommentId( Content $object ) : ?int
{
if( $object instanceof \IPS\Content\Comment )
if( $object instanceof Comment )
{
$itemClass = $object::$itemClass;
if( $itemClass::$firstCommentRequired )
@@ -471,17 +514,17 @@ abstract class _Index extends \IPS\Patterns\Singleton
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
abstract public function index( \IPS\Content\Searchable $object );
abstract public function index( Content $object ): void;
/**
* Clear out any tasks associated with the search index method
*
* @return void
*/
public function clearTasks()
public function clearTasks(): void
{
// Do nothing by default
}
@@ -489,18 +532,18 @@ abstract class _Index extends \IPS\Patterns\Singleton
/**
* Retrieve the search ID for an item
*
* @param \IPS\Content\Searchable $object Item to add
* @return string
* @param Content $object Item to add
* @return mixed
*/
abstract public function getIndexId( \IPS\Content\Searchable $object ): string;
abstract public function getIndexId( Content $object ): mixed;
/**
* Remove item
*
* @param \IPS\Content\Searchable $object Item to remove
* @param Content $object Item to remove
* @return void
*/
abstract public function removeFromSearchIndex( \IPS\Content\Searchable $object );
abstract public function removeFromSearchIndex( Content $object ): void;
/**
* Removes all content for a classs
@@ -510,42 +553,21 @@ abstract class _Index extends \IPS\Patterns\Singleton
* @param int|NULL $authorId The author ID to delete, or NULL
* @return void
*/
abstract public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL );
abstract public function removeClassFromSearchIndex( string $class, int|null $containerId=NULL, int|null $authorId=NULL ): void;
/**
* Removes all content for a specific application from the index (for example, when uninstalling).
*
* @param \IPS\Application $application The application
* @param Application $application The application
* @return void
*/
public function removeApplicationContent( \IPS\Application $application )
public function removeApplicationContent( Application $application ): void
{
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $router )
foreach ( $application->extensions( 'core', 'SearchContent' ) as $extension )
{
foreach( $router->classes AS $class )
foreach( $extension::supportedClasses() as $class )
{
if ( is_subclass_of( $class, 'IPS\Content\Searchable' ) )
{
$this->removeClassFromSearchIndex( $class );
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
if ( is_subclass_of( $commentClass, 'IPS\Content\Searchable' ) )
{
$this->removeClassFromSearchIndex( $commentClass );
}
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
if ( is_subclass_of( $reviewClass, 'IPS\Content\Searchable' ) )
{
$this->removeClassFromSearchIndex( $reviewClass );
}
}
}
$this->removeClassFromSearchIndex( $class );
}
}
}
@@ -565,61 +587,61 @@ abstract class _Index extends \IPS\Patterns\Singleton
* @param bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
* @return void
*/
abstract public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE );
abstract public function massUpdate( string $class, int|null $containerId = NULL, int|null $itemId = NULL, string|null $newPermissions = NULL, int|null $newHiddenStatus = NULL, int|null $newContainer = NULL, int|null $authorId = NULL, int|null $newItemId = NULL, int|null $newItemAuthorId = NULL, bool $addAuthorToPermissions = FALSE ): void;
/**
* Update data for the first and last comment after a merge
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
*
* @param \IPS\Content\Item $item The item
* @param Item $item The item
* @return void
*/
abstract public function rebuildAfterMerge( \IPS\Content\Item $item );
abstract public function rebuildAfterMerge( Item $item ): void;
/**
* Prune search index
*
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @param DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @return void
*/
abstract public function prune( \IPS\DateTime $cutoff = NULL );
abstract public function prune( DateTime $cutoff = NULL ): void;
/**
* Reset the last comment flag in any given class/index_item_id
*
* @param string $class The class
* @param array $classes The class
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @return void
*/
abstract public function resetLastComment( $class, $indexItemId, $ignoreId = NULL, $firstCommentId = NULL );
abstract public function resetLastComment( array $classes, int|null $indexItemId, int|null $ignoreId = NULL, int|null $firstCommentId=null ): void;
/**
* Given a list of item index IDs, return the ones that a given member has participated in
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param Member $member The member
* @return array
*/
abstract public function iPostedIn( array $itemIndexIds, \IPS\Member $member );
abstract public function iPostedIn( array $itemIndexIds, Member $member ): array;
/**
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
*
* @param array $hashes Hashes
* @param \IPS\Member $member The member
* @param Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
abstract public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL );
abstract public function hashesWithPermission( array $hashes, Member $member, int|null $limit = NULL ): array;
/**
* Get timestamp of oldest thing in index
*
* @return int|null
*/
abstract public function firstIndexDate();
abstract public function firstIndexDate(): int|NULL;
/**
* Convert terms into stemmed terms for the highlighting JS
@@ -627,7 +649,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
* @param array $terms Terms
* @return array
*/
public function stemmedTerms( $terms )
public function stemmedTerms( array $terms ): array
{
return $terms;
}
@@ -637,7 +659,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
*
* @return bool
*/
public function supportViewFiltering()
public function supportViewFiltering(): bool
{
return TRUE;
}
+142 -139
View File
@@ -11,30 +11,48 @@
namespace IPS\Content\Search\Mysql;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Application;
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Search\Index as SearchIndex;
use IPS\DateTime;
use IPS\Db;
use IPS\IPS;
use IPS\Member;
use OutOfRangeException;
use function count;
use function defined;
use function get_class;
use function in_array;
use function is_array;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* MySQL Search Index
*/
class _Index extends \IPS\Content\Search\Index
class Index extends SearchIndex
{
/**
* Get index data
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return array|NULL
*/
public function indexData( \IPS\Content\Searchable $object )
public function indexData( Content $object ): array|NULL
{
$indexData = parent::indexData( $object );
if( $indexData === NULL )
{
return $indexData;
return null;
}
/* The index_title in core_search_index is varchar(255) so we have to limit to this length */
@@ -49,14 +67,16 @@ class _Index extends \IPS\Content\Search\Index
/**
* Index an item
*
* @param \IPS\Content\Searchable $object Item to add
* @param Content $object Item to add
* @return void
*/
public function index( \IPS\Content\Searchable $object )
public function index( Content $object ): void
{
/* Get the index data */
$indexData = $this->indexData( $object );
$class = \get_class( $object );
/* @var Content $class */
$class = get_class( $object );
/* If we got the data... */
if( $indexData )
@@ -77,10 +97,10 @@ class _Index extends \IPS\Content\Search\Index
try
{
$existingData = \IPS\Db::i()->select( 'index_id, index_class, index_object_id, index_item_id, index_hidden, index_is_last_comment, index_author', 'core_search_index', array( 'index_class=? AND index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) )->first();
$existingData = Db::i()->select( 'index_id, index_class, index_object_id, index_item_id, index_hidden, index_is_last_comment, index_author', 'core_search_index', array( 'index_class=? AND index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) )->first();
$existingIndexId = $existingData['index_id'];
}
catch( \Exception $e ) { }
catch( Exception ) { }
/* Adjust tags */
$tags = NULL;
@@ -92,7 +112,7 @@ class _Index extends \IPS\Content\Search\Index
unset( $indexData['index_prefix'] );
}
if ( $object instanceof \IPS\Content\Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
if ( $object instanceof Comment and $existingIndexId and $existingData['index_is_last_comment'] and $indexData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 )
{
/* We do not allow hidden or needing approval comments to become flagged as the last comment as this means users without hidden view permission never see the item in an item only stream */
$indexData['index_is_last_comment'] = 0;
@@ -104,7 +124,7 @@ class _Index extends \IPS\Content\Search\Index
$classes = array( $class );
/* If this is the latest comment, unflag what was set before on both item and comment */
if ( $object instanceof \IPS\Content\Comment )
if ( $object instanceof Comment )
{
$itemClass = $object::$itemClass;
if ( ! $itemClass::$firstCommentRequired )
@@ -117,7 +137,7 @@ class _Index extends \IPS\Content\Search\Index
$classes[] = $itemClass::$reviewClass;
}
}
else if ( $object instanceof \IPS\Content\Item )
else if ( $object instanceof Item )
{
if ( isset( $class::$commentClass ) )
{
@@ -129,59 +149,60 @@ class _Index extends \IPS\Content\Search\Index
}
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexData['index_item_id'] ) );
Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexData['index_item_id'] ) );
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $indexData['index_date_updated'], 'index_date_commented' => $indexData['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_object_id=?', $indexData['index_item_id'], ( $firstCommentId ?? $indexData['index_item_id'] ) ) );
Db::i()->update( 'core_search_index', array( 'index_date_updated' => $indexData['index_date_updated'], 'index_date_commented' => $indexData['index_date_commented'] ), array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_object_id=?', $indexData['index_item_id'], ( $firstCommentId ?? $indexData['index_item_id'] ) ) );
}
if ( $existingData !== NULL and ( $indexData['index_class'] == $existingData['index_class'] and $indexData['index_object_id'] == $existingData['index_object_id'] ) )
{
\IPS\Db::i()->update( 'core_search_index', $indexData, array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
Db::i()->update( 'core_search_index', $indexData, array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
$newIndexId = $existingIndexId;
}
else
{
if ( $existingData !== NULL )
{
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? and index_object_id=?', $indexData['index_class'], $indexData['index_object_id'] ) );
}
try
{
$newIndexId = \IPS\Db::i()->insert( 'core_search_index', $indexData );
$newIndexId = Db::i()->insert( 'core_search_index', $indexData );
}
catch( \IPS\Db\Exception $e )
catch( Db\Exception $e )
{
if ( $e->getCode() == 1062 )
{
/* Duplicate key which could be caused by a race condition on rebuild. Use replace in this case, as it is more expensive than an insert, so we only use it when we have to */
$newIndexId = \IPS\Db::i()->replace( 'core_search_index', $indexData );
$newIndexId = Db::i()->replace( 'core_search_index', $indexData );
}
}
}
/* If that was successful... */
/* @var array $databaseColumnMap */
if ( $newIndexId )
{
/* Remove existing tags */
if ( $existingIndexId )
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id=?', $existingIndexId ) );
Db::i()->delete( 'core_search_index_tags', array( 'index_id=?', $existingIndexId ) );
}
/* Add add them back if we have any */
if ( \count( $tags ) )
/* Add them back if we have any */
if ( count( $tags ) )
{
foreach( $tags as $tag )
{
\IPS\Db::i()->replace( 'core_search_index_tags', array( 'index_id' => $newIndexId, 'index_tag' => $tag, 'index_is_prefix' => ( $tag == $prefix ) ) );
Db::i()->replace( 'core_search_index_tags', array( 'index_id' => $newIndexId, 'index_tag' => $tag, 'index_is_prefix' => ( isset( $prefix ) AND $tag == $prefix ) ) );
}
}
/* Populate the map table, we always populate it under the item class regardless */
if ( $existingData == NULL or ( $existingData !== NULL and ( $existingData['index_author'] != $indexData['index_author'] OR $existingData['index_item_id'] != $indexData['index_item_id'] ) ) )
if ( $existingData == NULL or ( $existingData['index_author'] != $indexData['index_author'] OR $existingData['index_item_id'] != $indexData['index_item_id'] ) )
{
\IPS\Db::i()->replace( 'core_search_index_item_map', array( 'index_author_id' => $indexData['index_author'], 'index_item_id' => $indexData['index_item_id'], 'index_class' => ( $object instanceof \IPS\Content\Comment ? $object::$itemClass : $class ) ) );
Db::i()->replace( 'core_search_index_item_map', array( 'index_author_id' => $indexData['index_author'], 'index_item_id' => $indexData['index_item_id'], 'index_class' => ( $object instanceof Comment ? $object::$itemClass : $class ) ) );
}
$databaseColumnId = $object::$databaseColumnId;
@@ -189,7 +210,7 @@ class _Index extends \IPS\Content\Search\Index
/* Set index_item_index_id on other index items */
if ( $existingIndexId != $newIndexId )
{
if ( $object instanceof \IPS\Content\Item )
if ( $object instanceof Item )
{
$subClasses = array( $class );
if ( isset( $class::$commentClass ) )
@@ -201,15 +222,15 @@ class _Index extends \IPS\Content\Search\Index
$subClasses[] = $class::$reviewClass;
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( \IPS\Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
}
elseif ( $object instanceof \IPS\Content\Comment )
elseif ( $object instanceof Comment )
{
$itemClass = $object::$itemClass;
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
{
$itemColumnId = $class::$databaseColumnMap['item'];
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( \IPS\Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
}
}
}
@@ -226,33 +247,33 @@ class _Index extends \IPS\Content\Search\Index
/**
* Retrieve the search ID for an item
*
* @param \IPS\Content\Searchable $object Item to add
* @return string
* @param Content $object Item to add
* @return mixed
*/
public function getIndexId( \IPS\Content\Searchable $object ): string
public function getIndexId( Content $object ): mixed
{
$databaseColumnId = $object::$databaseColumnId;
return \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_object_id=?', \get_class( $object ),$object->$databaseColumnId ) )->first();
return Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_object_id=?', get_class( $object ),$object->$databaseColumnId ) )->first();
}
/**
* Remove item
*
* @param \IPS\Content\Searchable $object Item to remove
* @param Content $object Item to remove
* @return void
*/
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
public function removeFromSearchIndex( Content $object ): void
{
$class = \get_class( $object );
$class = get_class( $object );
$idColumn = $class::$databaseColumnId;
/* Tags */
$this->_deleteTagsFromIndex( $class, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_object_id=?', $class, $object->$idColumn ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_object_id=?', $class, $object->$idColumn ) );
/* If this was a comment, we really need to reset the index_is_last_comment flag if it was set */
if ( $object instanceof \IPS\Content\Comment )
if ( $object instanceof Comment )
{
$itemClass = $object::$itemClass;
$classes = array( $class );
@@ -269,38 +290,38 @@ class _Index extends \IPS\Content\Search\Index
try
{
$this->resetLastComment( $classes, $object->mapped('item'), $object->$idColumn, $this->getFirstCommentId( $object ) );
$this->resetLastComment( $classes, $object->mapped('item'), $object->$idColumn, null, $this->getFirstCommentId( $object ) );
}
catch( \Exception $ex ) { }
catch( Exception ) { }
/* We need to see if this is the only comment the author has in this item and if so, remove their map */
if ( ! \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index', array( \IPS\Db::i()->in('index_class', $classes ) . ' and index_item_id=? and index_author=?', $object->mapped('item'), (int) $object->mapped('author') ) )->first() )
if ( ! Db::i()->select( 'COUNT(*)', 'core_search_index', array( Db::i()->in('index_class', $classes ) . ' and index_item_id=? and index_author=?', $object->mapped('item'), (int) $object->mapped('author') ) )->first() )
{
try
{
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=? and index_author_id=?', $itemClass, $object->mapped('item'), (int) $object->mapped('author') ) );
Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=? and index_author_id=?', $itemClass, $object->mapped('item'), (int) $object->mapped('author') ) );
}
catch( \Exception $ex ) { }
catch( Exception ) { }
}
}
else if ( $object instanceof \IPS\Content\Item )
else if ( $object instanceof Item )
{
/* Just remove all rows matching the item and class. */
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=?', $class, $object->$idColumn ) );
Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=?', $class, $object->$idColumn ) );
}
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$this->_deleteTagsFromIndex( $commentClass, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $commentClass, $object->$idColumn ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $commentClass, $object->$idColumn ) );
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->_deleteTagsFromIndex( $reviewClass, $object->$idColumn );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $object->$idColumn ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $object->$idColumn ) );
}
}
@@ -311,106 +332,84 @@ class _Index extends \IPS\Content\Search\Index
* @param int $id ID
* @return void
*/
public function directIndexRemoval( $class, $id )
public function directIndexRemoval( string $class, int $id ): void
{
/* Tags */
$this->_deleteTagsFromIndex( $class, $id );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_object_id=?', $class, $id ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_object_id=?', $class, $id ) );
/* If this was a comment, we really need to reset the index_is_last_comment flag if it was set */
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=?', $class, $id ) );
Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? AND index_item_id=?', $class, $id ) );
}
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$this->_deleteTagsFromIndex( $commentClass, $id );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $commentClass, $id ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $commentClass, $id ) );
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->_deleteTagsFromIndex( $reviewClass, $id );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $id ) );
Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $id ) );
}
}
/**
* Return the index IDs associated with this class and $id
*
* @param \IPS\Content object $class The class
* @param string $class The class
* @param int $id The index_item_id
* @return array
*/
protected function _deleteTagsFromIndex( $class, $id )
protected function _deleteTagsFromIndex( string $class, int $id ): array
{
try
{
$ids = iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) );
$ids = iterator_to_array( Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) );
}
catch( \Exception $ex )
catch( Exception )
{
$ids = FALSE;
}
if ( \is_array( $ids ) and \count( $ids ) < 1000 )
if ( is_array( $ids ) and count( $ids ) < 1000 )
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_id', $ids ) ) );
Db::i()->delete( 'core_search_index_tags', array( Db::i()->in( 'index_id', $ids ) ) );
}
else
{
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) ) );
Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_item_id=?', $class, $id ) ) ) );
}
return is_array( $ids ) ? $ids : array();
}
/**
* Removes all content for a specific application from the index (for example, when uninstalling).
*
* @param \IPS\Application $application The application
* @param Application $application The application
* @return void
*/
public function removeApplicationContent( \IPS\Application $application )
public function removeApplicationContent( Application $application ): void
{
$classes = array();
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $router )
foreach ( $application->extensions( 'core', 'SearchContent' ) as $extension )
{
foreach( $router->classes AS $class )
{
if ( is_subclass_of( $class, 'IPS\Content\Searchable' ) )
{
$classes[] = $class;
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
if ( is_subclass_of( $commentClass, 'IPS\Content\Searchable' ) )
{
$classes[] = $commentClass;
}
}
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
if ( is_subclass_of( $reviewClass, 'IPS\Content\Searchable' ) )
{
$classes[] = $reviewClass;
}
}
}
}
$classes = array_merge( $classes, $extension::supportedClasses() );
}
$where = array( \IPS\Db::i()->in( 'index_class', $classes ) );
$where = array( Db::i()->in( 'index_class', $classes ) );
\IPS\Db::i()->delete( 'core_search_index_item_map', $where );
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index', $where );
Db::i()->delete( 'core_search_index_item_map', $where );
Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
Db::i()->delete( 'core_search_index', $where );
}
/**
@@ -421,7 +420,7 @@ class _Index extends \IPS\Content\Search\Index
* @param int|NULL $authorId The author ID to delete, or NULL
* @return void
*/
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
public function removeClassFromSearchIndex( string $class, int|null $containerId=null, int|null $authorId=NULL ): void
{
$where = array( array( 'index_class=?', $class ) );
if ( $containerId !== NULL )
@@ -433,9 +432,9 @@ class _Index extends \IPS\Content\Search\Index
$where[] = array( 'index_author=?', $authorId );
}
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
\IPS\Db::i()->delete( 'core_search_index', $where );
Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
Db::i()->delete( 'core_search_index', $where );
}
/**
@@ -453,8 +452,13 @@ class _Index extends \IPS\Content\Search\Index
* @param bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
* @return void
*/
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
public function massUpdate( string $class, int|null $containerId = null, int|null $itemId = NULL, string|null $newPermissions = NULL, int|null $newHiddenStatus = NULL, int|null $newContainer = NULL, int|null $authorId = NULL, int|null $newItemId = NULL, int|null $newItemAuthorId = NULL, bool $addAuthorToPermissions = FALSE ): void
{
if( !Content\Search\SearchContent::isSearchable( $class) )
{
return;
}
$where = array( array( 'index_class=?', $class ) );
if ( $containerId !== NULL )
{
@@ -478,13 +482,13 @@ class _Index extends \IPS\Content\Search\Index
{
$update['index_container_id'] = $newContainer;
if ( $itemClass = ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
{
try
{
$update['index_club_id'] = $containerClass::load( $newContainer )->$clubIdColumn;
}
catch ( \OutOfRangeException $e )
catch ( OutOfRangeException )
{
$update['index_club_id'] = NULL;
}
@@ -499,15 +503,15 @@ class _Index extends \IPS\Content\Search\Index
$update['index_item_author'] = $newItemAuthorId;
}
if ( \count( $update ) )
if ( count( $update ) )
{
\IPS\Db::i()->update( 'core_search_index', $update, $where );
Db::i()->update( 'core_search_index', $update, $where );
}
if ( $addAuthorToPermissions )
{
$addAuthorToPermissionsWhere = $where;
$addAuthorToPermissionsWhere[] = array( 'index_author<>0' );
\IPS\Db::i()->update( 'core_search_index', "index_permissions = CONCAT( index_permissions, ',m', index_author )", $addAuthorToPermissionsWhere );
Db::i()->update( 'core_search_index', "index_permissions = CONCAT( index_permissions, ',m', index_author )", $addAuthorToPermissionsWhere );
}
if ( $newHiddenStatus !== NULL )
@@ -521,7 +525,7 @@ class _Index extends \IPS\Content\Search\Index
$where[] = array( 'index_hidden=2' );
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_hidden' => $newHiddenStatus ), $where );
Db::i()->update( 'core_search_index', array( 'index_hidden' => $newHiddenStatus ), $where );
}
}
@@ -529,10 +533,10 @@ class _Index extends \IPS\Content\Search\Index
* Update data for the first and last comment after a merge
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
*
* @param \IPS\Content\Item $item The item
* @param Item $item The item
* @return void
*/
public function rebuildAfterMerge( \IPS\Content\Item $item )
public function rebuildAfterMerge( Item $item ): void
{
if ( $item::$commentClass )
{
@@ -545,7 +549,7 @@ class _Index extends \IPS\Content\Search\Index
{
$update['index_title'] = NULL;
}
\IPS\Db::i()->update( 'core_search_index', $update, array( 'index_class=? AND index_item_id=?', $item::$commentClass, $item->$idColumn ) );
Db::i()->update( 'core_search_index', $update, array( 'index_class=? AND index_item_id=?', $item::$commentClass, $item->$idColumn ) );
if ( $firstComment )
{
@@ -561,22 +565,22 @@ class _Index extends \IPS\Content\Search\Index
/**
* Prune search index
*
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @param DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
* @return void
*/
public function prune( \IPS\DateTime $cutoff = NULL )
public function prune( DateTime|null $cutoff = NULL ): void
{
if ( $cutoff )
{
\IPS\Db::i()->delete( array( 'core_search_index_item_map', 'core_search_index' ), array( "core_search_index_item_map.index_item_id=core_search_index.index_item_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_item_map' );
\IPS\Db::i()->delete( array( 'core_search_index_tags', 'core_search_index' ), array( "core_search_index_tags.index_id=core_search_index.index_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_tags' );
\IPS\Db::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
Db::i()->delete( array( 'core_search_index_item_map', 'core_search_index' ), array( "core_search_index_item_map.index_item_id=core_search_index.index_item_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_item_map' );
Db::i()->delete( array( 'core_search_index_tags', 'core_search_index' ), array( "core_search_index_tags.index_id=core_search_index.index_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_tags' );
Db::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
}
else
{
\IPS\Db::i()->delete( 'core_search_index_item_map' );
\IPS\Db::i()->delete( 'core_search_index_tags' );
\IPS\Db::i()->delete( 'core_search_index' );
Db::i()->delete( 'core_search_index_item_map' );
Db::i()->delete( 'core_search_index_tags' );
Db::i()->delete( 'core_search_index' );
}
}
@@ -586,72 +590,71 @@ class _Index extends \IPS\Content\Search\Index
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @param int|null $firstCommentId The first comment in this item, used only if $firstCommentRequired
* @return void
*/
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL, $firstCommentId = NULL )
public function resetLastComment( array $classes, int|null $indexItemId, int|null $ignoreId = null, int|null $firstCommentId=null ): void
{
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexItemId ) );
Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexItemId ) );
try
{
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
$latest = Db::i()->select( 'index_object_id, index_date_updated, index_class', 'core_search_index', array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
}
catch( \Exception )
catch( Exception )
{
/* Didn't find a latest? Was it because the entire thing is hidden? */
try
{
$index = \IPS\Db::i()->select( 'index_id, index_item_index_id, index_hidden', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
$index = Db::i()->select( 'index_id, index_item_index_id, index_hidden', 'core_search_index', array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=?', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
/* Is the root item hidden? */
if ( $index['index_id'] == $index['index_item_index_id'] and in_array( $index['index_hidden'], [ -1, 2 ] ) )
{
$latest = \IPS\Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=?', $indexItemId, $index['index_hidden'] ), 'index_date_created DESC', array( 0, 1 ) )->first();
$latest = Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=?', $indexItemId, $index['index_hidden'] ), 'index_date_created DESC', array( 0, 1 ) )->first();
}
else
{
/* No, so we can't find a latest item that is hidden, so skip this */
return FALSE;
return;
}
}
catch( \Exception )
catch( Exception )
{
return FALSE;
return;
}
}
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
/* Now reset the item index with the latest comment time */
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, ( $firstCommentId ?? $indexItemId ) ) );
Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, ( $firstCommentId ?? $indexItemId ) ) );
}
/**
* Given a list of item index IDs, return the ones that a given member has participated in
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param Member $member The member
* @return array
*/
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
public function iPostedIn( array $itemIndexIds, Member $member ): array
{
return iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', $member->member_id ) ) )->setKeyField('index_item_index_id') );
return iterator_to_array( Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', $member->member_id ) ) )->setKeyField('index_item_index_id') );
}
/**
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param array $hashes Item index IDs
* @param Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL )
public function hashesWithPermission( array $hashes, Member $member, int|null $limit = null ): array
{
return iterator_to_array( \IPS\Db::i()->select( 'index_class_type_id_hash', array( 'core_search_index', 'si' ), array(
array( "( si.index_permissions = '*' OR " . \IPS\Db::i()->findInSet( 'si.index_permissions', \IPS\Member::loggedIn()->permissionArray() ) . ' AND si.index_hidden=0 )' ),
array( \IPS\Db::i()->in( 'si.index_class_type_id_hash', $hashes ) )
return iterator_to_array( Db::i()->select( 'index_class_type_id_hash', array( 'core_search_index', 'si' ), array(
array( "( si.index_permissions = '*' OR " . Db::i()->findInSet( 'si.index_permissions', Member::loggedIn()->permissionArray() ) . ' AND si.index_hidden=0 )' ),
array( Db::i()->in( 'si.index_class_type_id_hash', $hashes ) )
), NULL, $limit )->setKeyField('index_class_type_id_hash') );
}
@@ -660,8 +663,8 @@ class _Index extends \IPS\Content\Search\Index
*
* @return int|null
*/
public function firstIndexDate()
public function firstIndexDate(): int|NULL
{
return \IPS\Db::i()->select( 'MIN(index_date_updated)', 'core_search_index' )->first();
return Db::i()->select( 'MIN(index_date_updated)', 'core_search_index' )->first();
}
}
File diff suppressed because it is too large. Load diff
+112 -71
View File
@@ -11,16 +11,28 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Content;
use IPS\DateTime;
use IPS\Db;
use IPS\Db\Select;
use IPS\Http\Url;
use IPS\Member;
use IPS\Member\Club;
use IPS\Settings;
use function defined;
use function is_null;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Abstract Search Query
*/
abstract class _Query
abstract class Query
{
const TERM_OR_TAGS = 1; // If both tags and a term are provided, match results where either the tags or the term match
const TERM_AND_TAGS = 2; // If both tags and a term are provided, match only results where BOTH the tags or the term match
@@ -47,38 +59,38 @@ abstract class _Query
/**
* Create new query
*
* @param \IPS\Member $member The member performing the search (NULL for currently logged in member)
* @return \IPS\Content\Search\Query
* @param Member|null $member The member performing the search (NULL for currently logged in member)
* @return Query
*/
public static function init( \IPS\Member $member = NULL )
public static function init( Member|null $member = NULL ): static
{
if ( \IPS\Settings::i()->search_method == 'elastic' )
if ( Settings::i()->search_method == 'elastic' )
{
return new \IPS\Content\Search\Elastic\Query( $member ?: \IPS\Member::loggedIn(), \IPS\Http\Url::external( rtrim( \IPS\Settings::i()->search_elastic_server, '/' ) . '/' . \IPS\Settings::i()->search_elastic_index ) );
return new Elastic\Query( $member ?: Member::loggedIn(), Url::external( rtrim( Settings::i()->search_elastic_server, '/' ) . '/' . Settings::i()->search_elastic_index ) );
}
else
{
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
return new Mysql\Query( $member ?: Member::loggedIn() );
}
}
/**
* @brief Number of results to get
*/
public $resultsToGet = 25;
public int $resultsToGet = 25;
/**
* @brief The member performing the search
*/
protected $member;
protected Member $member;
/**
* Constructor
*
* @param \IPS\Member $member The member performing the search
* @param Member $member The member performing the search
* @return void
*/
public function __construct( \IPS\Member $member )
public function __construct( Member $member )
{
$this->member = $member;
@@ -90,9 +102,9 @@ abstract class _Query
/* Exclude disabled applications */
$filters = array();
foreach ( \IPS\Content::routedClasses( $member, FALSE, TRUE ) as $class )
foreach ( Content::routedClasses( $member, FALSE, TRUE ) as $class )
{
$filters[] = \IPS\Content\Search\ContentFilter::init( $class );
$filters[] = ContentFilter::init( $class );
}
if ( !empty( $filters ) )
{
@@ -105,50 +117,50 @@ abstract class _Query
*
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByContent( array $contentFilters, $type = TRUE );
abstract public function filterByContent( array $contentFilters, bool $type = TRUE ): static;
/**
* Filter by author
*
* @param \IPS\Member|int|array $author The author, or an array of author IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member|int|array $author The author, or an array of author IDs
* @return Query (for daisy chaining)
*/
abstract public function filterByAuthor( $author );
abstract public function filterByAuthor( Member|int|array $author ): static;
/**
* Filter by club
*
* @param \IPS\Member\Club|int|array $club The club, or array of club IDs
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Club|int|array|null $club The club, or array of club IDs
* @return Query (for daisy chaining)
*/
abstract public function filterByClub( $club );
abstract public function filterByClub( Club|int|array|null $club ): static;
/**
* Filter for profile
*
* @param \IPS\Member $member The member whose profile is being viewed
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $member The member whose profile is being viewed
* @return Query (for daisy chaining)
*/
abstract public function filterForProfile( \IPS\Member $member );
abstract public function filterForProfile( Member $member ): static;
/**
* Filter by item author
*
* @param \IPS\Member $author The author
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param Member $author The author
* @return Query (for daisy chaining)
*/
abstract public function filterByItemAuthor( \IPS\Member $author );
abstract public function filterByItemAuthor( Member $author ): static;
/**
* Filter by container class
*
* @param array $classes Container classes to exclude from results.
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class such as Gallery images, comments and reviews.
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByContainerClasses( $classes=array(), $exclude=array() );
abstract public function filterByContainerClasses( array $classes=array(), array $exclude=array() ): static;
/**
* Filter by content the user follows
@@ -156,72 +168,72 @@ abstract class _Query
* @param bool $includeContainers Include content in containers the user follows?
* @param bool $includeItems Include items and comments/reviews on items the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByFollowed( $includeContainers, $includeItems, $includeMembers );
abstract public function filterByFollowed( bool $includeContainers, bool $includeItems, bool $includeMembers ): static;
/**
* Filter by content the user has posted in
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByItemsIPostedIn();
abstract public function filterByItemsIPostedIn(): static;
/**
* Filter by content the user has not read
*
* @note If applicable, it is more efficient to call filterByContent() before calling this method
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByUnread();
abstract public function filterByUnread(): static;
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterBySolved();
abstract public function filterBySolved(): static;
/**
* Filter by content the user has not read
*
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function filterByUnsolved();
abstract public function filterByUnsolved(): static;
/**
* Filter by start date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
abstract public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL );
abstract public function filterByCreateDate( DateTime|null $start = NULL, DateTime|null $end = NULL ): static;
/**
* Filter by last updated date
*
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @param DateTime|NULL $start The start date (only results AFTER this date will be returned)
* @param DateTime|NULL $end The end date (only results BEFORE this date will be returned)
* @return Query (for daisy chaining)
*/
abstract public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL );
abstract public function filterByLastUpdatedDate( DateTime|null $start = NULL, DateTime|null $end = NULL ): static;
/**
* Set hidden status
*
* @param int|array|NULL $statuses The statuses (see HIDDEN_ constants) or NULL for any
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function setHiddenFilter( $statuses );
abstract public function setHiddenFilter( int|array|null $statuses ): static;
/**
* Set limit
*
* @param int $limit Number per page
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
public function setLimit( $limit )
public function setLimit( int $limit ): static
{
$this->resultsToGet = $limit;
return $this;
@@ -231,24 +243,24 @@ abstract class _Query
* Set page
*
* @param int $page The page number
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function setPage( $page );
abstract public function setPage( int $page ): static;
/**
* Set order
*
* @param int $order Order (see ORDER_ constants)
* @return \IPS\Content\Search\Query (for daisy chaining)
* @return Query (for daisy chaining)
*/
abstract public function setOrder( $order );
abstract public function setOrder( int $order ): static;
/**
* Permission Array
*
* @return array
*/
public function permissionArray()
public function permissionArray(): array
{
return $this->member->permissionArray();
}
@@ -260,16 +272,16 @@ abstract class _Query
* @param array|null $tags The tags to search for
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
* @return \IPS\Content\Search\Results
* @return Results
*/
abstract public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL );
abstract public function search( string|null $term = NULL, array|null $tags = NULL, int $method = 1, string|null $operator = NULL ): Results;
/**
* Get the default date cut off
*
* @return string
*/
public function getDefaultDateCutOff()
public function getDefaultDateCutOff(): string
{
return 'any';
}
@@ -279,7 +291,7 @@ abstract class _Query
*
* @return string
*/
public function getDefaultSortMethod()
public function getDefaultSortMethod(): string
{
return 'relevancy';
}
@@ -301,9 +313,9 @@ abstract class _Query
* @param array|null $tags The tags to search for
* @param int $method \IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
* @return int
* @return Select|int
*/
public function count( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
public function count( string|null $term = NULL, array|null $tags = NULL, int $method = 1, string|null $operator = NULL ): Select|int
{
return $this->search( $term, $tags, $method, $operator )->count( TRUE );
}
@@ -314,9 +326,9 @@ abstract class _Query
* @param string $term The term to search for
* @return boolean
*/
public static function termIsPhrase( $term )
public static function termIsPhrase( string $term ): bool
{
return (boolean) preg_match( '#^".*"$#', $term );
return (bool) preg_match( '#^".*"$#', $term );
}
/**
@@ -328,10 +340,10 @@ abstract class _Query
* @param int|NULL $maxLength The maximum length a sequence of characters can be for it to be considered a word. If null, ft_max_word_len/innodb_ft_max_token_size is used.
* @return array
*/
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=NULL, $maxLength=NULL )
public static function termAsWordsArray( string $term, bool $ignorePhrase=FALSE, int|null $minLength=NULL, int|null $maxLength=NULL ): array
{
$minLength = \is_null( $minLength ) ? 0 : $minLength;
$maxLength = \is_null( $maxLength ) ? 255 : $maxLength;
$minLength = is_null( $minLength ) ? 0 : $minLength;
$maxLength = is_null( $maxLength ) ? 255 : $maxLength;
/* Sub quotes with smaller than min word strings can cause issues for larger tables, so we convert the sub quote into normal words for a boolean search */
if ( ! static::termIsPhrase( $term ) )
@@ -405,8 +417,37 @@ abstract class _Query
*
* @return boolean
*/
public static function isRebuildRunning()
public static function isRebuildRunning(): bool
{
return \IPS\Db::i()->select( 'COUNT(*)', 'core_queue', array( '`key`=?', 'RebuildSearchIndex' ) )->first() ? TRUE : FALSE;
return (bool) Db::i()->select( 'COUNT(*)', 'core_queue', array( '`key`=?', 'RebuildSearchIndex' ) )->first();
}
/**
* Run custom search engine filters
*
* @param bool $itemsOnly
* @return void
*/
public function customFiltering( bool $itemsOnly=FALSE ) : void
{
/* Get the list of valid classes */
$classesChecked = array();
foreach( SearchContent::searchableClasses() as $class )
{
if ( $itemsOnly AND in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
}
/* Give content item classes a chance to inspect and manipulate filters */
$filters = array();
foreach( $classesChecked as $itemClass )
{
if( $extension = SearchContent::extension( $itemClass ) )
{
$extension::searchEngineFiltering( $filters, $this );
}
}
}
}
+54 -24
View File
@@ -11,46 +11,67 @@
namespace IPS\Content\Search\Result;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use Exception;
use IPS\Content\Comment;
use IPS\Content\Search\Result;
use IPS\Content\Search\SearchContent;
use IPS\DateTime;
use IPS\File;
use IPS\Xml\Rss;
use OutOfRangeException;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Search Result
*/
class _Content extends \IPS\Content\Search\Result
class Content extends Result
{
/**
* Index Data
*/
protected $indexData;
protected array $indexData;
/**
* Author Data
*/
protected $authorData;
protected array $authorData;
/**
* Item Data
*/
protected $itemData;
protected array $itemData;
/**
* Author Data
*/
protected $containerData;
protected array|null $containerData;
/**
* Reputation Data
*/
protected array|null $reputationData;
/**
* Review Rating
*/
protected $reviewRating;
protected int|null $reviewRating;
/**
* If the user has posted in the item
*/
protected $iPostedIn;
protected mixed $iPostedIn = array();
/**
* Reactions
*/
protected array $reactions;
/**
* Constructor
@@ -65,10 +86,10 @@ class _Content extends \IPS\Content\Search\Result
* @param array $reactions Reaction Data
* @return void
*/
public function __construct( array $indexData, array $authorData, array $itemData, ?array $containerData, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE, $reactions=array() )
public function __construct( array $indexData, array $authorData, array $itemData, array|null $containerData, array $reputationData, int|null $reviewRating = NULL, bool $iPostedIn = FALSE, array $reactions=array() )
{
$this->createdDate = \IPS\DateTime::ts( $indexData['index_date_created'] );
$this->lastUpdatedDate = \IPS\DateTime::ts( $indexData['index_date_updated'] );
$this->createdDate = DateTime::ts( $indexData['index_date_created'] );
$this->lastUpdatedDate = DateTime::ts( $indexData['index_date_updated'] );
$this->indexData = $indexData;
$this->authorData = $authorData;
$this->itemData = $itemData;
@@ -88,10 +109,14 @@ class _Content extends \IPS\Content\Search\Result
* @param array|NULL $template Optional custom template
* @return string
*/
public function html( $view = 'expanded', $asItem = FALSE, $canIgnoreComments=FALSE, $template=NULL )
public function html( string $view = 'expanded', bool $asItem = FALSE, bool $canIgnoreComments=FALSE, array|null $template=NULL ): string
{
$searchResultTemplate = array( $this->indexData['index_class'], 'searchResult' );
return $searchResultTemplate( $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem, $canIgnoreComments, $template, $this->reactions );
if( $extension = SearchContent::extension( $this->indexData['index_class'] ) )
{
$searchResultTemplate = array( $extension, 'searchResult' );
return $searchResultTemplate( $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem, $canIgnoreComments, $template, $this->reactions );
}
return "";
}
/**
@@ -99,8 +124,10 @@ class _Content extends \IPS\Content\Search\Result
*
* @return array( 'indexData' => array() ... )
*/
public function asArray()
public function asArray(): array
{
$extension = SearchContent::extension( $this->indexData['index_class'] );
return array(
'indexData' => $this->indexData,
'authorData' => $this->authorData,
@@ -108,17 +135,20 @@ class _Content extends \IPS\Content\Search\Result
'containerData' => $this->containerData,
'reputationData' => $this->reputationData,
'reviewRating' => $this->reviewRating,
'iPostedIn' => $this->iPostedIn
'iPostedIn' => $this->iPostedIn,
'template' => $extension ? $extension::searchResultBlock() : null,
'url' => $extension ? $extension::urlFromIndexData( $this->indexData, $this->itemData ) : null,
'searchResultClassName' => $extension ? $extension::$searchResultClassName : ''
);
}
/**
* Add to RSS feed
*
* @param \IPS\Xml\Rss $document Document to add to
* @return string
* @param Rss $document Document to add to
* @return void
*/
public function addToRssFeed( \IPS\Xml\Rss $document )
public function addToRssFeed( Rss $document ): void
{
try
{
@@ -133,14 +163,14 @@ class _Content extends \IPS\Content\Search\Result
try
{
$enclosure = \IPS\File::get( $key, $data[ $key ] );
$enclosure = File::get( $key, $data[ $key ] );
}
catch( \Exception $ex ) { }
catch( Exception ) { }
}
$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 );
$document->addItem( $object instanceof Comment ? $object->item()->mapped( 'title' ) : $object->mapped( 'title' ), $object->url(), Result::preDisplay( $this->indexData['index_content'] ), DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
}
/* If the search result was orphaned, let us continue */
catch( \OutOfRangeException $e ){}
catch( OutOfRangeException ){}
}
}
+18 -12
View File
@@ -11,42 +11,48 @@
namespace IPS\Content\Search\Result;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use IPS\Content\Search\Result;
use IPS\DateTime;
use IPS\Theme;
use function defined;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Search Result not from Index
*/
class _Custom extends \IPS\Content\Search\Result
class Custom extends Result
{
/**
* @brief HTML
*/
protected $html;
protected string $html;
/**
* @brief Image
*/
protected $image;
protected string|null $image;
/**
* @brief Data used for merging multiple extra items
*/
protected $mergeData;
protected array $mergeData;
/**
* Constructor
*
* @param \IPS\DateTime $time The time for this result
* @param DateTime $time The time for this result
* @param string $html HTML to display
* @param string|NULL $image HTML for image to display
* @param string|NULL $mergeData Data for merging
* @return void
*/
public function __construct( \IPS\DateTime $time, $html, $image = NULL, $mergeData = NULL )
public function __construct( DateTime $time, string $html, string|null $image = NULL, string|null $mergeData = NULL )
{
$this->createdDate = $time;
$this->lastUpdatedDate = $time;
@@ -62,7 +68,7 @@ class _Custom extends \IPS\Content\Search\Result
* @param string|NULL $mergeData Data for merging
* @return void
*/
public function mergeInData( $html, $mergeData = NULL )
public function mergeInData( string $html, string|null $mergeData = NULL ): void
{
$this->html = $html;
$this->mergeData[] = $mergeData;
@@ -73,7 +79,7 @@ class _Custom extends \IPS\Content\Search\Result
*
* @return array
*/
public function getMergeData()
public function getMergeData(): array
{
return $this->mergeData ?: array();
}
@@ -84,8 +90,8 @@ class _Custom extends \IPS\Content\Search\Result
* @param string $view The view to use (expanded or condensed)
* @return string
*/
public function html( $view = 'expanded' )
public function html( string $view = 'expanded' ): string
{
return \IPS\Theme::i()->getTemplate( 'streams', 'core' )->extraItem( $this->createdDate, $this->image, $this->html, $view );
return Theme::i()->getTemplate( 'streams', 'core' )->extraItem( $this->createdDate, $this->image, $this->html, $view );
}
}
+25 -14
View File
@@ -11,16 +11,25 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use IPS\DateTime;
use IPS\Member;
use IPS\Settings;
use IPS\Text\Parser;
use function defined;
use function preg_replace;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Search Result
*/
abstract class _Result
abstract class Result
{
/**
* Pre-Display logic for search index content
@@ -28,7 +37,7 @@ abstract class _Result
* @param string $content The search result content
* @return string
*/
public static function preDisplay( $content )
public static function preDisplay( string $content ): string
{
/* We cannot use images (or *anything* other than text) in search results as it messes with the truncation
logic. All HTML (including emoticons) are already stripped before they go into the search index
@@ -36,23 +45,24 @@ abstract class _Result
with one of the alternative Emoji style images though because that re-introduces images, so we
just strip them out (making them behave like emoticons) */
if ( \IPS\Settings::i()->emoji_style == 'twemoji' )
if ( Settings::i()->emoji_style == 'twemoji' )
{
$content = preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
$content = preg_replace( '/(?:' . Parser::getEmojiRegex() . '|\x{200d})+/u', '', $content );
}
return $content;
/* We need to strip the image labels as returned by the AI image scanner */
return preg_replace('#<i\s+?l="([^<]+?)</i>#', '', $content);
}
/**
* @brief Created Date
*/
public $createdDate;
public DateTime $createdDate;
/**
* @brief Last Updated Date
*/
public $lastUpdatedDate;
public DateTime $lastUpdatedDate;
/**
* Separator for activity streams - "past hour", "today", etc.
@@ -60,13 +70,13 @@ abstract class _Result
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
* @return string
*/
public function streamSeparator( $createdDate=TRUE )
public function streamSeparator( bool $createdDate=TRUE ): string
{
$date = $createdDate ? $this->createdDate : $this->lastUpdatedDate;
$now = \IPS\DateTime::ts( time() );
$now = DateTime::ts( time() );
$yesterday = clone $now;
$yesterday = $yesterday->sub( new \DateInterval('P1D') );
$yesterday = $yesterday->sub( new DateInterval('P1D') );
$diff = $date->diff( $now );
if ( $date->format('Y-m-d') == $yesterday->format('Y-m-d') )
@@ -94,7 +104,7 @@ abstract class _Result
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @param Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse string title Title of search result
* @apiresponse string content Content of search result
@@ -117,8 +127,9 @@ abstract class _Result
* @apiresponse string authorPhotoThumbnail URL to author's profile photo thumbnail
* @apiresponse array tags Array of tags associated with the search result
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
public function apiOutput( Member|null $authorizedMember = NULL ): array
{
/* @note This is only here to populate the AdminCP reference tab - the array of search result entries is built manually */
return [];
}
}
+165 -146
View File
@@ -11,77 +11,110 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use ArrayIterator;
use IPS\Application;
use IPS\Content\Item;
use IPS\Content\Reaction;
use IPS\Content\Review;
use IPS\DateTime;
use IPS\Db;
use IPS\Db\Select;
use IPS\IPS;
use IPS\Log;
use IPS\Member;
use IPS\Member\Club;
use IPS\Node\Model;
use IPS\Request;
use IPS\Settings;
use IPS\Text\Parser;
use IPS\Theme;
use LogicException;
use OutOfRangeException;
use function array_slice;
use function count;
use function defined;
use function get_class;
use function gettype;
use function in_array;
use function is_integer;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Search Results
*/
class _Results extends \ArrayIterator
class Results extends ArrayIterator
{
/**
* @brief Count
*/
protected $countAllRows;
protected mixed $countAllRows = null;
/**
* @brief Has this been initated?
*/
protected $initated = FALSE;
protected bool $initated = FALSE;
/**
* @brief Author data
*/
protected $authorData = array();
protected array $authorData = array();
/**
* @brief Item titles
*/
protected $itemData = array();
protected array $itemData = array();
/**
* @brief Container data
*/
protected $containerData = array();
protected array $containerData = array();
/**
* @brief Reputation data
*/
protected $reputationData = array();
protected array $reputationData = array();
/**
* @brief Reaction Data
*/
protected $reactions = array();
protected array $reactions = array();
/**
* @brief Attached Images
*/
protected $attachedImages = array();
protected array $attachedImages = array();
/**
* @brief Review Ratings
*/
protected $reviewRatings = array();
protected array $reviewRatings = array();
/**
* @brief Index IDs of items I have posted in
*/
protected $iPostedIn = array();
protected array $iPostedIn = array();
/**
* @brief Tag values for index_ids returned from search
*/
protected $tags = array();
protected array $tags = array();
/**
* @brief Prefixes for index_ids returned from search
*/
protected array $prefixes = array();
/**
* Constructor
*
* @param array $results The results
* @param \IPS\Db\Select|int $countAllRows Count for all rows query or int
* @param Select|int $countAllRows Count for all rows query or int
* @return void
*/
public function __construct( $results, $countAllRows )
@@ -95,7 +128,7 @@ class _Results extends \ArrayIterator
*
* @return void
*/
public function init()
public function init(): void
{
/* Init */
$membersToLoad = array();
@@ -104,7 +137,6 @@ class _Results extends \ArrayIterator
$reputationIds = array();
$reviewRatings = array();
$itemIndexIds = array();
$indexIds = array();
$attachedImages = array();
/* Loop */
@@ -115,7 +147,7 @@ class _Results extends \ArrayIterator
$membersToLoad[ $result['index_author'] ] = $result['index_author'];
}
if ( \in_array( 'IPS\Content\Comment', class_parents( $result['index_class'] ) ) )
if ( in_array( 'IPS\Content\Comment', class_parents( $result['index_class'] ) ) )
{
$commentClass = $result['index_class'];
$itemsToLoad[ $commentClass::$itemClass ][ $result['index_item_id'] ] = $result['index_item_id'];
@@ -128,33 +160,23 @@ class _Results extends \ArrayIterator
if ( $result['index_container_id'] )
{
$class = $result['index_class'];
if ( $class == 'IPS\core\Statuses\Status' )
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
if ( isset( $itemClass::$containerNodeClass ) )
{
if ( $result['index_container_id'] != $result['index_author'] )
$containerClass = $itemClass::$containerNodeClass;
if ( isset( $containerClass::$seoTitleColumn ) )
{
$membersToLoad[ $result['index_container_id'] ] = $result['index_container_id'];
}
}
else
{
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
if ( isset( $containerClass::$seoTitleColumn ) )
{
$containersToLoad[ $itemClass::$containerNodeClass ][ $result['index_container_id'] ] = $result['index_container_id'];
}
$containersToLoad[ $itemClass::$containerNodeClass ][ $result['index_container_id'] ] = $result['index_container_id'];
}
}
}
if ( \IPS\IPS::classUsesTrait( $result['index_class'], 'IPS\Content\Reactable' ) )
if ( IPS::classUsesTrait( $result['index_class'], 'IPS\Content\Reactable' ) )
{
$reputationIds[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
}
if ( \in_array( 'IPS\Content\Review', class_parents( $result['index_class'] ) ) )
if ( in_array( 'IPS\Content\Review', class_parents( $result['index_class'] ) ) )
{
$reviewRatings[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
}
@@ -175,7 +197,9 @@ class _Results extends \ArrayIterator
/* Load item data */
foreach ( $itemsToLoad as $itemClass => $itemIds )
{
foreach ( \IPS\Db::i()->select( $itemClass::basicDataColumns(), $itemClass::$databaseTable, \IPS\Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnId, $itemIds ) )->setKeyField( $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) as $itemId => $itemData )
/* @var Item $itemClass */
/* @var array $databaseColumnMap */
foreach ( Db::i()->select( $itemClass::basicDataColumns(), $itemClass::$databaseTable, Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnId, $itemIds ) )->setKeyField( $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) as $itemId => $itemData )
{
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
@@ -196,33 +220,34 @@ class _Results extends \ArrayIterator
}
/* Load author data */
if( \count( $membersToLoad ) )
if( count( $membersToLoad ) )
{
$this->authorData = iterator_to_array( \IPS\Db::i()->select( \IPS\Member::columnsForPhoto(), 'core_members', \IPS\Db::i()->in( 'member_id', $membersToLoad ) )->setKeyField( 'member_id' ) );
$this->authorData = iterator_to_array( Db::i()->select( Member::columnsForPhoto(), 'core_members', Db::i()->in( 'member_id', $membersToLoad ) )->setKeyField( 'member_id' ) );
}
/* Load container data */
foreach ( $containersToLoad as $containerClass => $containerIds )
{
$this->containerData[ $containerClass ] = iterator_to_array( \IPS\Db::i()->select( $containerClass::basicDataColumns(), $containerClass::$databaseTable, \IPS\Db::i()->in( $containerClass::$databasePrefix . $containerClass::$databaseColumnId, $containerIds ) )->setKeyField( $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) );
/* @var string|Model $containerClass */
$this->containerData[ $containerClass ] = iterator_to_array( Db::i()->select( $containerClass::basicDataColumns(), $containerClass::$databaseTable, Db::i()->in( $containerClass::$databasePrefix . $containerClass::$databaseColumnId, $containerIds ) )->setKeyField( $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) );
}
/* Load reputation data */
$reputation = array();
if ( \count( $reputationIds ) )
if ( count( $reputationIds ) )
{
$clause = array();
$binds = array();
foreach ( $reputationIds as $class => $ids )
{
$clause[] = "( reaction_enabled=? AND app=? AND type=? AND " . \IPS\Db::i()->in( 'type_id', $ids ) . " )";
/* @var Item $class */
$clause[] = "( reaction_enabled=? AND app=? AND type=? AND " . Db::i()->in( 'type_id', $ids ) . " )";
$binds[] = 1;
$binds[] = $class::$application;
$binds[] = $class::reactionType();
}
$where = array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) );
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating', 'reaction' ), 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
foreach ( Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating', 'reaction' ), 'core_reputation_index', $where )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['reaction'];
@@ -238,26 +263,23 @@ class _Results extends \ArrayIterator
/* Load rating reviews */
foreach ( $reviewRatings as $reviewClass => $reviewIds )
{
$this->reviewRatings[ $reviewClass ] = iterator_to_array( \IPS\Db::i()->select( array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ), $reviewClass::$databaseTable, \IPS\Db::i()->in( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewIds ) )->setKeyField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId )->setValueField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ) );
/* @var string|Review $reviewClass */
/* @var array $databaseColumnMap */
$this->reviewRatings[ $reviewClass ] = iterator_to_array( Db::i()->select( array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ), $reviewClass::$databaseTable, Db::i()->in( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewIds ) )->setKeyField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId )->setValueField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ) );
}
/* Load data for the "I posted in this" stars */
if ( \count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
if ( count( $itemIndexIds ) and Member::loggedIn()->member_id )
{
$this->iPostedIn = \IPS\Content\Search\Index::i()->iPostedIn( $itemIndexIds, \IPS\Member::loggedIn() );
$this->iPostedIn = Index::i()->iPostedIn( $itemIndexIds, Member::loggedIn() );
}
/* Load tags and prefixes */
if ( \count( $itemIndexIds ) and \IPS\Settings::i()->search_method == 'mysql' and \IPS\Settings::i()->tags_enabled )
if ( count( $itemIndexIds ) and Settings::i()->search_method == 'mysql' and Settings::i()->tags_enabled)
{
$orderBy = '';
if ( \IPS\Settings::i()->tags_alphabetical )
{
$orderBy = ' ORDER BY index_tag ASC';
}
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag' . $orderBy . ') 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') );
$this->tags = iterator_to_array( Db::i()->select( 'index_id, GROUP_CONCAT(index_tag' . $orderBy . ') as index_tags', 'core_search_index_tags', array( array( Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
$this->prefixes = iterator_to_array( Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
}
/* Load attached images */
@@ -266,12 +288,12 @@ class _Results extends \ArrayIterator
$locationKeyMap = [];
foreach ( $attachedImages as $class => $ids )
{
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$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 );
$locationKey = $itemClass::$application . '_' . IPS::mb_ucfirst( $itemClass::$module );
$locationKeyMap[$locationKey] = $class;
$clause[] = "( location_key=? AND id1=? AND id2=?)";
$binds[] = $locationKey;
@@ -281,11 +303,11 @@ class _Results extends \ArrayIterator
}
}
if ( \count( $attachedImages ) and \count( $clause ) )
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' )
$ids = iterator_to_array( Db::i()->select( 'attachment_id', 'core_attachments_map', array( array_merge( array( implode( ' OR ', $clause ) ), $binds ) ) ) );
foreach ( Db::i()->select( '*', 'core_attachments', array( array( 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'] ) )
@@ -294,7 +316,7 @@ class _Results extends \ArrayIterator
'extension' => 'core_Attachment',
'location' => $row['attach_location'],
'thumb_location' => $row['attach_thumb_location'],
'labels' => implode( ', ', \IPS\Text\Parser::getAttachmentLabels( $row ) )
'labels' => implode( ', ', Parser::getAttachmentLabels( $row ) )
];
}
}
@@ -307,24 +329,24 @@ class _Results extends \ArrayIterator
/**
* Get current
*
* @return \IPS\Patterns\ActiveRecord
* @return Result\Content
*/
public function current()
public function current(): Result\Content
{
$data = parent::current();
$class = $data['index_class'];
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$containerClass = isset( $itemClass::$containerNodeClass ) ? $itemClass::$containerNodeClass : NULL;
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$containerClass = $itemClass::$containerNodeClass ?? NULL;
$reputationData = array();
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
if ( IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
{
$reputationData = $this->reputationData[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
}
$reactions = array();
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
if ( IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and isset( $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ] ) )
{
$reactions = $this->reactions[ $class::$application ][ $class::reactionType() ][ $data['index_object_id'] ];
}
@@ -333,10 +355,10 @@ class _Results extends \ArrayIterator
if ( ! isset( $this->itemData[ $itemClass ][ $data['index_item_id'] ] ) )
{
/* This should not happen in production, so debug log */
\IPS\Log::debug( "index_item_id ID " . $data['index_item_id'] . " for " . $itemClass . " has no itemData" , 'searchResults' );
Log::debug( "index_item_id ID " . $data['index_item_id'] . " for " . $itemClass . " has no itemData" , 'searchResults' );
/* Then cleanup/remove the item now */
\IPS\Content\Search\Index::i()->directIndexRemoval( $data['index_class'], $data['index_object_id'] );
Index::i()->directIndexRemoval( $data['index_class'], $data['index_object_id'] );
/* And continue, moving on to the next item */
static::next();
@@ -344,10 +366,6 @@ class _Results extends \ArrayIterator
}
$itemData = $this->itemData[ $itemClass ][ $data['index_item_id'] ];
if ( $class == 'IPS\core\Statuses\Status' and $data['index_container_id'] != $data['index_author'] )
{
$itemData['profile'] = $this->authorData[ $data['index_container_id'] ];
}
if ( isset( $itemClass::$databaseColumnMap['author'] ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
{
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
@@ -357,7 +375,7 @@ class _Results extends \ArrayIterator
}
}
if ( \is_array( $this->attachedImages ) and isset( $this->attachedImages[ $class ][ $data['index_object_id'] ] ) )
if ( isset( $this->attachedImages[ $class ][ $data['index_object_id'] ] ) )
{
$itemData['attachedImages'] = $this->attachedImages[ $class ][ $data['index_object_id'] ];
}
@@ -384,12 +402,12 @@ class _Results extends \ArrayIterator
return new Result\Content(
$data,
isset( $this->authorData[ $data['index_author'] ] ) ? $this->authorData[ $data['index_author'] ] : array( 'member_id' => 0, 'name' => \IPS\Member::loggedIn()->language()->addToStack('guest'), 'members_seo_name' => '', 'pp_main_photo' => NULL, 'pp_photo_type' => 'none' ),
$this->authorData[$data['index_author']] ?? array('member_id' => 0, 'name' => Member::loggedIn()->language()->addToStack('guest'), 'members_seo_name' => '', 'pp_main_photo' => NULL, 'pp_photo_type' => 'none'),
$itemData,
$containerData,
$reputationData,
isset( $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] ) ? $this->reviewRatings[ $data['index_class'] ][ $data['index_object_id'] ] : NULL,
\in_array( $data['index_item_index_id'], $this->iPostedIn ),
$this->reviewRatings[$data['index_class']][$data['index_object_id']] ?? NULL,
in_array( $data['index_item_index_id'], $this->iPostedIn ),
$reactions
);
}
@@ -399,13 +417,14 @@ class _Results extends \ArrayIterator
*
* @return void
*/
public function rewind()
public function rewind(): void
{
if ( !$this->initated )
{
$this->init();
}
return parent::rewind();
parent::rewind();
}
/**
@@ -414,15 +433,15 @@ class _Results extends \ArrayIterator
* @param bool $allRows If TRUE, will get the number of rows ignoring the limit
* @return int
*/
public function count( $allRows = FALSE )
public function count( bool $allRows = FALSE ): int
{
if ( $allRows )
{
if ( \is_integer( $this->countAllRows ) )
if ( is_integer( $this->countAllRows ) )
{
return $this->countAllRows;
}
else if ( \gettype( $this->countAllRows ) === 'object' and \get_class( $this->countAllRows ) === 'IPS\Db\Select' )
else if ( gettype( $this->countAllRows ) === 'object' and get_class( $this->countAllRows ) === 'IPS\Db\Select' )
{
$this->countAllRows = $this->countAllRows->first();
@@ -430,7 +449,7 @@ class _Results extends \ArrayIterator
}
else
{
throw new \LogicException;
throw new LogicException;
}
}
else
@@ -443,13 +462,13 @@ class _Results extends \ArrayIterator
* Add in "extra" items
*
* @param array $extra Types ('register', 'follow_member', 'follow_content', 'photo', 'votes', 'like', 'rep_neg')
* @param \IPS\Member|NULL $author The author to limit extra items to
* @param \IPS\DateTime|NULL $lastTime If provided, only items since this date is included. If NULL, it works out which to include based on what results are being shown
* @param \IPS\DateTime|NULL $firstTime If provided, only items before this date is included. If NULL, it works out which to include based on what results are being shown
* @param Member|NULL $author The author to limit extra items to
* @param DateTime|NULL $lastTime If provided, only items since this date is included. If NULL, it works out which to include based on what results are being shown
* @param DateTime|NULL $firstTime If provided, only items before this date is included. If NULL, it works out which to include based on what results are being shown
* @return array
* @note Each thing is limited to 10 items. Even though this may result in accuracies, the limit is necessary so that if there's more extra items that content it doesn't create lots of queries and slow loading
*/
public function addExtraItems( $extra, \IPS\Member $author = NULL, $lastTime=NULL, $firstTime = NULL )
public function addExtraItems( array $extra, Member|null $author = NULL, DateTime|null $lastTime=NULL, DateTime|null $firstTime = NULL ): array
{
/* Work out the timestamps */
$results = iterator_to_array( $this );
@@ -457,7 +476,7 @@ class _Results extends \ArrayIterator
{
$firstTime = $firstTime->getTimestamp();
}
elseif ( isset( \IPS\Request::i()->page ) and \IPS\Request::i()->page != 1 )
elseif ( isset( Request::i()->page ) and Request::i()->page != 1 )
{
foreach ( $results as $result )
{
@@ -495,7 +514,7 @@ class _Results extends \ArrayIterator
if ( $lastTime )
{
/* Users registered */
if ( \in_array( 'register', $extra ) )
if ( in_array( 'register', $extra ) )
{
$where = array( array( 'joined>?', $lastTime ), array( 'completed=?', true ), array( 'temp_ban!=?', '-1' ) );
if ( $firstTime )
@@ -508,22 +527,22 @@ class _Results extends \ArrayIterator
}
foreach (
\IPS\Db::i()->select( implode( ', ', array_merge( array_map( function( $c ) { return "core_members.{$c}"; }, \IPS\Member::columnsForPhoto() ), array( 'core_members.joined', 'core_validating.new_reg' ) ) ), 'core_members', $where, 'joined DESC', 10 )
Db::i()->select( implode( ', ', array_merge( array_map( function( $c ) { return "core_members.{$c}"; }, Member::columnsForPhoto() ), array( 'core_members.joined', 'core_validating.new_reg' ) ) ), 'core_members', $where, 'joined DESC', 10 )
->join( 'core_validating', 'core_validating.member_id=core_members.member_id' )
as $member ) {
if ( empty( $member['new_reg'] ) )
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $member['joined'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'], $member['member_group_id'] ) ) ),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], \IPS\Member::photoUrl( $member ), 'tiny' )
DateTime::ts( $member['joined'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'], $member['member_group_id'] ) ) ),
Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], Member::photoUrl( $member ), 'fluid' )
);
}
}
}
/* Users changed photo */
if ( \in_array( 'photo', $extra ) )
if ( in_array( 'photo', $extra ) )
{
$where = array( array( 'photo_last_update>? AND photo_last_update>(joined+300)', $lastTime ), array( 'completed=?', true ) );
if ( $firstTime )
@@ -534,27 +553,27 @@ class _Results extends \ArrayIterator
{
$where[] = array( 'member_id=?', $author->member_id );
}
foreach ( \IPS\Db::i()->select( array_merge( \IPS\Member::columnsForPhoto(), array( 'photo_last_update' ) ), 'core_members', $where, 'photo_last_update DESC', 10 ) as $member )
foreach ( Db::i()->select( array_merge( Member::columnsForPhoto(), array( 'photo_last_update' ) ), 'core_members', $where, 'photo_last_update DESC', 10 ) as $member )
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $member['photo_last_update'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_updated_photo', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'], $member['member_group_id'] ) ) ),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], \IPS\Member::photoUrl( $member ), 'tiny' )
DateTime::ts( $member['photo_last_update'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_updated_photo', FALSE, array( 'htmlsprintf' => Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'], $member['member_group_id'] ) ) ),
Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], Member::photoUrl( $member ), 'fluid' )
);
}
}
/* Follow */
if ( \in_array( 'follow_member', $extra ) or \in_array( 'follow_content', $extra ) )
if ( in_array( 'follow_member', $extra ) or in_array( 'follow_content', $extra ) )
{
$where = array( array( 'follow_is_anon=0' ), array( 'follow_added>?', $lastTime ) );
if ( $firstTime )
{
$where[] = array( 'follow_added<?', $firstTime );
}
if ( \in_array( 'follow_member', $extra ) xor \in_array( 'follow_content', $extra ) )
if ( in_array( 'follow_member', $extra ) xor in_array( 'follow_content', $extra ) )
{
if ( \in_array( 'follow_member', $extra ) )
if ( in_array( 'follow_member', $extra ) )
{
$where[] = array( 'follow_app=? AND follow_area=?', 'core', 'member' );
}
@@ -565,11 +584,11 @@ class _Results extends \ArrayIterator
}
if ( $author )
{
if ( \in_array( 'follow_member', $extra ) and \in_array( 'follow_content', $extra ) )
if ( in_array( 'follow_member', $extra ) and in_array( 'follow_content', $extra ) )
{
$where[] = array( '( follow_id IN (?) )', \IPS\Db::i()->select( 'follow_id', 'core_follow', array( '( follow_member_id=? OR ( follow_app=? AND follow_area=? AND follow_rel_id=? ) )', $author->member_id, 'core', 'member', $author->member_id ) ) );
$where[] = array( '( follow_id IN (?) )', Db::i()->select( 'follow_id', 'core_follow', array( '( follow_member_id=? OR ( follow_app=? AND follow_area=? AND follow_rel_id=? ) )', $author->member_id, 'core', 'member', $author->member_id ) ) );
}
elseif ( \in_array( 'follow_member', $extra ) )
elseif ( in_array( 'follow_member', $extra ) )
{
$where[] = array( 'follow_rel_id=?', $author->member_id );
}
@@ -580,23 +599,23 @@ class _Results extends \ArrayIterator
}
/* If an application was not updated or is not installed, do not try to fetch it or we can get a class does not exist fatal error */
$where[] = array( "follow_app IN('" . implode( "','", array_keys( \IPS\Application::enabledApplications() ) ) . "')" );
$where[] = array( "follow_app IN('" . implode( "','", array_keys( Application::enabledApplications() ) ) . "')" );
$lastClass = NULL;
$lastMember = NULL;
foreach ( \IPS\Db::i()->select( array( 'follow_app', 'follow_area', 'follow_rel_id', 'follow_member_id', 'follow_added' ), 'core_follow', $where, 'follow_added DESC', 10 ) as $follow )
foreach ( Db::i()->select( array( 'follow_app', 'follow_area', 'follow_rel_id', 'follow_member_id', 'follow_added' ), 'core_follow', $where, 'follow_added DESC', 10 ) as $follow )
{
if ( $follow['follow_app'] == 'core' and $follow['follow_area'] == 'member' )
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $follow['follow_added'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_rel_id'] ) ) ) ) )
DateTime::ts( $follow['follow_added'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $follow['follow_member_id'] ) ), Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $follow['follow_rel_id'] ) ) ) ) )
);
}
else
{
$class = 'IPS\\' . $follow['follow_app'] . '\\' . mb_ucfirst( $follow['follow_area'] );
$class = 'IPS\\' . $follow['follow_app'] . '\\' . IPS::mb_ucfirst( $follow['follow_area'] );
try
{
@@ -611,7 +630,7 @@ class _Results extends \ArrayIterator
if( $result['indexData']['index_class'] == $class AND $result['indexData']['index_object_id'] == $follow['follow_rel_id'] )
{
$urlToThing = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $class::urlFromIndexData( $result['indexData'], $result['itemData'] ), FALSE, $result['indexData']['index_title'], FALSE );
$urlToThing = Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $result['url'], FALSE, $result['indexData']['index_title'], FALSE );
break;
}
}
@@ -619,31 +638,31 @@ class _Results extends \ArrayIterator
if( $urlToThing === NULL )
{
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
$urlToThing = \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE );
$urlToThing = Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE );
}
/* Do we need to merge with the last result? */
if( $lastClass !== NULL AND $lastClass === $class AND $lastMember === $follow['follow_member_id'] AND \count( $extraItems ) )
if( $lastClass !== NULL AND $lastClass === $class AND $lastMember === $follow['follow_member_id'] AND count( $extraItems ) )
{
$extraItem = array_pop( $extraItems );
$mergeList = array_filter( array_merge( array( $urlToThing ), $extraItem->getMergeData() ) );
$listToDisplay = $mergeList;
if( \count( $mergeList ) > 3 )
if( count( $mergeList ) > 3 )
{
$listToDisplay = \array_slice( $mergeList, 0, 3 );
$listToDisplay[] = \IPS\Member::loggedIn()->language()->addToStack( 'x_others', FALSE, array( 'pluralize' => array( \count( $mergeList ) - 3 ) ) );
$listToDisplay = array_slice( $mergeList, 0, 3 );
$listToDisplay[] = Member::loggedIn()->language()->addToStack( 'x_others', FALSE, array( 'pluralize' => array( count( $mergeList ) - 3 ) ) );
}
$extraItem->mergeInData( \IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Member::loggedIn()->language()->formatList( $listToDisplay ) ) ) ), $urlToThing );
$extraItem->mergeInData( Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $follow['follow_member_id'] ) ), Member::loggedIn()->language()->formatList( $listToDisplay ) ) ) ), $urlToThing );
$extraItems[] = $extraItem;
}
else
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $follow['follow_added'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), $urlToThing ) ) ),
DateTime::ts( $follow['follow_added'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $follow['follow_member_id'] ) ), $urlToThing ) ) ),
NULL,
$urlToThing
);
@@ -651,7 +670,7 @@ class _Results extends \ArrayIterator
}
}
catch ( \OutOfRangeException $e ) { }
catch ( OutOfRangeException ) { }
$lastClass = $class;
$lastMember = $follow['follow_member_id'];
@@ -660,7 +679,7 @@ class _Results extends \ArrayIterator
}
/* Likes */
if ( \IPS\Settings::i()->reputation_enabled and \IPS\Member::loggedIn()->group['gbw_view_reps'] and ( \in_array( 'like', $extra ) or \in_array( 'react', $extra ) ) )
if ( Settings::i()->reputation_enabled and Member::loggedIn()->group['gbw_view_reps'] and ( in_array( 'like', $extra ) or in_array( 'react', $extra ) ) )
{
$where = array( array( 'rep_date>?', $lastTime ) );
$where[] = array( 'reaction_enabled=?', 1 );
@@ -670,23 +689,23 @@ class _Results extends \ArrayIterator
}
/* get only the reputation data for installed applications */
$where[] = \IPS\Db::i()->in( 'app', array_keys( \IPS\Application::applications() ) ) ;
$where[] = Db::i()->in( 'app', array_keys( Application::applications() ) ) ;
if ( $author )
{
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
foreach ( Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_id=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
foreach ( Db::i()->select( '*', 'core_reputation_index', array_merge( $where, array( array( 'member_received=?', $author->member_id ) ) ), 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
}
else
{
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
foreach ( Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
{
$this->addReputationExtraItem( $rep, $extraItems );
}
@@ -694,7 +713,7 @@ class _Results extends \ArrayIterator
}
/* Clubs */
if ( \IPS\Settings::i()->clubs and \in_array( 'clubs', $extra ) )
if ( Settings::i()->clubs and in_array( 'clubs', $extra ) )
{
$where = array( array( 'created>?', $lastTime ) );
if ( $firstTime )
@@ -706,24 +725,24 @@ class _Results extends \ArrayIterator
$where[] = array( 'owner=?', $author->member_id );
}
foreach ( \IPS\Member\Club::clubs( \IPS\Member::loggedIn(), NULL, 'created', FALSE, array(), $where ) as $club )
foreach ( Club::clubs( Member::loggedIn(), NULL, 'created', FALSE, array(), $where ) as $club )
{
if ( $club->owner AND $club->owner->member_id )
{
$extraItems[] = new Result\Custom(
$club->created,
\IPS\Member::loggedIn()->language()->addToStack( 'activity_club', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $club->owner ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $club->url(), FALSE, $club->name, FALSE ) ) ) )
Member::loggedIn()->language()->addToStack( 'activity_club', FALSE, array( 'htmlsprintf' => array( Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( $club->owner ), Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $club->url(), FALSE, $club->name, FALSE ) ) ) )
);
}
}
}
/* Extensions */
foreach ( \IPS\Application::allExtensions( 'core', 'StreamItems', TRUE, 'core' ) as $key => $extension )
foreach ( Application::allExtensions( 'core', 'StreamItems', TRUE, 'core' ) as $key => $extension )
{
$settingKey = "all_activity_" . mb_strtolower( $key );
if( method_exists( $extension, 'extraItems' ) and \IPS\Settings::i()->$settingKey )
if( Settings::i()->$settingKey )
{
$extraItems = array_merge( $extraItems, $extension->extraItems( $author, $lastTime, $firstTime ) );
}
@@ -762,16 +781,16 @@ class _Results extends \ArrayIterator
* @param array $extraItems Array of extra items (passed by reference)
* @return void
*/
protected function addReputationExtraItem( $rep, &$extraItems )
protected function addReputationExtraItem( array $rep, array &$extraItems ): void
{
try
{
$thingBeingRepped = NULL;
foreach ( \IPS\Application::load( $rep['app'] )->extensions( 'core', 'ContentRouter', TRUE, TRUE ) as $ext )
foreach ( Application::load( $rep['app'] )->extensions( 'core', 'ContentRouter', TRUE, TRUE ) as $ext )
{
foreach ( $ext->classes as $class )
{
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and $class::reactionType() == $rep['type'] )
if ( IPS::classUsesTrait( $class, 'IPS\Content\Reactable' ) and $class::reactionType() == $rep['type'] )
{
$thingBeingRepped = $class::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -779,7 +798,7 @@ class _Results extends \ArrayIterator
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $commentClass::reactionType() == $rep['type'] )
if ( IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) and $commentClass::reactionType() == $rep['type'] )
{
$thingBeingRepped = $commentClass::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -788,7 +807,7 @@ class _Results extends \ArrayIterator
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
if ( \IPS\IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) and $reviewClass::reactionType() == $rep['type'] )
if ( IPS::classUsesTrait( $reviewClass, 'IPS\Content\Reactable' ) and $reviewClass::reactionType() == $rep['type'] )
{
$thingBeingRepped = $reviewClass::loadAndCheckPerms( $rep['type_id'] );
break;
@@ -798,35 +817,35 @@ class _Results extends \ArrayIterator
}
if ( !$thingBeingRepped )
{
throw new \OutOfRangeException;
throw new OutOfRangeException;
}
if ( \IPS\Content\Reaction::isLikeMode() )
if ( Reaction::isLikeMode() )
{
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $rep['rep_date'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_liked', FALSE, array( 'htmlsprintf' => array(
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
DateTime::ts( $rep['rep_date'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_liked', FALSE, array( 'htmlsprintf' => array(
Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $rep['member_id'] ) ),
$thingBeingRepped->indefiniteArticle(),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
) ) )
);
}
else
{
$reaction = \IPS\Content\Reaction::load( $rep['reaction'] );
$reaction = Reaction::load( $rep['reaction'] );
$extraItems[] = new Result\Custom(
\IPS\DateTime::ts( $rep['rep_date'] ),
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_reacted', FALSE, array( 'htmlsprintf' => array(
(string) $reaction->_icon,
DateTime::ts( $rep['rep_date'] ),
Member::loggedIn()->language()->addToStack( 'activity_member_reacted', FALSE, array( 'htmlsprintf' => array(
(string) $reaction->_icon->url,
$reaction->_title,
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( Member::load( $rep['member_id'] ) ),
$thingBeingRepped->indefiniteArticle(),
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
) ) )
);
}
}
catch ( \OutOfRangeException $e ) { }
catch ( OutOfRangeException ) { }
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
/**
* @brief SearchContent
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 7/3/2023
*/
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Application;
use IPS\Content;
use function get_class;
use function is_string;
use function in_array;
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
class SearchContent
{
/**
* @var array|null
*/
protected static ?array $_searchableClasses = null;
/**
* Returns all searchable classes
*
* @return array
*/
public static function searchableClasses() : array
{
if( static::$_searchableClasses === null )
{
$classes = array();
foreach( Application::allExtensions( 'core', 'SearchContent', false, null, null, false ) as $extension )
{
$classes = array_merge( $classes, $extension::supportedClasses() );
}
static::$_searchableClasses = $classes;
}
return static::$_searchableClasses;
}
/**
* Determines if the class is searchable
*
* @param string|object $object
* @return bool
*/
public static function isSearchable( string|object $object ) : bool
{
$class = is_string( $object ) ? $object : get_class( $object );
return in_array( $class, static::searchableClasses() );
}
/**
* @var array
*/
protected static array $_extensions = array();
/**
* Returns the extension that handles this class
*
* @param Content|string $object
* @param bool $init
* @return SearchContentAbstract|null
*/
public static function extension( Content|string $object, bool $init = TRUE ) : SearchContentAbstract|null
{
$classname = is_string( $object ) ? $object : get_class( $object );
if( !isset( static::$_extensions[ $classname ] ) )
{
foreach( Application::allExtensions( 'core', 'SearchContent', false, null, null, false ) as $extension )
{
if( in_array( $classname, $extension::supportedClasses() ) )
{
static::$_extensions[ $classname ] = new $extension;
break;
}
}
}
/* Make sure to set the extension with whatever object we are working with */
if( isset( static::$_extensions[ $classname ] ) )
{
if( $object instanceof Content AND $init )
{
static::$_extensions[ $classname ]->setObject( $object );
}
return static::$_extensions[ $classname ];
}
return null;
}
}
@@ -0,0 +1,468 @@
<?php
/**
* @brief ContentIndexExtension
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 6/29/2023
*/
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
use BadMethodCallException;
use IPS\Content;
use IPS\Content\Comment;
use IPS\Content\Item;
use IPS\Content\Reaction;
use IPS\Content\Review;
use IPS\Http\Url;
use IPS\Http\Url\Friendly;
use IPS\IPS;
use IPS\Member;
use IPS\Node\Model;
use IPS\Settings;
use IPS\Theme;
use OutOfRangeException;
if (!defined('\IPS\SUITE_UNIQUE_KEY'))
{
header(( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden');
exit;
}
abstract class SearchContentAbstract
{
/**
* @brief Object being indexed
*/
protected Content|null $object = null;
/**
* @brief Item associated with the content (used for comments/reviews)
*/
protected Item|null $item = null;
/**
* @brief Base item class for the extension [REQUIRED]
*/
protected static ?string $itemClass = null;
/**
* @brief A classname applied to the search result block
*/
public static string $searchResultClassName = '';
/**
* Set the content object that we are indexing
*
* @param Content $object
* @return void
*/
public function setObject( Content $object ) : void
{
$this->object = $object;
$this->item = ( $object instanceof Comment ) ? $object->item() : $object;
}
/**
* Return all searchable classes in your application,
* including comments and/or reviews
*
* @return array
*/
abstract public static function supportedClasses() : array;
/**
* Title for search index
*
* @return string
*/
public function searchIndexTitle(): string
{
return $this->item->mapped( 'title' );
}
/**
* Content for search index
*
* @return string
*/
public function searchIndexContent(): string
{
return $this->object->mapped('content');
}
/**
* Give class a chance to inspect and manipulate search engine filters for streams
*
* @param array $filters Filters to be used for activity stream
* @param Query $query Search query object
* @return void
*/
public static function searchEngineFiltering( array &$filters, Query &$query ): void
{
/* Intentionally left blank but child classes can override */
}
/**
* Get HTML for search result display
*
* @param array $indexData Data from the search index
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
* @param array $reputationData Array of people who have given reputation and the reputation they gave
* @param int|NULL $reviewRating If this is a review, the rating
* @param bool $iPostedIn If the user has posted in the item
* @param string $view 'expanded' or 'condensed'
* @param bool $asItem Displaying results as items?
* @param bool $canIgnoreComments Can ignore comments in the result stream? Activity stream can, but search results cannot.
* @param array|null $template Optional custom template
* @param array $reactions Reaction Data
* @return string
*/
public static function searchResult( array $indexData, array $authorData, array $itemData, array|null $containerData, array $reputationData, int|null $reviewRating, bool $iPostedIn, string $view, bool $asItem, bool $canIgnoreComments=FALSE, array|null $template=null, array $reactions=array() ): string
{
$objectClass = $indexData['index_class'];
/* Item details */
$itemClass = $indexData['index_class'];
if( is_subclass_of( $objectClass, Comment::class ) )
{
$itemClass = $objectClass::$itemClass;
}
if( IPS::classUsesTrait( $itemClass, 'IPS\Content\ReadMarkers' ) )
{
$unread = $itemClass::unreadFromData( NULL, $indexData['index_date_updated'], $indexData['index_date_created'], $indexData['index_item_id'], $indexData['index_container_id'], FALSE );
}
else
{
$unread = NULL;
}
$itemUrl = static::urlFromIndexData( $indexData, $itemData, 'getPrefComment' );
/* Object URL */
if( is_subclass_of( $objectClass, Comment::class ) )
{
if( is_subclass_of( $objectClass, Review::class ) )
{
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findReview', 'review' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsReview', 'review' => $indexData['index_object_id'] ) );
}
else
{
$objectUrl = $itemUrl->setQueryString( array( 'do' => 'findComment', 'comment' => $indexData['index_object_id'] ) );
$showRepUrl = $itemUrl->setQueryString( array( 'do' => 'showReactionsComment', 'comment' => $indexData['index_object_id'] ) );
}
}
else
{
$objectUrl = $itemUrl;
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
}
/* Articles language */
$articles = static::articlesFromIndexData( $objectClass, $containerData );
$summaryLanguage = static::searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData );
/* Container details */
$containerUrl = NULL;
$containerTitle = NULL;
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
$containerTitle = $containerClass::titleFromIndexData( $indexData, $itemData, $containerData );
$containerUrl = $containerClass::urlFromIndexData( $indexData, $itemData, $containerData );
}
/* Reputation - if we are showing the total value, then we need to load them up and total up all the values */
if ( Settings::i()->reaction_count_display == 'count' )
{
$repCount = 0;
foreach( $reputationData AS $memberId => $reactionId )
{
try
{
$repCount += Reaction::load( $reactionId )->value;
}
catch( OutOfRangeException ) {}
}
}
else
{
$repCount = count( $reputationData );
}
/* Snippet */
$snippet = static::searchResultSnippet( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $view );
if ( $template === NULL )
{
$template = array( Theme::i()->getTemplate( 'system', 'core', 'front' ), 'searchResult' );
}
/* Return */
return $template( $indexData, $summaryLanguage, $authorData, $itemData, $unread, $asItem ? $itemUrl : $objectUrl, $itemUrl, $containerUrl, $containerTitle, $repCount, $showRepUrl, $snippet, $iPostedIn, $view, $canIgnoreComments, $reactions, static::$searchResultClassName );
}
/**
* Search Result Block
*
* @return array
*/
public static function searchResultBlock(): array
{
return array( Theme::i()->getTemplate( 'widgets', 'core', 'front' ), 'streamItem' );
}
/**
* Get snippet HTML for search result display
*
* @param array $indexData Data from the search index
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
* @param array $reputationData Array of people who have given reputation and the reputation they gave
* @param int|NULL $reviewRating If this is a review, the rating
* @param string $view 'expanded' or 'condensed'
* @return callable
*/
public static function searchResultSnippet( array $indexData, array $authorData, array $itemData, array|null $containerData, array $reputationData, int|null $reviewRating, string $view ): string
{
return $view == 'expanded' ? Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData, $itemData ) : '';
}
/**
* Return the language string key to use in search results
*
* @note Normally we show "(user) posted a (thing) in (area)" but sometimes this may not be accurate, so this is abstracted to allow
* content classes the ability to override
* @param array $authorData Author data
* @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( array $authorData, array $articles, array $indexData, array $itemData, bool $includeLinks = TRUE ): string
{
if( $includeLinks )
{
$authorTemplate = Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $authorData['member_id'], $authorData['name'], $authorData['members_seo_name'], $authorData['member_group_id'] ?? Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
else
{
$authorTemplate = Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $authorData['name'], $authorData['member_group_id'] ?? Settings::i()->guest_group, NULL, $indexData['index_is_anon'] );
}
if( in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
{
$itemClass = $indexData['index_class']::$itemClass;
if( isset( $itemData['author'] ) )
{
if( $includeLinks )
{
$itemAuthorTemplate = Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $itemData['author']['member_id'], $itemData['author']['name'], $itemData['author']['members_seo_name'], $itemData['author']['member_group_id'] ?? Settings::i()->guest_group );
}
else
{
$itemAuthorTemplate = Theme::i()->getTemplate( 'global', 'core', 'front' )->userNameFromData( $itemData['author']['name'], $itemData['author']['member_group_id'] ?? Settings::i()->guest_group );
}
}
if( in_array( 'IPS\Content\Review', class_parents( $indexData['index_class'] ) ) )
{
if( isset( $itemData['author'] ) AND isset( $itemAuthorTemplate ) )
{
return Member::loggedIn()->language()->addToStack( "user_other_activity_review", FALSE, array( 'sprintf' => array( $articles['definite'] ), 'htmlsprintf' => array( $authorTemplate, $itemAuthorTemplate ) ) );
}
else
{
return Member::loggedIn()->language()->addToStack( "user_own_activity_review", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
}
}
else
{
if( $itemClass::$firstCommentRequired )
{
if( $indexData['index_title'] )
{
return Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
}
else
{
if( isset( $itemData['author'] ) AND isset( $itemAuthorTemplate ) )
{
return Member::loggedIn()->language()->addToStack( "user_other_activity_reply", FALSE, array( 'sprintf' => array( $articles['definite'] ), 'htmlsprintf' => array( $authorTemplate, $itemAuthorTemplate ) ) );
}
else
{
return Member::loggedIn()->language()->addToStack( "user_own_activity_reply", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
}
}
}
else
{
if( isset( $itemData['author'] ) AND isset( $itemAuthorTemplate ) )
{
return Member::loggedIn()->language()->addToStack( "user_other_activity_comment", FALSE, array( 'sprintf' => array( $articles['definite'] ), 'htmlsprintf' => array( $authorTemplate, $itemAuthorTemplate ) ) );
}
else
{
return Member::loggedIn()->language()->addToStack( "user_own_activity_comment", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
}
}
}
}
else
{
if ( isset( $indexData['index_class']::$databaseColumnMap['author'] ) )
{
return Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $articles['indefinite'] ), 'htmlsprintf' => array( $authorTemplate ) ) );
}
else
{
return Member::loggedIn()->language()->addToStack( "generic_activity_item", FALSE, array( 'sprintf' => array( $articles['definite_uc'] ) ) );
}
}
}
/**
* Return the container class to store in the search index
*
* @return Model|NULL
*/
public function searchIndexContainerClass(): Model|NULL
{
try
{
return $this->item->directContainer();
}
catch( BadMethodCallException ){}
return null;
}
/**
* Get container ID for search index
*
* @return int
*/
public function searchIndexContainer(): int
{
if( isset( $this->item::$databaseColumnMap['container'] ) )
{
return $this->item->mapped( 'container' );
}
return 0;
}
/**
* Search Index Permissions
*
* @return string Comma-delimited values or '*'
* @li Number indicates a group
* @li Number prepended by "m" indicates a member
* @li Number prepended by "s" indicates a social group
*/
public function searchIndexPermissions(): string
{
try
{
return $this->item->directContainer()->searchIndexPermissions();
}
catch ( BadMethodCallException )
{
return '*';
}
}
/**
* 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( string $itemClass, ?array $containerData ): array
{
/* @var Content $itemClass */
$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 URL from index data
*
* @param array $indexData Data from the search index
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param string|NULL $action Action
* @return Url
*/
public static function urlFromIndexData( array $indexData, array $itemData, string|null $action = NULL ): Url
{
$objectClass = $indexData['index_class'];
if( is_subclass_of( $objectClass, Comment::class ) )
{
$objectClass = $objectClass::$itemClass;
}
if( $action == 'getPrefComment' )
{
$pref = Member::loggedIn()->linkPref() ?: Settings::i()->link_default;
switch( $pref )
{
case 'unread':
$action = Member::loggedIn()->member_id ? 'getNewComment' : NULL;
break;
case 'last':
$action = 'getLastComment';
break;
default:
$action = NULL;
break;
}
}
elseif( !Member::loggedIn()->member_id AND $action == 'getNewComment' )
{
$action = NULL;
}
/* @var array $databaseColumnMap */
$url = Url::internal( $objectClass::$urlBase . $indexData['index_item_id'], 'front', $objectClass::$urlTemplate, Friendly::seoTitle( $indexData['index_title'] ?: $itemData[ $objectClass::$databasePrefix . $objectClass::$databaseColumnMap['title'] ] ) );
if( $action )
{
$url = $url->setQueryString( 'do', $action );
}
return $url;
}
/**
* Get title from index data
*
* @param array $indexData Data from the search index
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
* @return string
*/
public static function titleFromIndexData( array $indexData, array $itemData, array|null $containerData ): string
{
return Member::loggedIn()->language()->addToStack( $indexData['index_container_class']::$titleLangPrefix . $indexData['index_container_id'] );
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @brief SearchIndexAbstract
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @subpackage
* @since 11/21/2023
*/
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Content;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
abstract class SearchIndexAbstract
{
/**
* Alter index_content before it is stored
*
* @param Content $object The class object (eg: \IPS\forums\Topic\Post)
* @param string $content The HTML stripped content
* @return string
*/
abstract public function content( Content $object, string $content ): string;
}