Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+34 -10
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -30,7 +30,7 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
public function getOutput()
{
/* Get the query object from the stored select */
$queryObject = $this->select[0];
$queryObject = $this->iterator[0];
/* Set pagination */
$queryObject->setLimit( $this->resultsPerPage );
@@ -38,20 +38,44 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
/* Run query */
$searchResults = $queryObject->search(
$this->select[2],
$this->select[3],
$this->select[1] + \IPS\Content\Search\Query::TAGS_MATCH_ITEMS_ONLY,
$this->select[4]
$this->iterator[2],
$this->iterator[3],
$this->iterator[1] + \IPS\Content\Search\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 ) )
{
$memberPermissions = $this->authorizedMember;
if( !$this->authorizedMember AND isset( \IPS\Request::i()->search_as ) )
{
$memberPermissions = \IPS\Member::load( \IPS\Request::i()->search_as );
if( !$memberPermissions->member_id )
{
$memberPermissions = NULL;
}
}
\IPS\Db::i()->insert( 'core_statistics', array(
'type' => 'search',
'time' => time(),
'value_4' => \IPS\Request::i()->q,
'value_2' => $this->count,
'extra_data' => $memberPermissions ? md5( $memberPermissions->email . $memberPermissions->joined ) : md5( 'rest-api' ) // Intentionally anonymized
) );
}
$results = array();
foreach ( $searchResults as $result )
{
$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;
@@ -78,7 +102,7 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
}
else
{
$repCount = count( $resultData['reputationData'] );
$repCount = \count( $resultData['reputationData'] );
}
$commentCount = NULL;
@@ -99,9 +123,9 @@ class _ApiResponse extends \IPS\Api\PaginatedResponse
{
$reviewCount = $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_reviews'] ];
}
$results[] = array(
'title' => $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['title'] ],
'title' => ( isset( $itemClass::$databaseColumnMap['title'] ) and isset( $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['title'] ] ) ) ? $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['title'] ] : $resultData['indexData']['index_title'],
'content' => $resultData['indexData']['index_content'],
'class' => $indexClass,
'objectId' => $resultData['indexData']['index_object_id'],
+19 -13
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -37,6 +37,12 @@ class _ContentFilter
* @note NULL means not to filter by container classes
*/
public $containerClasses = NULL;
/**
* @brief Classes to include when filtering by container classes
* @note NULL means no special overrides
*/
public $containerClassExclusions = NULL;
/**
* @brief Container ID filter
@@ -100,7 +106,7 @@ class _ContentFilter
* @param bool $includeItems Include items in results?
* @param bool $includeComments Include comments in results?
* @param bool $includeReviews Include reviews in results?
* @return \IPS\Content\Search\ContentFilter
* @return static
*/
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
{
@@ -130,7 +136,7 @@ class _ContentFilter
* init() adds in review classes, comment classes, etc. But if we want to filter based on just the comment class, we need this method
*
* @param string $class The class
* @return \IPS\Content\Search\ContentFilter
* @return static
*/
public static function initWithSpecificClass( $class )
{
@@ -144,7 +150,7 @@ class _ContentFilter
* Only include results in containers
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function onlyInContainers( array $ids )
{
@@ -158,7 +164,7 @@ class _ContentFilter
* Exclude results in containers
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function excludeInContainers( array $ids )
{
@@ -172,7 +178,7 @@ class _ContentFilter
* Only include results in items
*
* @param array $ids Acceptable item IDs
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function onlyInItems( array $ids )
{
@@ -186,7 +192,7 @@ class _ContentFilter
* Only include results with specific IDs
*
* @param array $ids Acceptable object IDs
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function onlyInIds( array $ids )
{
@@ -200,7 +206,7 @@ class _ContentFilter
* Exclude results in items
*
* @param array $ids Acceptable container IDs
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function excludeInItems( array $ids )
{
@@ -214,7 +220,7 @@ class _ContentFilter
* Set minimum number of comments
*
* @param int $minimumComments The minimum number of comments
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function minimumComments( $minimumComments )
{
@@ -227,7 +233,7 @@ class _ContentFilter
* Set minimum number of reviews
*
* @param int $minimumReviews The minimum number of reviews
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function minimumReviews( $minimumReviews )
{
@@ -240,7 +246,7 @@ class _ContentFilter
* Set minimum number of views
*
* @param int $minimumViews The minimum number of views
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function minimumViews( $minimumViews )
{
@@ -252,7 +258,7 @@ class _ContentFilter
/**
* Only include results which are the first comment on an item requiring a comment
*
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function onlyFirstComment()
{
@@ -264,7 +270,7 @@ class _ContentFilter
/**
* Only include results which are the last comment on an item requiring a comment
*
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
* @return static (for daisy chaining)
*/
public function onlyLastComment()
{
@@ -0,0 +1,25 @@
<?php
/**
* @brief Elasticsearch Search Exception Class
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 26 Nov 2019
*/
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* Elasticsearch Search Exception Class
*/
class _Exception extends \DomainException
{
}
+243 -182
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,6 +22,12 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _Index extends \IPS\Content\Search\Index
{
/**
* @brief Elasticsearch version requirements
*/
const MINIMUM_VERSION = '6.0.0';
const UNSUPPORTED_VERSION = '8.0.0';
/**
* @brief The server URL
*/
@@ -44,14 +50,23 @@ class _Index extends \IPS\Content\Search\Index
* @return void
*/
public function init()
{
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
$settings = array(
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
);
if ( $analyzer === 'custom' )
{
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $settings['analysis']['analyzer'] );
}
$this->url->request()->delete();
$this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( array(
'settings' => array(
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
),
$definition = array(
'settings' => $settings,
'mappings' => array(
'content' => array(
'_doc' => array(
'properties' => array(
'index_class' => array( 'type' => 'keyword' ),
'index_object_id' => array( 'type' => 'long' ),
@@ -60,11 +75,11 @@ class _Index extends \IPS\Content\Search\Index
'index_container_id' => array( 'type' => 'long' ),
'index_title' => array(
'type' => 'text',
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
'analyzer' => $analyzer,
),
'index_content' => array(
'type' => 'text',
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
'analyzer' => $analyzer,
),
'index_permissions' => array( 'type' => 'keyword' ),
'index_date_created' => array(
@@ -88,14 +103,27 @@ class _Index extends \IPS\Content\Search\Index
'index_is_last_comment' => array( 'type' => 'boolean' ),
'index_club_id' => array( 'type' => 'long' ),
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
'index_views' => array( 'type' => 'long' ),
'index_comments' => array( 'type' => 'long' ),
'index_reviews' => array( 'type' => 'long' ),
'index_participants' => array( 'type' => 'long' ),
)
)
)
) ) );
);
try
{
$response = $this->url->setQueryString( 'include_type_name', 'true' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
if( $response->httpResponseCode != 200 )
{
throw new \RuntimeException;
}
}
catch( \Exception $e )
{
$response = $this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
}
}
/**
@@ -147,15 +175,13 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$class = get_class( $object );
$existingData = NULL;
$existingIndexId = NULL;
$resetLastComment = FALSE;
try
{
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
if ( $r['found'] )
{
$existingData = $r['_source'];
@@ -173,65 +199,22 @@ class _Index extends \IPS\Content\Search\Index
}
else if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
{
$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 )
{
$itemClass = $object::$itemClass;
if ( ! $itemClass::$firstCommentRequired )
{
$classes[] = $itemClass;
}
if ( isset( $itemClass::$reviewClass ) )
{
$classes[] = $itemClass::$reviewClass;
}
}
else if ( $object instanceof \IPS\Content\Item )
{
if ( isset( $class::$commentClass ) )
{
$classes[] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$classes[] = $class::$reviewClass;
}
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_is_last_comment = false;",
'lang' => 'painless'
),
'query' => array(
'bool' => array(
'must' => array(
array(
'terms' => array(
'index_class' => $classes
)
),
array(
'term' => array(
'index_item_id' => $indexData['index_item_id']
)
),
)
)
)
) ) );
/* We have a new "last comment" */
$resetLastComment = TRUE;
}
/* Insert into index */
$r = $this->url
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) )
->request( \IPS\LONG_REQUEST_TIMEOUT )
->setHeaders( array( 'Content-Type' => 'application/json' ) )
->put( json_encode( $indexData ) );
if( $error = $this->getResponseError( $r ) )
{
throw new \IPS\Content\Search\Elastic\Exception( $error['type'] . ' ' . $error['type'] );
}
/* Views / Comments / Reviews */
if ( $object instanceof \IPS\Content\Item )
{
@@ -247,6 +230,10 @@ class _Index extends \IPS\Content\Search\Index
{
\IPS\Log::log( $e, 'elasticsearch' );
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
}
if ( $resetLastComment )
{
@@ -256,50 +243,6 @@ class _Index extends \IPS\Content\Search\Index
}
}
/**
* Update view count
*
* @param string $class Class to update
* @param int $id ID of item
* @param int $count Count to update
* @throws \OutOfRangeException When table to update no longer exists
*/
public function updateViewCounts( $class, $id, $count )
{
$classes = array( $class );
if ( isset( $class::$commentClass ) )
{
$classes[] = $class::$commentClass;
}
if ( isset( $class::$reviewClass ) )
{
$classes[] = $class::$reviewClass;
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_views += ". intval( $count ) . ";",
'lang' => 'painless'
),
'query' => array(
'bool' => array(
'must' => array(
array(
'terms' => array(
'index_class' => $classes
)
),
array(
'term' => array(
'index_item_id' => $id
)
),
)
)
)
) ) );
}
/**
* Clear out any tasks associated with the search index method
*
@@ -310,7 +253,14 @@ class _Index extends \IPS\Content\Search\Index
try
{
/* This request *intentionally* goes to _tasks and not (ourpath)/_tasks */
$response = $this->url->setPath( '/_tasks' )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
$response = $this->url->setPath( '/_tasks' )->request( \IPS\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'] );
}
$response = $response->decodeJson();
foreach( $response['nodes'] as $nodeId => $nodeData )
{
@@ -332,6 +282,10 @@ class _Index extends \IPS\Content\Search\Index
}
}
}
catch ( \IPS\Content\Search\Elastic\Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch_response_error' );
}
catch( \Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
@@ -362,7 +316,6 @@ class _Index extends \IPS\Content\Search\Index
$participants = array_values( array_unique( $participants ) );
return array(
'index_views' => $item->mapped('views'),
'index_comments' => $item->mapped('num_comments'),
'index_reviews' => $item->mapped('num_reviews'),
'index_participants' => $participants
@@ -379,7 +332,7 @@ class _Index extends \IPS\Content\Search\Index
protected function rebuildMetaCounts( $item, $lastCommentIndexId = NULL )
{
$databaseColumnId = $item::$databaseColumnId;
$class = get_class( $item );
$class = \get_class( $item );
$classes = array( $class );
if ( isset( $class::$commentClass ) )
{
@@ -392,30 +345,36 @@ class _Index extends \IPS\Content\Search\Index
try
{
$updates = array();
$updates = array();
$params = array();
foreach ( $this->metaCounts( $item ) as $k => $v )
{
if ( is_array( $v ) )
if ( \is_array( $v ) )
{
$updates[] = "ctx._source.{$k} = [" . implode( ',', array_map( 'intval', $v ) ) . "];";
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params[ 'param_' . $k ] = array_map( 'intval', $v );
}
elseif ( is_null( $v ) )
elseif ( \is_null( $v ) )
{
$updates[] = "ctx._source.{$k} = null;";
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = null;
}
else
{
$updates[] = "ctx._source.{$k} = " . intval( $v ) . ';';
$updates[] = "ctx._source.{$k} = params.param_{$k};";
$params['param_' . $k ] = \intval( $v );
}
}
if ( !is_null( $lastCommentIndexId ) )
if ( !\is_null( $lastCommentIndexId ) )
{
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == '{$lastCommentIndexId}' );";
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == params.param_lastcommentid );";
$params['param_lastcommentid'] = $lastCommentIndexId;
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request( \IPS\LONG_REQUEST_TIMEOUT )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => implode( ' ', $updates ),
'lang' => 'painless'
'source' => implode( ' ', $updates ),
'lang' => 'painless',
'params' => $params
),
'query' => array(
'bool' => array(
@@ -450,7 +409,7 @@ class _Index extends \IPS\Content\Search\Index
public function getIndexId( \IPS\Content\Searchable $object )
{
$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;
}
/**
@@ -463,16 +422,57 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )->request()->delete();
$class = \get_class( $object );
$idColumn = $class::$databaseColumnId;
$this->directIndexRemoval( $class, $object->$idColumn );
if ( !( $object instanceof \IPS\Content\Item ) )
{
$this->rebuildMetaCounts( $object->item() );
$itemClass = \get_class( $object->item() );
$itemIdColumn = $itemClass::$databaseColumnId;
$classes = array( $itemClass );
if ( isset( $itemClass::$commentClass ) )
{
$classes[] = $itemClass::$commentClass;
}
if ( isset( $itemClass::$reviewClass ) )
{
$classes[] = $itemClass::$reviewClass;
}
$this->resetLastComment( $classes, $object->item()->$itemIdColumn, $object->$idColumn );
}
}
catch ( \IPS\Http\Request\Exception $e )
{
\IPS\Log::log( $e, 'elasticsearch' );
}
}
/**
* Direct removal from the search index - only used when we don't need to perform ancillary cleanup (i.e. orphaned data)
*
* @param string $class Class
* @param int $id ID
* @return void
*/
public function directIndexRemoval( $class, $id )
{
try
{
$indexId = \strtolower( str_replace( '\\', '_', \substr( $class, 4 ) ) ) . '-' . $id;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $indexId )->request()->delete();
if ( $object instanceof \IPS\Content\Item )
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
{
$class = get_class( $object );
$idColumn = $class::$databaseColumnId;
if ( isset( $class::$commentClass ) )
{
$commentClass = $class::$commentClass;
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -483,7 +483,7 @@ class _Index extends \IPS\Content\Search\Index
),
array(
'term' => array(
'index_item_id' => $object->$idColumn
'index_item_id' => $id
)
),
)
@@ -495,7 +495,7 @@ class _Index extends \IPS\Content\Search\Index
if ( isset( $class::$reviewClass ) )
{
$reviewClass = $class::$reviewClass;
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'bool' => array(
'must' => array(
@@ -506,7 +506,7 @@ class _Index extends \IPS\Content\Search\Index
),
array(
'term' => array(
'index_item_id' => $object->$idColumn
'index_item_id' => $id
)
),
)
@@ -516,10 +516,6 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
else
{
$this->rebuildMetaCounts( $object->item() );
}
}
catch ( \IPS\Http\Request\Exception $e )
{
@@ -571,13 +567,13 @@ class _Index extends \IPS\Content\Search\Index
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => $query
) ) );
}
else
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'term' => array(
'index_class' => $class
@@ -642,42 +638,50 @@ class _Index extends \IPS\Content\Search\Index
);
}
$updates = array();
$updates = array();
$params = array();
if ( $newPermissions !== NULL )
{
$updates[] = "ctx._source.index_permissions = " . json_encode( explode( ',', $newPermissions ) ) . ";";
$updates[] = "ctx._source.index_permissions = params.params_indexpermissions;";
$params['params_indexpermissions'] = explode( ',', $newPermissions );
}
if ( $newContainer )
{
$updates[] = "ctx._source.index_container_id = " . intval( $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() )
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() )
{
try
{
$updates[] = "ctx._source.index_club_id = " . intval( $containerClass::load( $newContainer )->$clubIdColumn ) . ";";
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = \intval( $containerClass::load( $newContainer )->$clubIdColumn );
}
catch ( \OutOfRangeException $e )
{
$updates[] = "ctx._source.index_club_id = null;";
$updates[] = "ctx._source.index_club_id = params.params_indexclub;";
$params['params_indexclub'] = null;
}
}
}
if ( $newItemId )
{
$updates[] = "ctx._source.index_item_id = " . intval( $newItemId ) . ";";
$updates[] = "ctx._source.index_item_id = params.params_indexitem;";
$params['params_indexitem'] = \intval( $newItemId );
}
if ( $newItemAuthorId )
{
$updates[] = "ctx._source.index_item_author = " . intval( $newItemAuthorId ) . ";";
$updates[] = "ctx._source.index_item_author = params.params_indexauthor;";
$params['params_indexauthor'] = \intval( $newItemAuthorId );
}
if ( count( $updates ) )
if ( \count( $updates ) )
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => implode( ' ', $updates ),
'lang' => 'painless'
'source' => implode( ' ', $updates ),
'lang' => 'painless',
'params' => $params
),
'query' => array(
'bool' => $conditions
@@ -694,9 +698,9 @@ class _Index extends \IPS\Content\Search\Index
)
);
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_permissions.add( 'm.' + ctx._source.index_author );",
'source' => "ctx._source.index_permissions.add( 'm' + ctx._source.index_author );",
'lang' => 'painless'
),
'query' => array(
@@ -724,10 +728,11 @@ class _Index extends \IPS\Content\Search\Index
);
}
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_hidden = " . intval( $newHiddenStatus ) . ";",
'lang' => 'painless'
'source' => "ctx._source.index_hidden = params.newHiddenStatus;",
'lang' => 'painless',
'params' => array( 'newHiddenStatus' => \intval( $newHiddenStatus ) )
),
'query' => array(
'bool' => $conditions
@@ -749,11 +754,11 @@ class _Index extends \IPS\Content\Search\Index
*/
public static function convertConditionsToQuery( $conditions )
{
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 );
}
@@ -790,7 +795,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => $update
) ) );
@@ -822,7 +827,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'query' => array(
'range' => array(
'index_date_updated' => array(
@@ -848,16 +853,17 @@ 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
* @return void
*/
public function resetLastComment( $classes, $indexItemId )
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL )
{
try
{
/* Remove the flag */
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_is_last_comment = false;",
'source' => "ctx._source.index_is_last_comment = false;",
'lang' => 'painless'
),
'query' => array(
@@ -873,6 +879,11 @@ class _Index extends \IPS\Content\Search\Index
'index_item_id' => $indexItemId
)
),
array(
'term' => array(
'index_is_last_comment' => true
)
)
)
)
)
@@ -882,7 +893,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;
@@ -898,10 +909,19 @@ class _Index extends \IPS\Content\Search\Index
{
$item = $itemClass::load( $indexItemId );
if ( $lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE ) )
$where = NULL;
if( $ignoreId !== NULL )
{
$commentClass = $itemClass::$commentClass;
$commentIdColumn = $commentClass::$databaseColumnId;
$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 )
{
/* Set that it is the latest comment */
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $lastComment ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $lastComment ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'doc' => array(
'index_is_last_comment' => true
)
@@ -909,10 +929,14 @@ 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 );
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => \IPS\Settings::i()->search_index_maxresults ) )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
'script' => array(
'inline' => "ctx._source.index_date_updated = " . intval( $indexDataForLastComment['index_date_updated'] ) ."; ctx._source.index_date_commented = " . intval( $indexDataForLastComment['index_date_commented'] ) .";",
'lang' => 'painless'
'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'] )
)
),
'query' => array(
'bool' => array(
@@ -938,7 +962,7 @@ class _Index extends \IPS\Content\Search\Index
) ) );
}
}
catch ( \OutOfRangeException $e ) { }
catch ( \OutOfRangeException $e ) {}
}
}
catch ( \Exception $e )
@@ -977,22 +1001,23 @@ class _Index extends \IPS\Content\Search\Index
);
/* Get the count */
$count = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$count = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 0,
'query' => $query
) ) )->decodeJson();
if ( !$count['hits']['total'] )
$total = $count['hits']['total']['value'] ?? $count['hits']['total'];
if ( !$total )
{
return array();
}
/* Now get the unique item ids */
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'aggs' => array(
'itemIds' => array(
'terms' => array(
'field' => 'index_item_index_id',
'size' => $count['hits']['total']
'size' => $total
)
)
),
@@ -1020,15 +1045,16 @@ 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 $itemIndexIds Item index IDs
* @param array $hashes Item index hashes
* @param \IPS\Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
public function hashesWithPermission( array $hashes, \IPS\Member $member )
public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL )
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'query' => array(
'bool' => array(
'filter' => array(
@@ -1049,7 +1075,8 @@ 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 )
@@ -1076,7 +1103,7 @@ class _Index extends \IPS\Content\Search\Index
{
try
{
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_doc/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'size' => 1,
'sort' => array( array( 'index_date_updated' => 'asc' ) )
) ) )->decodeJson();
@@ -1103,16 +1130,23 @@ class _Index extends \IPS\Content\Search\Index
*/
public function stemmedTerms( $terms )
{
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
if ( $analyzer === 'custom' )
{
$analysisSettings = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
$analyzer = key( $analysisSettings['analyzer'] );
}
try
{
$results = $this->url->setPath( '/_analyze' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
'analyzer' => $analyzer,
'text' => implode( ' ', $terms )
) ) )->decodeJson();
if ( isset( $results['tokens'] ) )
{
$stemmed = array();
$stemmed = $terms;
foreach ( $results['tokens'] as $token )
{
$stemmed[] = $token['token'];
@@ -1127,4 +1161,31 @@ class _Index extends \IPS\Content\Search\Index
return $terms;
}
}
/**
* Supports filtering by views?
*
* @return bool
*/
public function supportViewFiltering()
{
return FALSE;
}
/**
* Check response to see if an error was produced
*
* @param \IPS\Http\Response $response Response object
* @return string|null
*/
protected function getResponseError( \IPS\Http\Response $response )
{
/* Log any errors */
if( $response->httpResponseCode != 200 AND $content = $response->decodeJson() AND isset( $content['error'] ) )
{
return $content['error'];
}
return NULL;
}
}
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,6 +22,9 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class _MassIndexer extends \IPS\Content\Search\Elastic\Index
{
/**
* @brief Data to store in bulk
*/
protected $data = array();
/**
@@ -46,7 +49,7 @@ class _MassIndexer extends \IPS\Content\Search\Elastic\Index
$this->data[] = array(
'index' => array(
'_index' => trim( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/' ),
'_type' => 'content',
'_type' => '_doc',
'_id' => $this->getIndexId( $object ),
)
);
+93 -34
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Elastic;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -174,8 +174,8 @@ class _Query extends \IPS\Content\Search\Query
}
}
/* Minimum views / comments / reviews */
foreach ( array( 'minimumViews' => 'index_views', 'minimumComments' => 'index_comments', 'minimumReviews' => 'index_reviews' ) as $filterKey => $indexKey )
/* Minimum comments / reviews */
foreach ( array( 'minimumComments' => 'index_comments', 'minimumReviews' => 'index_reviews' ) as $filterKey => $indexKey )
{
if ( $filter->$filterKey )
{
@@ -204,11 +204,14 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it together */
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
if( \count( $conditions ) )
{
$contentFilterConditions[] = Index::convertConditionsToQuery( $conditions );
}
}
/* Put them together */
if ( count( $contentFilterConditions ) > 1 )
if ( \count( $contentFilterConditions ) > 1 )
{
$this->filters[] = array(
'bool' => array(
@@ -218,11 +221,17 @@ class _Query extends \IPS\Content\Search\Query
}
elseif ( $type )
{
$this->filters[] = $contentFilterConditions[0];
if( \count( $contentFilterConditions ) )
{
$this->filters[] = $contentFilterConditions[0];
}
}
else
{
$this->mustNot[] = $contentFilterConditions[0];
if( \count( $contentFilterConditions ) )
{
$this->mustNot[] = $contentFilterConditions[0];
}
}
return $this;
@@ -236,7 +245,7 @@ class _Query extends \IPS\Content\Search\Query
*/
public function filterByAuthor( $author )
{
if ( is_array( $author ) )
if ( \is_array( $author ) )
{
$this->filters[] = array( 'terms' => array( 'index_author' => $author ) );
}
@@ -262,7 +271,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 )
@@ -274,6 +283,25 @@ class _Query extends \IPS\Content\Search\Query
'term' => array( 'index_club_id' => $club instanceof \IPS\Member\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 );
}
return $this;
}
@@ -318,7 +346,7 @@ class _Query extends \IPS\Content\Search\Query
{
foreach ( $object->classes as $class )
{
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
@@ -399,7 +427,7 @@ 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 $includeContainers Include content posted by members the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
@@ -477,7 +505,7 @@ class _Query extends \IPS\Content\Search\Query
{
$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'];
}
@@ -537,7 +565,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( count( $conditions ) )
if ( \count( $conditions ) )
{
$this->filters[] = array( 'bool' => array( 'should' => $conditions ) );
}
@@ -646,7 +674,7 @@ 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 ) )
if ( \count( $items ) )
{
foreach( $items as $mid => $mtime )
{
@@ -656,10 +684,10 @@ class _Query extends \IPS\Content\Search\Query
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( in_array( $mid, $markers ) )
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 ] );
@@ -704,7 +732,7 @@ class _Query extends \IPS\Content\Search\Query
}
$notIn = array();
if ( count( $markers ) )
if ( \count( $markers ) )
{
$useIds = array_flip( $markers );
@@ -713,7 +741,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 )
{
@@ -726,7 +754,7 @@ 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 )
{
@@ -735,7 +763,7 @@ class _Query extends \IPS\Content\Search\Query
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
{
/* Still read */
$notIn[] = intval( $row['_id'] );
$notIn[] = \intval( $row['_id'] );
}
}
}
@@ -747,7 +775,7 @@ class _Query extends \IPS\Content\Search\Query
'filter' => $classConditions
)
);
if ( count( $notIn ) )
if ( \count( $notIn ) )
{
$_condition['bool']['must_not'] = array(
'terms' => array(
@@ -760,7 +788,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( count( $conditions ) )
if ( \count( $conditions ) )
{
$this->filters[] = array(
'bool' => array(
@@ -907,7 +935,7 @@ class _Query extends \IPS\Content\Search\Query
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
{
/* 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( ( $this->offset + $this->resultsToGet ) > \IPS\Settings::i()->search_index_maxresults )
{
return new \IPS\Content\Search\Results( array(), 0 );
}
@@ -920,11 +948,12 @@ class _Query extends \IPS\Content\Search\Query
if ( $term !== NULL or $tags !== NULL )
{
$searchConditions = array();
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
$titleField = \IPS\Settings::i()->search_title_boost ? ( 'index_title^' . \intval( \IPS\Settings::i()->search_title_boost ) ) : 'index_title';
/* Build the condition for the search term */
if ( $term !== NULL )
{
/* If term is in "quotes" handle it as a phrase */
if ( static::termIsPhrase( $term ) )
{
$term = trim( $term, '"' );
@@ -937,6 +966,30 @@ class _Query extends \IPS\Content\Search\Query
$searchConditions[] = array( 'multi_match' => array( 'query' => $term, 'fields' => array( 'index_content', $titleField ), 'type' => 'phrase' ) );
}
}
/* If term contains * wildcard, but doesn't start with it (from elasticsearch docs: "In order to prevent extremely slow wildcard queries, a wildcard term should not start with one of the wildcards * or ?"), handle it as a wildcard search - NOTE this will not use the analyzer */
elseif ( preg_match( '/^[^\s\*].*\*.*$/', $term ) )
{
if ( $method & static::TERM_TITLES_ONLY )
{
$searchConditions[] = array( 'wildcard' => array( 'index_title' => array( 'value' => $term ) ) );
}
else
{
$searchConditions[] = array(
'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 ) ) )
),
array(
'wildcard' => array( 'index_content' => array( 'value' => $term ) )
)
)
)
);
}
}
/* Otherwise just do it as a match search */
else
{
if ( $method & static::TERM_TITLES_ONLY )
@@ -988,7 +1041,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 ) );
}
@@ -1018,8 +1071,9 @@ class _Query extends \IPS\Content\Search\Query
'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( \IPS\Settings::i()->search_decay_days ) . 'd',
'decay' => number_format( \IPS\Settings::i()->search_decay_factor, 1, '.', '' ),
'origin' => time(),
)
)
)
@@ -1034,8 +1088,12 @@ class _Query extends \IPS\Content\Search\Query
'query' => $query,
'script_score' => array(
'script' => array(
'inline' => "doc['index_author'].value == " . intval( $this->member->member_id ) . " ? ( _score * " . number_format( \IPS\Settings::i()->search_elastic_self_boost, 1, '.', '' ) . " ) : _score",
'lang' => 'painless'
'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, '.', '' )
)
)
)
)
@@ -1057,7 +1115,7 @@ class _Query extends \IPS\Content\Search\Query
{
return new \IPS\Content\Search\Results( array(), 0 );
}
/* Make the call! */
$return = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( $json )->decodeJson();
if ( isset( $return['error'] ) )
@@ -1065,14 +1123,15 @@ class _Query extends \IPS\Content\Search\Query
\IPS\Log::log( print_r( $return['error'], TRUE ), 'elasticsearch' );
return new \IPS\Content\Search\Results( array(), 0 );
}
/* Set results */
/* Set results */
$total = $return['hits']['total']['value'] ?? $return['hits']['total'];
return new \IPS\Content\Search\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'] ), $return['hits']['total'] <= \IPS\Settings::i()->search_index_maxresults ? $return['hits']['total'] : (int) \IPS\Settings::i()->search_index_maxresults );
}, $return['hits']['hits'] ), $total <= \IPS\Settings::i()->search_index_maxresults ? $total : (int) \IPS\Settings::i()->search_index_maxresults );
}
catch ( \Exception $e )
{
+53 -27
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -112,17 +112,30 @@ abstract class _Index extends \IPS\Patterns\Singleton
*/
public function indexSingleItem( \IPS\Content\Searchable $object )
{
/* It is possible for some items to not have a valid URL */
try
{
if( !$url = (string) $object->url() )
{
throw new \LogicException;
}
}
catch( \LogicException $e )
{
$url = '';
}
try
{
$idColumn = $object::$databaseColumnId;
if ( isset( $object::$commentClass ) AND is_subclass_of( $object::$commentClass, 'IPS\Content\Searchable' ) )
{
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$commentClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => (string) $object->url() ), 5, TRUE );
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$commentClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ), 5, TRUE );
}
if ( isset( $object::$reviewClass ) AND is_subclass_of( $object::$reviewClass, 'IPS\Content\Searchable' ) )
{
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$reviewClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => (string) $object->url() ), 5, TRUE );
\IPS\Task::queue( 'core', 'IndexSingleItem', array( 'class' => $object::$reviewClass, 'id' => $object->$idColumn, 'title' => $object->mapped('title'), 'url' => $url ), 5, TRUE );
}
}
catch( \OutOfRangeException $e ) {}
@@ -137,7 +150,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
public function indexData( \IPS\Content\Searchable $object )
{
/* Init */
$class = get_class( $object );
$class = \get_class( $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;
@@ -277,7 +290,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
}
/* 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();
@@ -289,6 +302,10 @@ abstract class _Index extends \IPS\Patterns\Singleton
{
$hiddenStatus = 0;
}
if ( $hiddenStatus === -3 )
{
return NULL;
}
/* Get the item index ID */
$itemIndexId = NULL;
@@ -363,25 +380,35 @@ abstract class _Index extends \IPS\Patterns\Singleton
$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;
}
/* Return */
return array(
'index_class' => $class,
'index_object_id' => $object->$idColumn,
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? ( $object->searchIndexContainerClass() ? get_class( $object->searchIndexContainerClass() ) : NULL ) : get_class( $object->item()->searchIndexContainerClass() ),
'index_container_class' => $containerClass,
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
'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_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 \IPS\Content\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 )
@@ -397,19 +424,6 @@ abstract class _Index extends \IPS\Patterns\Singleton
*/
abstract public function index( \IPS\Content\Searchable $object );
/**
* Update view count
*
* @param string $class Class to update
* @param int $id ID of item
* @param int $count Count to update
* @throws \OutOfRangeException When table to update no longer exists
*/
public function updateViewCounts( $class, $id, $count )
{
// Do nothing by default
}
/**
* Clear out any tasks associated with the search index method
*
@@ -523,9 +537,10 @@ abstract class _Index extends \IPS\Patterns\Singleton
*
* @param string $class The class
* @param int|NULL $indexItemId The index item ID
* @param int|NULL $ignoreId ID to ignore because it is being removed
* @return void
*/
abstract public function resetLastComment( $class, $indexItemId );
abstract public function resetLastComment( $class, $indexItemId, $ignoreId = NULL );
/**
* Given a list of item index IDs, return the ones that a given member has participated in
@@ -539,11 +554,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
/**
* 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 Hashes
* @param \IPS\Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
abstract public function hashesWithPermission( array $hashes, \IPS\Member $member );
abstract public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL );
/**
* Get timestamp of oldest thing in index
@@ -562,4 +578,14 @@ abstract class _Index extends \IPS\Patterns\Singleton
{
return $terms;
}
/**
* Supports filtering by views?
*
* @return bool
*/
public function supportViewFiltering()
{
return TRUE;
}
}
+98 -15
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Mysql;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -56,7 +56,7 @@ class _Index extends \IPS\Content\Search\Index
{
/* Get the index data */
$indexData = $this->indexData( $object );
$class = get_class( $object );
$class = \get_class( $object );
/* If we got the data... */
if( $indexData )
@@ -167,7 +167,7 @@ class _Index extends \IPS\Content\Search\Index
}
/* Add add them back if we have any */
if ( count( $tags ) )
if ( \count( $tags ) )
{
foreach( $tags as $tag )
{
@@ -229,7 +229,7 @@ class _Index extends \IPS\Content\Search\Index
public function getIndexId( \IPS\Content\Searchable $object )
{
$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 \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_object_id=?', \get_class( $object ),$object->$databaseColumnId ) )->first();
}
/**
@@ -240,7 +240,7 @@ class _Index extends \IPS\Content\Search\Index
*/
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
{
$class = get_class( $object );
$class = \get_class( $object );
$idColumn = $class::$databaseColumnId;
/* Tags */
@@ -266,7 +266,7 @@ class _Index extends \IPS\Content\Search\Index
try
{
$this->resetLastComment( $classes, $object->mapped('item') );
$this->resetLastComment( $classes, $object->mapped('item'), $object->$idColumn );
}
catch( \Exception $ex ) { }
@@ -300,6 +300,41 @@ class _Index extends \IPS\Content\Search\Index
\IPS\Db::i()->delete( 'core_search_index', array( 'index_class=? AND index_item_id=?', $reviewClass, $object->$idColumn ) );
}
}
/**
* Direct removal from the search index - only used when we don't need to perform ancillary cleanup (i.e. orphaned data)
*
* @param string $class Class
* @param int $id ID
* @return void
*/
public function directIndexRemoval( $class, $id )
{
/* Tags */
$this->_deleteTagsFromIndex( $class, $id );
\IPS\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 ) );
}
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 ) );
}
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 ) );
}
}
/**
* Return the index IDs associated with this class and $id
@@ -319,7 +354,7 @@ class _Index extends \IPS\Content\Search\Index
$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 ) ) );
}
@@ -328,6 +363,52 @@ class _Index extends \IPS\Content\Search\Index
\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 ) ) ) );
}
}
/**
* Removes all content for a specific application from the index (for example, when uninstalling).
*
* @param \IPS\Application $application The application
* @return void
*/
public function removeApplicationContent( \IPS\Application $application )
{
$classes = array();
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $router )
{
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;
}
}
}
}
}
$where = array( \IPS\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 );
}
/**
* Removes all content for a classs
@@ -394,7 +475,7 @@ 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\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
{
try
{
@@ -415,7 +496,7 @@ 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 );
}
@@ -423,7 +504,7 @@ class _Index extends \IPS\Content\Search\Index
{
$addAuthorToPermissionsWhere = $where;
$addAuthorToPermissionsWhere[] = array( 'index_author<>0' );
\IPS\Db::i()->update( 'core_search_index', "index_permissions = CONCAT( index_permissions, ',m.', index_author )", $addAuthorToPermissionsWhere );
\IPS\Db::i()->update( 'core_search_index', "index_permissions = CONCAT( index_permissions, ',m', index_author )", $addAuthorToPermissionsWhere );
}
if ( $newHiddenStatus !== NULL )
@@ -486,8 +567,8 @@ class _Index extends \IPS\Content\Search\Index
{
$prefix = \IPS\Db::i()->prefix;
\IPS\Db::i()->query( "DELETE {$prefix}core_search_index_item_map FROM {$prefix}core_search_index_item_map, {$prefix}core_search_index WHERE {$prefix}core_search_index_item_map.index_item_id={$prefix}core_search_index.index_item_id AND index_date_updated < " . $cutoff->getTimestamp() );
\IPS\Db::i()->query( "DELETE {$prefix}core_search_index_tags FROM {$prefix}core_search_index_tags, {$prefix}core_search_index WHERE {$prefix}core_search_index_tags.index_id={$prefix}core_search_index.index_id AND index_date_updated < " . $cutoff->getTimestamp() );
\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() ) );
}
else
@@ -503,9 +584,10 @@ 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
* @return void
*/
public function resetLastComment( $classes, $indexItemId )
public function resetLastComment( $classes, $indexItemId, $ignoreId = NULL )
{
\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=?', $indexItemId ) );
@@ -537,14 +619,15 @@ class _Index extends \IPS\Content\Search\Index
*
* @param array $itemIndexIds Item index IDs
* @param \IPS\Member $member The member
* @param int|NULL $limit Number of results to return
* @return array
*/
public function hashesWithPermission( array $hashes, \IPS\Member $member )
public function hashesWithPermission( array $hashes, \IPS\Member $member, $limit = NULL )
{
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 ) )
) )->setKeyField('index_class_type_id_hash') );
), NULL, $limit )->setKeyField('index_class_type_id_hash') );
}
/**
+91 -68
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Mysql;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -72,7 +72,13 @@ class _Query extends \IPS\Content\Search\Query
* @see filterByItemsIPostedIn()
*/
protected $filterByItemsIPostedIn = FALSE;
/**
* @brief Filter by unread items?
* @see filterByUnread()
*/
protected $filterByUnread = FALSE;
/**
* Filter by multiple content types
*
@@ -100,7 +106,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Set the class */
if ( count( $filter->classes ) > 1 )
if ( \count( $filter->classes ) > 1 )
{
$clause[] = \IPS\Db::i()->in( 'index_class', $filter->classes );
}
@@ -134,7 +140,14 @@ class _Query extends \IPS\Content\Search\Query
/* Are we excluding certain container classes? */
if( $filter->containerClasses !== NULL )
{
$clause[] = \IPS\Db::i()->in( 'index_container_class', $filter->containerClasses );
if( $filter->containerClassExclusions !== NULL )
{
$clause[] = '(' . \IPS\Db::i()->in( 'index_container_class', $filter->containerClasses ) . ' OR ' . \IPS\Db::i()->in( 'index_class', $filter->containerClassExclusions ) . ')';
}
else
{
$clause[] = \IPS\Db::i()->in( 'index_container_class', $filter->containerClasses );
}
}
/* Set the item IDs */
@@ -157,19 +170,19 @@ class _Query extends \IPS\Content\Search\Query
if ( $filter->minimumComments )
{
$this->select[ $class::$databaseTable . '_comments' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_comments'];
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_comments'] . '>=' . intval( $filter->minimumComments );
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_comments'] . '>=' . \intval( $filter->minimumComments );
}
if ( $filter->minimumReviews )
{
$this->select[ $class::$databaseTable . '_reviews' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'];
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . '>=' . intval( $filter->minimumReviews );
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . '>=' . \intval( $filter->minimumReviews );
}
if ( $filter->minimumViews )
{
$this->select[ $class::$databaseTable . '_views' ] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['views'];
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['views'] . '>=' . intval( $filter->minimumViews );
$clause[] = $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['views'] . '>=' . \intval( $filter->minimumViews );
}
}
@@ -186,7 +199,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it together */
if ( count( $clause ) > 1 )
if ( \count( $clause ) > 1 )
{
$filters[] = '( ' . implode( ' AND ', $clause ) . ' )';
}
@@ -211,7 +224,7 @@ class _Query extends \IPS\Content\Search\Query
*/
public function filterByAuthor( $author )
{
if ( is_array( $author ) )
if ( \is_array( $author ) )
{
$this->where[] = array( \IPS\Db::i()->in( 'index_author', $author ) );
}
@@ -236,7 +249,7 @@ class _Query extends \IPS\Content\Search\Query
{
$this->where[] = 'index_club_id IS NULL';
}
if ( is_array( $club ) )
if ( \is_array( $club ) )
{
$this->where[] = array( \IPS\Db::i()->in( 'index_club_id', $club ) );
}
@@ -244,6 +257,26 @@ class _Query extends \IPS\Content\Search\Query
{
$this->where[] = array( 'index_club_id=?', $club instanceof \IPS\Member\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 );
}
return $this;
}
@@ -261,14 +294,7 @@ class _Query extends \IPS\Content\Search\Query
if ( $count )
{
if ( $count > 500 )
{
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
}
else
{
$this->where[] = array( '( index_author=? OR (' . \IPS\Db::i()->in( 'index_id', iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_container_id=?', 'IPS\core\Statuses\Status', $member->member_id ) ) ) ) . ') )', $member->member_id );
}
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
$filterResult = $this;
}
@@ -282,7 +308,7 @@ class _Query extends \IPS\Content\Search\Query
{
foreach ( $object->classes as $class )
{
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
if ( \in_array( 'IPS\Content\Item', class_parents( $class ) ) )
{
$classesChecked[] = $class;
}
@@ -428,7 +454,7 @@ class _Query extends \IPS\Content\Search\Query
{
$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'];
}
@@ -463,13 +489,13 @@ class _Query extends \IPS\Content\Search\Query
{
$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', array( 0, 501 ) ) );
if ( count( $followed ) == 501 )
if ( \count( $followed ) == 501 )
{
/* Assume we have loads of matches, so do a full query */
$where[] = 'index_author IN(?)';
$params[] = \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 ) );
}
else if ( count( $followed ) )
else if ( \count( $followed ) )
{
/* IN is not a SIN. It's been a long day */
$where[] = \IPS\Db::i()->in( 'index_author', $followed );
@@ -488,7 +514,7 @@ class _Query extends \IPS\Content\Search\Query
}
/* Put it all together */
if ( count( $where ) )
if ( \count( $where ) )
{
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
}
@@ -570,11 +596,13 @@ class _Query extends \IPS\Content\Search\Query
}
}
if ( count( $where ) )
if ( \count( $where ) )
{
/* Put it all together */
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
}
$this->filterByUnread = TRUE;
}
/**
@@ -595,7 +623,7 @@ class _Query extends \IPS\Content\Search\Query
$where = array();
$unreadWheres = array();
$containerIds = array();
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
$containerClass = isset( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
foreach( $resetTimes as $containerId => $timestamp )
{
@@ -615,7 +643,7 @@ class _Query extends \IPS\Content\Search\Query
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
if ( count( $items ) )
if ( \count( $items ) )
{
foreach( $items as $mid => $mtime )
{
@@ -625,10 +653,10 @@ class _Query extends \IPS\Content\Search\Query
in it's old location, with the previously 'read' time. In this circumstance, we need
to only use more recent read time, otherwise the topic may be incorrectly included
in the results */
if ( in_array( $mid, $markers ) )
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 ] );
@@ -646,25 +674,25 @@ class _Query extends \IPS\Content\Search\Query
}
}
if( count( $containerIds ) )
if( \count( $containerIds ) )
{
$unreadWheres[] = "( index_date_updated > " . intval( $this->member->marked_site_read ) . " AND ( index_container_id NOT IN(" . implode( ',', $containerIds ) . ") ) )";
$unreadWheres[] = "( index_date_updated > " . \intval( $this->member->marked_site_read ) . " AND ( index_container_id NOT IN(" . implode( ',', $containerIds ) . ") ) )";
}
else
{
$unreadWheres[] = "( index_date_updated > " . intval( $this->member->marked_site_read ) . ")";
$unreadWheres[] = "( index_date_updated > " . \intval( $this->member->marked_site_read ) . ")";
}
if( count( $unreadWheres ) )
if( \count( $unreadWheres ) )
{
$where[] = array( "(" . implode( " OR ", $unreadWheres ) . ")" );
}
if ( count( $markers ) )
if ( \count( $markers ) )
{
/* Avoid packet issues */
krsort( $markers );
$useIds = array_flip( array_slice( $markers, 0, 500, TRUE ) );
$useIds = array_flip( \array_slice( $markers, 0, 500, TRUE ) );
$select = '';
$from = '';
$notIn = array();
@@ -675,7 +703,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 )
{
@@ -688,7 +716,7 @@ 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 )
{
@@ -697,12 +725,12 @@ class _Query extends \IPS\Content\Search\Query
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
{
/* Still read */
$notIn[] = intval( $row['_id'] );
$notIn[] = \intval( $row['_id'] );
}
}
}
if ( count( $notIn ) )
if ( \count( $notIn ) )
{
$where[] = array( "( index_item_id NOT IN (" . implode( ',', $notIn ) . ") )" );
}
@@ -759,11 +787,11 @@ class _Query extends \IPS\Content\Search\Query
*/
public function setHiddenFilter( $statuses )
{
if ( is_null( $statuses ) )
if ( \is_null( $statuses ) )
{
$this->hiddenClause = NULL;
}
if ( is_array( $statuses ) )
if ( \is_array( $statuses ) )
{
$this->hiddenClause = array( \IPS\Db::i()->in( 'index_hidden', $statuses ) );
}
@@ -856,7 +884,7 @@ class _Query extends \IPS\Content\Search\Query
$tagIds = iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ), 'index_id asc', array( 0, 501 ) ) );
/* Now, if we have 501 results, then we have to assume there are more, so the join is required */
if ( count( $tagIds ) == 501 )
if ( \count( $tagIds ) == 501 )
{
$tagWhere = array( $itemsOnlyTagSearch . 'index_item_index_id IN ( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index_tags', array( \IPS\Db::i()->in( 'index_tag', $tags ) ) ) );
}
@@ -937,16 +965,16 @@ class _Query extends \IPS\Content\Search\Query
}
}
$results = iterator_to_array( \IPS\Db::i()->select( 'index_item_id, index_class', array( 'core_search_index_item_map', 'sub' ), array( array( 'index_author_id=' . intval( $this->member->member_id ) . ' AND ' . \IPS\Db::i()->in( 'index_class', $classes ) ) ), 'index_item_id desc', array( 0, 501 ) ) );
$results = iterator_to_array( \IPS\Db::i()->select( 'index_item_id, index_class', array( 'core_search_index_item_map', 'sub' ), array( array( 'index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . \IPS\Db::i()->in( 'index_class', $classes ) ) ), 'index_item_id desc', array( 0, 501 ) ) );
if ( count( $results ) == 501 )
if ( \count( $results ) == 501 )
{
/* Assume we have loads of matches, so do a full query */
$subClause = array( array( 'sub.index_item_id=main.index_item_id AND index_author_id=' . intval( $this->member->member_id ) . ' AND ' . $this->makeEmbeddedIfClauseForSearchMapTable('sub.index_class') ) );
$subClause = array( array( 'sub.index_item_id=main.index_item_id AND index_author_id=' . \intval( $this->member->member_id ) . ' AND ' . $this->makeEmbeddedIfClauseForSearchMapTable('sub.index_class') ) );
$query = \IPS\Db::i()->select( 'index_item_id', array( 'core_search_index_item_map', 'sub' ), $subClause );
$where[] = array( 'EXISTS(?)', $query );
}
else if ( count( $results ) )
else if ( \count( $results ) )
{
$classIds = array();
foreach( $results as $result )
@@ -1027,7 +1055,7 @@ class _Query extends \IPS\Content\Search\Query
}
}
if ( count( $stack ) )
if ( \count( $stack ) )
{
$if = '';
foreach( $stack as $commentClass => $class )
@@ -1035,7 +1063,7 @@ class _Query extends \IPS\Content\Search\Query
$if .= " IF( main.index_class='" . \IPS\Db::i()->escape_string( $commentClass ) . "', '" . \IPS\Db::i()->escape_string( $class ) . "', ";
}
$if .= 'main.index_class' . str_repeat( ' )', count( $stack ) );
$if .= 'main.index_class' . str_repeat( ' )', \count( $stack ) );
}
return $column . '=' . ( $if == '' ? $column : $if );
@@ -1066,25 +1094,14 @@ class _Query extends \IPS\Content\Search\Query
foreach ( static::termAsWordsArray( $term ) as $word )
{
/* Operators are not allowed */
$word = preg_replace( '/^([\+\-~<>]){1,}/', '', $word );
$word = preg_replace( '/([\+\-~\*<>]){2,}$/', '$1', $word );
/* +* and +- are not allowed anywhere in the word */
$word = str_replace( array( '+*', '+-', '+' ), '', $word );
/* Trailing + or -s are not allowed */
$word = rtrim( $word, '+-' );
/* Nor is @ or parenthesis (while paranthesis can be used to group words and apply operators to the group,
(e.g. "+apple +(>turnover <strudel)") - it's unlikely a user intends this behaviour) */
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
$word = preg_replace( '/[+\-><\(\)~*\"@%]+/', '', $word );
if ( $word )
{
$words[] = $word;
}
}
$booleanTerm = implode( ' +', $words );
/* Return */
@@ -1119,7 +1136,7 @@ class _Query extends \IPS\Content\Search\Query
foreach ( static::termAsWordsArray( $term ) as $word )
{
/* Add the default operator */
if ( $defaultOperator and !in_array( mb_substr( $word, 0, 1 ), array( '+', '-', '~', '<', '>' ) ) )
if ( $defaultOperator and !\in_array( mb_substr( $word, 0, 1 ), array( '+', '-', '~', '<', '>' ) ) )
{
/* Clear out leading * symbols so we don't end up with +*a** */
$word = $defaultOperator . ltrim( $word, '*' );
@@ -1179,11 +1196,11 @@ class _Query extends \IPS\Content\Search\Query
if ( $prepared )
{
/* Force newest as of 4.2.6 to test search results */
if ( count( $likeWhere ) and count( $words ) )
if ( \count( $likeWhere ) and \count( $words ) )
{
$return = array( implode( ' AND ', $likeWhere ) . " AND MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
}
else if ( count( $likeWhere ) )
else if ( \count( $likeWhere ) )
{
$return = array( implode( ' AND ', $likeWhere ) );
}
@@ -1195,7 +1212,7 @@ class _Query extends \IPS\Content\Search\Query
else
{
/* Force newest as of 4.2.6 to test search results */
if ( count( $likeWhere ) )
if ( \count( $likeWhere ) )
{
$return[] = implode( ' AND ', $likeWhere );
}
@@ -1231,7 +1248,13 @@ class _Query extends \IPS\Content\Search\Query
/* Set order clause */
$order = $this->order;
/* This forces MySQL to strongly hint a better index to the optimiser for MySQL 5.7+, even though sorting will not be affected */
if ( $this->filterByUnread and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
{
$order .= ", index_date_updated";
}
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
{
@@ -1240,7 +1263,7 @@ class _Query extends \IPS\Content\Search\Query
{
if ( \IPS\Settings::i()->search_title_boost )
{
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . '*' . intval( \IPS\Settings::i()->search_title_boost ) . ')'; // The title score times multiplier
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . '*' . \intval( \IPS\Settings::i()->search_title_boost ) . ')'; // The title score times multiplier
}
else
{
@@ -1394,11 +1417,11 @@ class _Query extends \IPS\Content\Search\Query
{
if ( $row['Variable_name'] === $minVariable )
{
\IPS\Data\Store::i()->mysqlMinWord = intval( $row['Value'] );
\IPS\Data\Store::i()->mysqlMinWord = \intval( $row['Value'] );
}
elseif ( $row['Variable_name'] === $maxVariable )
{
\IPS\Data\Store::i()->mysqlMaxWord = intval( $row['Value'] );
\IPS\Data\Store::i()->mysqlMaxWord = \intval( $row['Value'] );
}
}
}
+5 -6
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -145,8 +145,7 @@ abstract class _Query
* 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.
* @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)
*/
abstract public function filterByContainerClasses( $classes=array(), $exclude=array() );
@@ -156,7 +155,7 @@ 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 $includeContainers Include content posted by members the user follows?
* @param bool $includeMembers Include content posted by members the user follows?
* @return \IPS\Content\Search\Query (for daisy chaining)
*/
abstract public function filterByFollowed( $includeContainers, $includeItems, $includeMembers );
@@ -307,8 +306,8 @@ abstract class _Query
*/
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=NULL, $maxLength=NULL )
{
$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 ) )
+18 -3
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Result;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -90,7 +90,8 @@ class _Content extends \IPS\Content\Search\Result
*/
public function html( $view = 'expanded', $asItem = FALSE, $canIgnoreComments=FALSE, $template=NULL )
{
return call_user_func( array( $this->indexData['index_class'], 'searchResult' ), $this->indexData, $this->authorData, $this->itemData, $this->containerData, $this->reputationData, $this->reviewRating, $this->iPostedIn, $view, $asItem, $canIgnoreComments, $template, $this->reactions );
$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 );
}
/**
@@ -123,7 +124,21 @@ class _Content extends \IPS\Content\Search\Result
{
$class = $this->indexData['index_class'];
$object = $class::load( $this->indexData['index_object_id'] );
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ) );
$enclosure = NULL;
if ( $images = $object->contentImages(1) )
{
$data = array_pop( $images );
$key = key( $data );
try
{
$enclosure = \IPS\File::get( $key, $data[ $key ] );
}
catch( \Exception $ex ) { }
}
$document->addItem( $object instanceof \IPS\Content\Comment ? $object->item()->searchIndexTitle() : $object->searchIndexTitle(), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ), NULL, $enclosure );
}
/* If the search result was orphaned, let us continue */
catch( \OutOfRangeException $e ){}
+3 -2
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search\Result;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -58,7 +58,7 @@ class _Custom extends \IPS\Content\Search\Result
/**
* Merge in new data
*
* @param string $html New HTML to merge in as a list
* @param string $html New HTML to merge in as a list
* @param string|NULL $mergeData Data for merging
* @return void
*/
@@ -81,6 +81,7 @@ class _Custom extends \IPS\Content\Search\Result
/**
* HTML
*
* @param string $view The view to use (expanded or condensed)
* @return string
*/
public function html( $view = 'expanded' )
+7 -3
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -25,7 +25,7 @@ abstract class _Result
/**
* Pre-Display logic for search index content
*
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
* @param string $content The search result content
* @return string
*/
public static function preDisplay( $content )
@@ -35,7 +35,11 @@ abstract class _Result
for this reason, but Emoji (because they're text) won't be. We can't swap the Emoji back out
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' or \IPS\Settings::i()->emoji_style == 'emojione' )
/* Trim content down to a sensible length for display */
$content = mb_substr( $content, 0, 600 );
if ( \IPS\Settings::i()->emoji_style == 'twemoji' )
{
return preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
}
Whitespace-only changes.
+57 -34
View File
@@ -11,7 +11,7 @@
namespace IPS\Content\Search;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -109,7 +109,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'];
@@ -131,7 +131,7 @@ class _Results extends \ArrayIterator
}
else
{
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
if ( isset( $itemClass::$containerNodeClass ) )
{
$containerClass = $itemClass::$containerNodeClass;
@@ -148,7 +148,7 @@ class _Results extends \ArrayIterator
$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'];
}
@@ -188,7 +188,7 @@ 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' ) );
}
@@ -201,7 +201,7 @@ class _Results extends \ArrayIterator
/* Load reputation data */
$reputation = array();
if ( count( $reputationIds ) )
if ( \count( $reputationIds ) )
{
$clause = array();
$binds = array();
@@ -234,13 +234,13 @@ class _Results extends \ArrayIterator
}
/* Load data for the "I posted in this" stars */
if ( count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
if ( \count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
{
$this->iPostedIn = \IPS\Content\Search\Index::i()->iPostedIn( $itemIndexIds, \IPS\Member::loggedIn() );
}
/* Load tags and prefixes */
if ( count( $itemIndexIds ) and \IPS\Settings::i()->search_method == 'mysql' )
if ( \count( $itemIndexIds ) and \IPS\Settings::i()->search_method == 'mysql' )
{
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag) as index_tags', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
$this->prefixes = iterator_to_array( \IPS\Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
@@ -260,7 +260,7 @@ class _Results extends \ArrayIterator
$data = parent::current();
$class = $data['index_class'];
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$itemClass = \in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
$containerClass = isset( $itemClass::$containerNodeClass ) ? $itemClass::$containerNodeClass : NULL;
$reputationData = array();
@@ -278,8 +278,13 @@ class _Results extends \ArrayIterator
/* Ensure we have an item if results are gathered from the map table */
if ( ! isset( $this->itemData[ $itemClass ][ $data['index_item_id'] ] ) )
{
/* This should not happen in production, so debug log and move on to the next item */
/* 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' );
/* Then cleanup/remove the item now */
\IPS\Content\Search\Index::i()->directIndexRemoval( $data['index_class'], $data['index_object_id'] );
/* And continue, moving on to the next item */
static::next();
return static::current();
}
@@ -325,7 +330,7 @@ class _Results extends \ArrayIterator
$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 ),
\in_array( $data['index_item_index_id'], $this->iPostedIn ),
$reactions
);
}
@@ -354,11 +359,11 @@ class _Results extends \ArrayIterator
{
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();
@@ -378,7 +383,7 @@ class _Results extends \ArrayIterator
/**
* Add in "extra" items
*
* @param array $extra Types ('register', 'follow_member', 'follow_content', 'photo', 'like', 'rep_neg')
* @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
@@ -431,9 +436,9 @@ class _Results extends \ArrayIterator
if ( $lastTime )
{
/* Users registered */
if ( in_array( 'register', $extra ) )
if ( \in_array( 'register', $extra ) )
{
$where = array( array( 'joined>?', $lastTime ), array( '( name<>? and email<>? )', '', '' ), array( 'temp_ban!=?', '-1' ) );
$where = array( array( 'joined>?', $lastTime ), array( 'completed=?', true ), array( 'temp_ban!=?', '-1' ) );
if ( $firstTime )
{
$where[] = array( 'joined<?', $firstTime );
@@ -451,7 +456,7 @@ class _Results extends \ArrayIterator
{
$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'] ) ) ),
\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' )
);
}
@@ -459,9 +464,9 @@ class _Results extends \ArrayIterator
}
/* 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( '( name<>? and email<>? )', '', '' ) );
$where = array( array( 'photo_last_update>? AND photo_last_update>(joined+300)', $lastTime ), array( 'completed=?', true ) );
if ( $firstTime )
{
$where[] = array( 'photo_last_update<?', $firstTime );
@@ -474,23 +479,23 @@ class _Results extends \ArrayIterator
{
$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'] ) ) ),
\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' )
);
}
}
/* 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' );
}
@@ -501,11 +506,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_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 (?) )', \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 ) ) );
}
elseif ( in_array( 'follow_member', $extra ) )
elseif ( \in_array( 'follow_member', $extra ) )
{
$where[] = array( 'follow_rel_id=?', $author->member_id );
}
@@ -538,22 +543,40 @@ class _Results extends \ArrayIterator
{
if ( class_exists( $class ) )
{
$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 );
/* It is possible we've already queried and fetched this item, so attempt to pull from our search results first */
$urlToThing = NULL;
foreach( $results as $result )
{
$result = $result->asArray();
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 );
break;
}
}
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 );
}
/* 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( 'and_x_others', FALSE, array( 'pluralize' => array( count( $mergeList ) - 3 ) ) );
$listToDisplay = \array_slice( $mergeList, 0, 3 );
$listToDisplay[] = \IPS\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 );
$extraItems[] = $extraItem;
}
@@ -578,7 +601,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 ( \IPS\Settings::i()->reputation_enabled and \IPS\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 );
@@ -612,7 +635,7 @@ class _Results extends \ArrayIterator
}
/* Clubs */
if ( \IPS\Settings::i()->clubs and in_array( 'clubs', $extra ) )
if ( \IPS\Settings::i()->clubs and \in_array( 'clubs', $extra ) )
{
$where = array( array( 'created>?', $lastTime ) );
if ( $firstTime )