Version 4.1.2
This commit is contained in:
1 parent
3ba79295ee
commit
28bd025b35
1131 files changed
+63658
-20579
No files matched your search
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Content Filter for Search Queries
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 6 Jul 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Filter for Search Queries
|
||||
*/
|
||||
class _ContentFilter
|
||||
{
|
||||
/**
|
||||
* @brief Item class
|
||||
*/
|
||||
public $itemClass;
|
||||
|
||||
/**
|
||||
* @brief Classes to include
|
||||
*/
|
||||
public $classes = array();
|
||||
|
||||
/**
|
||||
* @brief Container ID filter
|
||||
*/
|
||||
public $containerIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Container IDs
|
||||
*/
|
||||
public $containerIds = array();
|
||||
|
||||
/**
|
||||
* @brief Item ID filter
|
||||
*/
|
||||
public $itemIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Item IDs
|
||||
*/
|
||||
public $itemIds = array();
|
||||
|
||||
/**
|
||||
* @brief Minimum comments
|
||||
*/
|
||||
public $minimumComments = 0;
|
||||
|
||||
/**
|
||||
* @brief Minimum reviews
|
||||
*/
|
||||
public $minimumReviews = 0;
|
||||
|
||||
/**
|
||||
* @brief Minimum views
|
||||
*/
|
||||
public $minimumViews = 0;
|
||||
|
||||
/**
|
||||
* @brief Only include results which are the first comment on an item requiring a comment
|
||||
*/
|
||||
public $onlyFirstComment = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Only include results which are the last comment on an item requiring a comment
|
||||
*/
|
||||
public $onlyLastComment = FALSE;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $itemClass The item class
|
||||
* @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\ContainerFilter
|
||||
*/
|
||||
public static function init( $itemClass, $includeItems=TRUE, $includeComments=TRUE, $includeReviews=TRUE )
|
||||
{
|
||||
$obj = new self;
|
||||
$obj->itemClass = $itemClass;
|
||||
|
||||
if ( $includeItems and !$itemClass::$firstCommentRequired )
|
||||
{
|
||||
$obj->classes[] = $itemClass;
|
||||
}
|
||||
|
||||
if ( $includeComments and isset( $itemClass::$commentClass ) )
|
||||
{
|
||||
$obj->classes[] = $itemClass::$commentClass;
|
||||
}
|
||||
|
||||
if ( $includeReviews and isset( $itemClass::$reviewClass ) )
|
||||
{
|
||||
$obj->classes[] = $itemClass::$reviewClass;
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInContainers( array $ids )
|
||||
{
|
||||
$this->containerIdFilter = TRUE;
|
||||
$this->containerIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in containers
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInContainers( array $ids )
|
||||
{
|
||||
$this->containerIdFilter = FALSE;
|
||||
$this->containerIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results in items
|
||||
*
|
||||
* @param array $ids Acceptable item IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInItems( array $ids )
|
||||
{
|
||||
$this->itemIdFilter = TRUE;
|
||||
$this->itemIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in items
|
||||
*
|
||||
* @param array $ids Acceptable container IDs
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function excludeInItems( array $ids )
|
||||
{
|
||||
$this->itemIdFilter = FALSE;
|
||||
$this->itemIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of comments
|
||||
*
|
||||
* @param int $minimumComments The minimum number of comments
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumComments( $minimumComments )
|
||||
{
|
||||
$this->minimumComments = $minimumComments;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of reviews
|
||||
*
|
||||
* @param int $minimumReviews The minimum number of reviews
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumReviews( $minimumReviews )
|
||||
{
|
||||
$this->minimumReviews = $minimumReviews;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum number of views
|
||||
*
|
||||
* @param int $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function minimumViews( $minimumViews )
|
||||
{
|
||||
$this->minimumViews = $minimumViews;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results which are the first comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyFirstComment()
|
||||
{
|
||||
$this->onlyFirstComment = TRUE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results which are the last comment on an item requiring a comment
|
||||
*
|
||||
* @return \IPS\Content\Search\ContainerFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyLastComment()
|
||||
{
|
||||
$this->onlyLastComment = TRUE;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
if( static::$instance === NULL )
|
||||
{
|
||||
$classname = '\\IPS\\Content\\Search\\' . ucfirst( \IPS\Settings::i()->search_engine ) . '\\Index';
|
||||
static::$instance = new $classname;
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
@@ -64,7 +63,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\Searchable' ) )
|
||||
{
|
||||
\IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ) );
|
||||
\IPS\Task::queue( 'core', 'RebuildSearchIndex', array( 'class' => $class ), 5, TRUE );
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $ex ) {}
|
||||
@@ -111,10 +110,37 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Get the last updated date */
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('edit_time');
|
||||
if ( $isForItem )
|
||||
{
|
||||
$dateUpdated = $object->item()->mapped('last_comment');
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('edit_time');
|
||||
}
|
||||
|
||||
/* Take the HTML out of the content */
|
||||
$content = trim( strip_tags( preg_replace( "#<blockquote(.*?)>(.*)</blockquote>#ui", "", preg_replace( "#<script(.*?)>(.*)</script>#ui", "", ' ' . $object->mapped('content') . ' ' ) ) ) );
|
||||
/* Is the the latest content? */
|
||||
$isLastComment = 0;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $object->item();
|
||||
$latestThing = 0;
|
||||
foreach ( array( 'updated', 'last_comment', 'last_review' ) as $k )
|
||||
{
|
||||
if ( isset( $item::$databaseColumnMap[ $k ] ) and ( $item->mapped( $k ) < time() AND $item->mapped( $k ) > $latestThing ) )
|
||||
{
|
||||
$latestThing = $item->mapped( $k );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $object->mapped('date') >= $latestThing )
|
||||
{
|
||||
$isLastComment = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Take the HTML out of the content */
|
||||
$content = trim( strip_tags( preg_replace( "/(<br(?: \/)?>|<\/p>)/i", ' ', preg_replace( "#<blockquote(.*?)>(.*)</blockquote>#uis", "", preg_replace( "#<script(.*?)>(.*)</script>#uis", "", ' ' . $object->mapped('content') . ' ' ) ) ) ) );
|
||||
|
||||
/* Work out the hidden status */
|
||||
$hiddenStatus = $object->hidden();
|
||||
@@ -122,26 +148,47 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
$hiddenStatus = $isForItem ? 1 : 2;
|
||||
}
|
||||
|
||||
if ( $hiddenStatus !== 0 and method_exists( $object, 'item' ) and $object->item()->isFutureDate() )
|
||||
{
|
||||
$hiddenStatus = 0;
|
||||
}
|
||||
|
||||
|
||||
/* Get the item index ID */
|
||||
$itemIndexId = NULL;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $object::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired )
|
||||
{
|
||||
try
|
||||
{
|
||||
$itemIndexId = $this->getIndexId( $object::load( $object->item()->mapped('first_comment_id') ) );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemIndexId = $this->getIndexId( $object->item() );
|
||||
}
|
||||
}
|
||||
|
||||
/* 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_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->mapped('container') : (int) $object->item()->mapped('container'),
|
||||
'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' => $object->mapped('date'),
|
||||
'index_date_updated' => $dateUpdated ?: $object->mapped('date'),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ?: $object->mapped('date') ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_hidden' => $hiddenStatus
|
||||
'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_is_last_comment' => $isLastComment
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,6 +200,14 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
*/
|
||||
abstract public function index( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
abstract public function getIndexId( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
@@ -217,9 +272,21 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @param int|NULL $authorId The author ID to update, or NULL
|
||||
* @param int|NULL $newItemId The new item ID (if applicable)
|
||||
* @param int|NULL $$newItemAuthorId The new item author ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
abstract public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL );
|
||||
abstract public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL );
|
||||
|
||||
/**
|
||||
* Update data for the first and last comment after a merge
|
||||
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return void
|
||||
*/
|
||||
abstract public function rebuildAfterMerge( \IPS\Content\Item $item );
|
||||
|
||||
/**
|
||||
* Prune search index
|
||||
|
||||
@@ -31,20 +31,83 @@ class _Index extends \IPS\Content\Search\Index
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
/* Get the index data */
|
||||
$indexData = $this->indexData( $object );
|
||||
|
||||
/* If we got the data... */
|
||||
if( $indexData )
|
||||
{
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
/* Otherwise, go ahead... */
|
||||
else
|
||||
{
|
||||
\IPS\Db::i()->replace( 'core_search_index', $indexData );
|
||||
/* Index it */
|
||||
$newIndexId = \IPS\Db::i()->replace( 'core_search_index', $indexData, TRUE );
|
||||
|
||||
/* If that was successful... */
|
||||
if ( $newIndexId )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
|
||||
/* Set index_item_index_id on other index items */
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$subClasses = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$subClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$subClasses[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( array( \IPS\Db::i()->in( 'index_class', $subClasses ) ), array( 'index_item_id=?', $object->$databaseColumnId ) ) );
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $object::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
|
||||
{
|
||||
$itemColumnId = $class::$databaseColumnMap['item'];
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_item_index_id' => $newIndexId ), array( \IPS\Db::i()->in( 'index_class', array( $class, $class::$itemClass ) ) . ' AND index_item_id=?', $object->$itemColumnId ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* And also set core_follow.follow_index_id */
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$class = $class::$itemClass;
|
||||
}
|
||||
\IPS\Db::i()->update( 'core_follow', array( 'follow_index_id' => $newIndexId ), array( 'follow_app=? AND follow_area=? AND follow_rel_id=?', $object::$application, mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ), $object->$databaseColumnId ) );
|
||||
|
||||
/* If this is the latest comment, unflag what was set before */
|
||||
if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( 'index_class=? AND index_item_id=? AND index_id<>?', $indexData['index_class'], $indexData['index_item_id'], $newIndexId ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
@@ -98,9 +161,12 @@ class _Index extends \IPS\Content\Search\Index
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @param int|NULL $authorId The author ID to update, or NULL
|
||||
* @param int|NULL $newItemId The new item ID (if applicable)
|
||||
* @param int|NULL $newItemAuthorId The new item author ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL )
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL )
|
||||
{
|
||||
$where = array( array( 'index_class=?', $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
@@ -111,12 +177,34 @@ class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
$where[] = array( 'index_item_id=?', $itemId );
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_item_author=?', $authorId );
|
||||
}
|
||||
|
||||
$update = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$update['index_permissions'] = $newPermissions;
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
}
|
||||
if ( $newItemId )
|
||||
{
|
||||
$update['index_item_id'] = $newItemId;
|
||||
}
|
||||
if ( $newItemAuthorId )
|
||||
{
|
||||
$update['index_item_author'] = $newItemAuthorId;
|
||||
}
|
||||
|
||||
if ( count( $update ) )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, $where );
|
||||
}
|
||||
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
@@ -128,14 +216,42 @@ class _Index extends \IPS\Content\Search\Index
|
||||
$where[] = array( 'index_hidden=2' );
|
||||
}
|
||||
|
||||
$update['index_hidden'] = $newHiddenStatus;
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_hidden' => $newHiddenStatus ), $where );
|
||||
}
|
||||
if ( $newContainer )
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data for the first and last comment after a merge
|
||||
* Sets index_is_last_comment on the last comment, and, if this is an item where the first comment is indexed rather than the item, sets index_title and index_tags on the first comment
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return void
|
||||
*/
|
||||
public function rebuildAfterMerge( \IPS\Content\Item $item )
|
||||
{
|
||||
if ( $item::$commentClass )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE );
|
||||
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$update = array( 'index_is_last_comment' => 0 );
|
||||
if ( $item::$firstCommentRequired )
|
||||
{
|
||||
$update['index_title'] = NULL;
|
||||
$update['index_tags'] = NULL;
|
||||
}
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, array( 'index_class=? AND index_item_id=?', $item::$commentClass, $item->$idColumn ) );
|
||||
|
||||
if ( $firstComment )
|
||||
{
|
||||
$this->index( $firstComment );
|
||||
}
|
||||
if ( $lastComment )
|
||||
{
|
||||
$this->index( $lastComment );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->update( 'core_search_index', $update, $where );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,6 +24,11 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
/**
|
||||
* @brief The SELECT clause
|
||||
*/
|
||||
protected $select = array( 'main' => 'main.*' );
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause
|
||||
*/
|
||||
protected $where = array();
|
||||
@@ -49,137 +54,465 @@ class _Query extends \IPS\Content\Search\Query
|
||||
protected $joins = array();
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
* @brief Item classes included
|
||||
*/
|
||||
protected $itemClasses = NULL;
|
||||
|
||||
/**
|
||||
* Filter by multiple content types
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
|
||||
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL )
|
||||
public function filterByContent( array $contentFilters, $type = TRUE )
|
||||
{
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
/* Init */
|
||||
$filters = array();
|
||||
$params = array();
|
||||
if ( $type )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes ) );
|
||||
|
||||
if ( $containers !== NULL )
|
||||
{
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_container_id', $containers ) );
|
||||
$this->itemClasses = array();
|
||||
}
|
||||
|
||||
if ( $minimumComments !== NULL or $minimumReviews !== NULL or $minimumViews !== NULL )
|
||||
/* Loop the filters */
|
||||
foreach ( $contentFilters as $filter )
|
||||
{
|
||||
$this->joins[ $class::$databaseTable ] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=core_search_index.index_item_id' );
|
||||
$clause = array();
|
||||
if ( $type )
|
||||
{
|
||||
$this->itemClasses[] = $filter->itemClass;
|
||||
}
|
||||
|
||||
if ( $minimumComments !== NULL )
|
||||
/* Set the class */
|
||||
if ( count( $filter->classes ) > 1 )
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['num_comments'] . '>=?', $minimumComments );
|
||||
$clause[] = \IPS\Db::i()->in( 'index_class', $filter->classes );
|
||||
}
|
||||
if ( $minimumReviews !== NULL )
|
||||
else
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['num_reviews'] . '>=?', $minimumReviews );
|
||||
$clause[] = 'index_class=?';
|
||||
$params[] = array_pop( $filter->classes );
|
||||
}
|
||||
if ( $minimumViews !== NULL )
|
||||
|
||||
/* Set the containers */
|
||||
if ( $filter->containerIdFilter !== NULL )
|
||||
{
|
||||
$this->where[] = array( $class::$databasePrefix . $class::$databaseColumnMap['views'] . '>=?', $minimumViews );
|
||||
$clause[] = \IPS\Db::i()->in( 'index_container_id', $filter->containerIds, $filter->containerIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Set the item IDs */
|
||||
if ( $filter->itemIdFilter !== NULL )
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_item_id', $filter->itemIds, $filter->itemIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Minimum comments/reviews/views? */
|
||||
if ( $filter->minimumComments or $filter->minimumReviews or $filter->minimumViews )
|
||||
{
|
||||
$class = $filter->itemClass;
|
||||
|
||||
$this->joins[] = array( 'from' => $class::$databaseTable, 'where' => $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . '=main.index_item_id' );
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
/* Only first comment? */
|
||||
if ( $filter->onlyFirstComment )
|
||||
{
|
||||
$clause[] = "index_title IS NOT NULL";
|
||||
}
|
||||
|
||||
/* Only last comment? */
|
||||
if ( $filter->onlyLastComment )
|
||||
{
|
||||
$clause[] = "index_is_last_comment=1";
|
||||
}
|
||||
|
||||
/* Put it together */
|
||||
if ( count( $clause ) > 1 )
|
||||
{
|
||||
$filters[] = '( ' . implode( ' AND ', $clause ) . ' )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$filters[] = array_pop( $clause );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Put it all together */
|
||||
$this->where[] = array_merge( array( $type ? ( '( ' . implode( ' OR ', $filters ) . ' )' ) : ( '!( ' . implode( ' OR ', $filters ) . ' )' ) ), $params );
|
||||
|
||||
/* Return */
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
* Filter by author
|
||||
*
|
||||
* @param array $classes The types of content to exclude (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @param \IPS\Member|int|array $author The author, or an array of author IDs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByExcludeContent( array $classes, $allowedContiners=FALSE )
|
||||
{
|
||||
if ( $allowedContiners )
|
||||
public function filterByAuthor( $author )
|
||||
{
|
||||
if ( is_array( $author ) )
|
||||
{
|
||||
$where = array();
|
||||
$params = array();
|
||||
$excludeAll = array();
|
||||
foreach ( $classes as $class => $allowed )
|
||||
{
|
||||
if ( count( $allowed ) )
|
||||
{
|
||||
$where[] = '( index_class<>? OR ' . \IPS\Db::i()->in( 'index_container_id', $allowed ) . ' )';
|
||||
$params[] = $class;
|
||||
}
|
||||
else
|
||||
{
|
||||
$excludeAll[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$finalWhere = array();
|
||||
if ( count( $excludeAll ) )
|
||||
{
|
||||
$finalWhere[] = \IPS\Db::i()->in( 'index_class', $excludeAll, TRUE );
|
||||
}
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$finalWhere[] = implode( ' OR ', $where );
|
||||
}
|
||||
|
||||
$this->where[] = array_merge( array( implode( ' AND ', $finalWhere ) ), $params );
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_author', $author ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes, TRUE ) );
|
||||
$this->where[] = array( 'index_author=?', $author instanceof \IPS\Member ? $author->member_id : $author );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
* Filter for profile
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @param \IPS\Member $member The member whose profile is being viewed
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItem( \IPS\Content\Item $item )
|
||||
public function filterForProfile( \IPS\Member $member )
|
||||
{
|
||||
$classes = array();
|
||||
if ( isset( $item::$commentClass ) )
|
||||
{
|
||||
$classes[] = $item::$commentClass;
|
||||
}
|
||||
if ( isset( $item::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $item::$reviewClass;
|
||||
}
|
||||
|
||||
$this->where[] = array( \IPS\Db::i()->in( 'index_class', $classes ) );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$this->where[] = array( 'index_item_id=?', $item->$idColumn );
|
||||
|
||||
$this->where[] = array( '( index_author=? OR ( index_class=? AND index_container_id=? ) )', $member->member_id, 'IPS\core\Statuses\Status', $member->member_id );
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
* Filter by item author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByAuthor( \IPS\Member $author )
|
||||
public function filterByItemAuthor( \IPS\Member $author )
|
||||
{
|
||||
$this->where[] = array( 'index_author=?', $author->member_id );
|
||||
$this->where[] = array( 'index_item_author=?', $author->member_id );
|
||||
|
||||
return $this;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
* @param bool $includeContainers Include content in containers the user follows?
|
||||
* @param bool $includeItems Include items and comments/reviews on items the user follows?
|
||||
* @param bool $includeMembers Include content posted by members the user follows?
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByFollowed( $includeContainers, $includeItems, $includeMembers )
|
||||
{
|
||||
$where = array();
|
||||
$params = array();
|
||||
|
||||
/* Are we including items or containers? */
|
||||
if ( $includeContainers or $includeItems )
|
||||
{
|
||||
/* Work out what classes we need to examine */
|
||||
if ( $this->itemClasses !== NULL )
|
||||
{
|
||||
$classes = $this->itemClasses;
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
}
|
||||
}
|
||||
|
||||
/* Loop them */
|
||||
foreach ( $classes as $class )
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\Followable' ) )
|
||||
{
|
||||
/* Construct a subquery which will get the IDs of the items of this class that we follow */
|
||||
$subQueryWhere = array( array( 'follow_app=?', $class::$application ) );
|
||||
if ( $includeContainers and $includeItems )
|
||||
{
|
||||
$subQueryWhere[] = array( '( follow_area=? OR follow_area=? )', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ), mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
|
||||
}
|
||||
elseif ( $includeItems )
|
||||
{
|
||||
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class, mb_strrpos( $class, '\\' ) + 1 ) ) );
|
||||
}
|
||||
elseif ( $includeContainers )
|
||||
{
|
||||
$subQueryWhere[] = array( 'follow_area=?', mb_strtolower( mb_substr( $class::$containerNodeClass, mb_strrpos( $class::$containerNodeClass, '\\' ) + 1 ) ) );
|
||||
}
|
||||
$subQueryWhere[] = array( 'follow_member_id=?', $this->member->member_id );
|
||||
$subQuery = \IPS\Db::i()->select( 'follow_rel_id', 'core_follow', $subQueryWhere );
|
||||
|
||||
/* If we're only doing one class, there's no point in doing "item_class=? AND ( item_class=? AND item_id IN(?) )" - we can just do "item_class=? AND item_id IN(?)" */
|
||||
if ( count( $classes ) == 1 )
|
||||
{
|
||||
$where[] = 'index_item_id IN(?)';
|
||||
$params[] = $subQuery;
|
||||
}
|
||||
/* But otherwise we need to group by class, because item IDs are not unique */
|
||||
else
|
||||
{
|
||||
/* Work out what classes this applies to - need to specify comment and review classes */
|
||||
$_classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$_classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$_classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
/* Constuct the where */
|
||||
$where[] = '( ' . \IPS\Db::i()->in( 'index_class', $_classes ) . ' AND index_item_id IN( ? ) )';
|
||||
$params[] = $subQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Are we including content posted by followed members? */
|
||||
if ( $includeMembers )
|
||||
{
|
||||
$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 ) );
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
|
||||
}
|
||||
|
||||
/* And return */
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content the user has posted in
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItemsIPostedIn()
|
||||
{
|
||||
$this->joins[] = array(
|
||||
'from' => \IPS\Db::i()->select(
|
||||
'index_item_index_id',
|
||||
array('core_search_index', 'sub' ),
|
||||
array( array( 'index_author=' . intval( $this->member->member_id ) ) ),
|
||||
NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT
|
||||
),
|
||||
'where' => 'sub.index_item_index_id=main.index_item_index_id',
|
||||
'type' => 'STRAIGHT_JOIN'
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by content the user has not read
|
||||
*
|
||||
* @note If applicable, it is more efficient to call filterByContent() before calling this method
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByUnread()
|
||||
{
|
||||
/* Work out what classes we need to examine */
|
||||
if ( $this->itemClasses !== NULL )
|
||||
{
|
||||
$classes = $this->itemClasses;
|
||||
}
|
||||
else
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
}
|
||||
}
|
||||
|
||||
/* Loop them */
|
||||
$where = array();
|
||||
$params = array();
|
||||
foreach ( $classes as $class )
|
||||
{
|
||||
if( is_subclass_of( $class, 'IPS\Content\ReadMarkers' ) )
|
||||
{
|
||||
/* Get the actual clause */
|
||||
$unreadWhere = $this->_getUnreadWhere( $class );
|
||||
|
||||
/* Work out what classes this applies to - need to specify comment and review classes */
|
||||
$_classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$_classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$_classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
/* Add it to the array */
|
||||
$clause = array( \IPS\Db::i()->in( 'index_class', $_classes ) );
|
||||
foreach ( $unreadWhere as $_clause )
|
||||
{
|
||||
$clause[] = array_shift( $_clause );
|
||||
$params = array_merge( $params, $_clause );
|
||||
}
|
||||
$where[] = '( ' . implode( ' AND ', $clause ) . ' )';
|
||||
}
|
||||
}
|
||||
|
||||
/* Put it all together */
|
||||
$this->where[] = array_merge( array( '( ' . implode( ' OR ', $where ) . ' )' ), $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'unread' where SQL
|
||||
*
|
||||
* @param string $class Content class (\IPS\forums\Forum)
|
||||
* @return array
|
||||
*/
|
||||
protected function _getUnreadWhere( $class )
|
||||
{
|
||||
$classBits = explode( "\\", $class );
|
||||
$application = $classBits[1];
|
||||
$resetTimes = $this->member->markersResetTimes( NULL );
|
||||
$resetTimes = isset( $resetTimes[ $application ] ) ? $resetTimes[ $application ] : array();
|
||||
$oldestTime = time();
|
||||
$markers = array();
|
||||
$excludeIds = array();
|
||||
$where = array();
|
||||
$unreadWheres = array();
|
||||
$containerIds = array();
|
||||
$containerClass = ( $class::$containerNodeClass ) ? $class::$containerNodeClass : NULL;
|
||||
|
||||
foreach( $resetTimes as $containerId => $timestamp )
|
||||
{
|
||||
/* Pages has different classes per database, but recorded as 'cms' and the container ID in the marking tables */
|
||||
if ( $containerClass and method_exists( $containerClass, 'isValidContainerId' ) )
|
||||
{
|
||||
if ( ! $containerClass::isValidContainerId( $containerId ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$timestamp = $timestamp ?: $this->member->marked_site_read;
|
||||
|
||||
$containerIds[] = $containerId;
|
||||
$unreadWheres[] = '( index_container_id=' . $containerId . ' AND index_date_updated > ' . (int) $timestamp . ')';
|
||||
|
||||
$items = $this->member->markersItems( $application, \IPS\Content\Item::makeMarkerKey( $containerId ) );
|
||||
|
||||
if ( count( $items ) )
|
||||
{
|
||||
foreach( $items as $mid => $mtime )
|
||||
{
|
||||
if ( $mtime > $timestamp )
|
||||
{
|
||||
/* If an item has been moved from one container to another, the user may have a marker
|
||||
in it's old location, with the previously 'read' time. In this circumstance, we need
|
||||
to only use more recent read time, otherwise the topic may be incorrectly included
|
||||
in the results */
|
||||
if ( in_array( $mid, $markers ) )
|
||||
{
|
||||
$_key = array_search( $mid, $markers );
|
||||
$_mtime = intval( mb_substr( $_key, 0, mb_strpos( $_key, '.' ) ) );
|
||||
if ( $_mtime < $mtime )
|
||||
{
|
||||
unset( $markers[ $_key ] );
|
||||
}
|
||||
}
|
||||
|
||||
$markers[ $mtime . '.' . $mid ] = $mid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( count( $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 ) . ")";
|
||||
}
|
||||
|
||||
if( count( $unreadWheres ) )
|
||||
{
|
||||
$where[] = array( "(" . implode( " OR ", $unreadWheres ) . ")" );
|
||||
}
|
||||
|
||||
if ( count( $markers ) )
|
||||
{
|
||||
/* Avoid packet issues */
|
||||
krsort( $markers );
|
||||
$useIds = array_flip( array_slice( $markers, 0, 500, TRUE ) );
|
||||
$select = '';
|
||||
$from = '';
|
||||
$notIn = array();
|
||||
|
||||
/* What is the best date column? */
|
||||
$dateColumns = array();
|
||||
foreach ( array( 'updated', 'last_comment', 'last_review' ) as $k )
|
||||
{
|
||||
if ( isset( $class::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
if ( is_array( $class::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
foreach ( $class::$databaseColumnMap[ $k ] as $v )
|
||||
{
|
||||
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $v . ", 0 )";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateColumns[] = " IFNULL( " . $class::$databaseTable . '.'. $class::$databasePrefix . $class::$databaseColumnMap[ $k ] . ", 0 )";
|
||||
}
|
||||
}
|
||||
}
|
||||
$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 )
|
||||
{
|
||||
if ( isset( $useIds[ $row['_id'] ] ) )
|
||||
{
|
||||
if ( $useIds[ $row['_id'] ] >= $row['_date'] )
|
||||
{
|
||||
/* Still read */
|
||||
$notIn[] = intval( $row['_id'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $notIn ) )
|
||||
{
|
||||
$where[] = array( "( index_item_id NOT IN (" . implode( ',', $notIn ) . ") )" );
|
||||
}
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,13 +605,21 @@ class _Query extends \IPS\Content\Search\Query
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_OLDEST_UPDATED:
|
||||
$this->order = 'index_date_updated ASC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_CREATED:
|
||||
$this->order = 'index_date_created DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_OLDEST_CREATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = NULL;
|
||||
$this->order = 'calcscore DESC';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -295,75 +636,86 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
/* Do we have a term? */
|
||||
$where = array();
|
||||
if ( $term !== NULL )
|
||||
/* Boolean tags */
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
if( mb_strpos( $term, ' ' ) !== FALSE AND mb_strpos( $term, '+' ) === FALSE AND mb_strpos( $term, '-' ) === FALSE AND mb_strpos( $term, '"' ) === FALSE )
|
||||
$theTags = array();
|
||||
foreach( $tags as $tagEntry )
|
||||
{
|
||||
$terms = explode( ' ', $term );
|
||||
$newTerms = array();
|
||||
|
||||
foreach( $terms as $_term )
|
||||
foreach( explode( ' ', $tagEntry ) as $_term )
|
||||
{
|
||||
if( mb_strlen( $_term ) > 2 )
|
||||
{
|
||||
$newTerms[] = "+" . $_term;
|
||||
$theTags[] = "+" . $_term;
|
||||
}
|
||||
else
|
||||
{
|
||||
$newTerms[] = $_term;
|
||||
$theTags[] = $_term;
|
||||
}
|
||||
}
|
||||
|
||||
$term = implode( ' ', $newTerms );
|
||||
|
||||
$searchTags[] = implode( ' ', $theTags );
|
||||
}
|
||||
}
|
||||
|
||||
/* Try to guess what mode will be best for this query. If it looks like a MySQL boolean mode search, use that */
|
||||
if ( mb_substr( $term, 0, 1 ) === '+' or mb_substr( $term, -1, 1 ) === '*' or preg_match( '/^".*"$/', $term ) )
|
||||
/* Do we have a term? */
|
||||
$where = array();
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$indexes = 'index_content,index_title';
|
||||
if ( $method & static::TERM_TITLES_ONLY )
|
||||
{
|
||||
$mode = 'IN BOOLEAN MODE';
|
||||
$indexes = 'index_title';
|
||||
}
|
||||
/* Otherwise, we're just using the default mode - we tried using query expansion for short queries, but it didn't work well (lots of irrelevant results) */
|
||||
else
|
||||
|
||||
/* Default to 'AND' boolean mode search unless asked otherwise*/
|
||||
if ( ! ( $method & static::TERM_OR_MODE ) and ! static::termIsPhrase( $term ) )
|
||||
{
|
||||
$mode = '';
|
||||
$theWords = array();
|
||||
foreach( static::termAsWordsArray( $term ) as $_term )
|
||||
{
|
||||
$potentialOperator = mb_substr( $_term, 0, 1 );
|
||||
|
||||
if ( ! in_array( $potentialOperator, array( '-', '+', '~' ) ) )
|
||||
{
|
||||
if( mb_strlen( $_term ) > 2 )
|
||||
{
|
||||
$theWords[] = "+" . $_term;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$term = implode( ' ', $theWords );
|
||||
}
|
||||
|
||||
/* If we also have tags, create a combined where */
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
if ( $method === static::TERM_OR_TAGS )
|
||||
{
|
||||
$where[] = array( "( MATCH(index_content,index_title) AGAINST (? {$mode}) OR " . \IPS\Db::i()->findInSet( 'index_tags', $tags ) . ' )', $term );
|
||||
if ( $method & static::TERM_OR_TAGS )
|
||||
{
|
||||
$where[] = array( "( MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE) ) OR " . \IPS\Db::i()->findInSet( 'index_tags', $tags ), $term );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( "MATCH(index_content,index_title) AGAINST (? {$mode})", $term );
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'index_tags', $tags ) );
|
||||
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
|
||||
}
|
||||
}
|
||||
/* Or just use the term */
|
||||
else
|
||||
{
|
||||
$where[] = array( "MATCH(index_content,index_title) AGAINST (? {$mode})", $term );
|
||||
$where[] = array( "MATCH({$indexes}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
}
|
||||
/* Or do we have tags? */
|
||||
elseif ( $tags !== NULL )
|
||||
{
|
||||
//$this->where[] = array( \IPS\Db::i()->findInSet( 'index_tags', $tags ) );
|
||||
|
||||
$tagWhere = array();
|
||||
$params = array();
|
||||
|
||||
foreach( $tags as $tag )
|
||||
{
|
||||
$where[] = \IPS\Db::i()->findInSet( 'index_tags', $tags );
|
||||
|
||||
if ( $this->order = 'calcscore DESC' )
|
||||
{
|
||||
array_push( $params, $tag, $tag, $tag, $tag );
|
||||
$tagWhere[] = "( index_tags=? OR index_tags LIKE CONCAT( ?, ',%' ) OR index_tags LIKE CONCAT( '%,', ? ) OR index_tags LIKE CONCAT( '%,', ?, ',%' ) )";
|
||||
$this->setOrder( static::ORDER_NEWEST_UPDATED );
|
||||
}
|
||||
|
||||
$where[] = array_merge( array( '(' . implode( ' OR ', $tagWhere ) . ')' ), $params );
|
||||
}
|
||||
|
||||
/* Only get stuff we have permission for */
|
||||
@@ -373,6 +725,12 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$where[] = $this->hiddenClause;
|
||||
}
|
||||
|
||||
/* MOST PERTINENT search? */
|
||||
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
|
||||
{
|
||||
$this->select['main'] .= ',' . "( ( MATCH (index_title) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) * 5 ) + (MATCH ({$indexes}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) ."' IN BOOLEAN MODE) ) ) / POWER( ( ( UNIX_TIMESTAMP( NOW() ) - index_date_updated ) / 3600 ) + 2, 1.5 ) AS calcscore";
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $where;
|
||||
}
|
||||
@@ -387,13 +745,14 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
$query = \IPS\Db::i()->select( '*', 'core_search_index', array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ), $this->order, array( $this->offset, $this->resultsToGet ), NULL, NULL, \IPS\Db::SELECT_SQL_CALC_FOUND_ROWS );
|
||||
foreach ( $this->joins as $table => $where )
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$query = \IPS\Db::i()->select( implode( ', ', $this->select ), array( 'core_search_index', 'main' ), $where, $this->order, array( $this->offset, $this->resultsToGet ), NULL, NULL );
|
||||
foreach ( $this->joins as $data )
|
||||
{
|
||||
$query->join( $table, $where );
|
||||
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
|
||||
}
|
||||
|
||||
$count = $query->count( TRUE );
|
||||
|
||||
$count = $this->count( $term, $tags, $method );
|
||||
return new \IPS\Content\Search\Results( iterator_to_array( $query ), $count );
|
||||
}
|
||||
|
||||
@@ -407,6 +766,13 @@ class _Query extends \IPS\Content\Search\Query
|
||||
*/
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
return \IPS\Db::i()->select( 'COUNT(*)', 'core_search_index', array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ) )->first();
|
||||
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) ) );
|
||||
|
||||
foreach ( $this->joins as $data )
|
||||
{
|
||||
$query->join( $data['from'], $data['where'], isset( $data['type'] ) ? $data['type'] : 'LEFT' );
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
}
|
||||
}
|
||||
+116
-105
@@ -25,6 +25,8 @@ abstract class _Query
|
||||
{
|
||||
const TERM_OR_TAGS = 1;
|
||||
const TERM_AND_TAGS = 2;
|
||||
const TERM_OR_MODE = 4;
|
||||
const TERM_TITLES_ONLY = 8;
|
||||
|
||||
const HIDDEN_VISIBLE = 0;
|
||||
const HIDDEN_UNAPPROVED = 1;
|
||||
@@ -34,6 +36,8 @@ abstract class _Query
|
||||
const ORDER_NEWEST_UPDATED = 1;
|
||||
const ORDER_NEWEST_CREATED = 2;
|
||||
const ORDER_RELEVANCY = 3;
|
||||
const ORDER_OLDEST_UPDATED = 4;
|
||||
const ORDER_OLDEST_CREATED = 5;
|
||||
|
||||
const SUPPORTS_JOIN_FILTERS = TRUE;
|
||||
|
||||
@@ -45,8 +49,7 @@ abstract class _Query
|
||||
*/
|
||||
public static function init( \IPS\Member $member = NULL )
|
||||
{
|
||||
$class = '\\IPS\\Content\\Search\\' . ucfirst( \IPS\Settings::i()->search_engine ) . '\\Query';
|
||||
return new $class( $member ?: \IPS\Member::loggedIn() );
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +61,7 @@ abstract class _Query
|
||||
* @brief The member performing the search
|
||||
*/
|
||||
protected $member;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -69,129 +72,90 @@ abstract class _Query
|
||||
{
|
||||
$this->member = $member;
|
||||
|
||||
/* Exclude hidden items */
|
||||
if ( !$member->modPermission('can_view_hidden_content') )
|
||||
{
|
||||
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude "first post" content items
|
||||
* Useful for cases such as activity streams where you do not want both the topic and the first post to show
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function excludeFirstPostContentItems()
|
||||
{
|
||||
$classes = array();
|
||||
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) and $class::$firstCommentRequired )
|
||||
{
|
||||
$classes[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $classes ) )
|
||||
/* Exclude disabled applications */
|
||||
$filters = array();
|
||||
foreach ( \IPS\Application::applications() as $application )
|
||||
{
|
||||
return $this->filterByExcludeContent( $classes );
|
||||
if ( !$application->enabled )
|
||||
{
|
||||
foreach ( $application->extensions( 'core', 'ContentRouter' ) as $extension )
|
||||
{
|
||||
foreach ( $extension->classes as $class )
|
||||
{
|
||||
$filters[] = \IPS\Content\Search\ContentFilter::init( $class );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
if ( !empty( $filters ) )
|
||||
{
|
||||
return $this;
|
||||
$this->filterByContent( $filters, FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exclude disabled applications
|
||||
* Filter by multiple content types
|
||||
*
|
||||
* @param array $contentFilters Array of \IPS\Content\Search\ContentFilter objects
|
||||
* @param bool $type TRUE means only include results matching the filters, FALSE means exclude all results matching the filters
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function excludeDisabledApps()
|
||||
{
|
||||
$includeClasses = array();
|
||||
foreach ( \IPS\Content::routedClasses( TRUE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) )
|
||||
{
|
||||
$includeClasses[] = $class;
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$includeClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$includeClasses[] = $class::$reviewClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
$allClasses = array();
|
||||
foreach ( \IPS\Content::routedClasses( FALSE, FALSE, TRUE ) as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Searchable', class_implements( $class ) ) )
|
||||
{
|
||||
$allClasses[] = $class;
|
||||
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$allClasses[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$allClasses[] = $class::$reviewClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$classes = array_diff( $allClasses, $includeClasses );
|
||||
abstract public function filterByContent( array $contentFilters, $type = TRUE );
|
||||
|
||||
if ( !empty( $classes ) )
|
||||
{
|
||||
return $this->filterByExcludeContent( $classes );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL );
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
*
|
||||
* @param string $classes The types of content to search (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByExcludeContent( array $classes, $allowedContiners=FALSE );
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByItem( \IPS\Content\Item $item );
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
*
|
||||
* @param \IPS\Member|int|array $author The author, or an array of author IDs
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByAuthor( $author );
|
||||
|
||||
/**
|
||||
* Filter for profile
|
||||
*
|
||||
* @param \IPS\Member $member The member whose profile is being viewed
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterForProfile( \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Filter by item author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByAuthor( \IPS\Member $author );
|
||||
abstract public function filterByItemAuthor( \IPS\Member $author );
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
* @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?
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByFollowed( $includeContainers, $includeItems, $includeMembers );
|
||||
|
||||
/**
|
||||
* Filter by content the user has posted in
|
||||
*
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByItemsIPostedIn();
|
||||
|
||||
/**
|
||||
* Filter by content the user has not read
|
||||
*
|
||||
* @note If applicable, it is more efficient to call filterByContent() before calling this method
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByUnread();
|
||||
|
||||
/**
|
||||
* Filter by start date
|
||||
@@ -279,4 +243,51 @@ abstract class _Query
|
||||
{
|
||||
return $this->search( $term, $tags, $method )->count( TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this term a phrase?
|
||||
*
|
||||
* @param string $term The term to search for
|
||||
* @return boolean
|
||||
*/
|
||||
public static function termIsPhrase( $term )
|
||||
{
|
||||
return (boolean) preg_match( '#^".*"$#', $term );
|
||||
}
|
||||
|
||||
/**
|
||||
* Term as words array
|
||||
* Returns an array of words. A word is considered as either an entire phrase (eg "sticks stones") when $ignorePhrase is FALSE, or a sequence of characters delimited by a space.
|
||||
* Words smaller than 2 characters are ignored.
|
||||
*
|
||||
* @param string $term The term to search for
|
||||
* @param boolean $ignorePhrase When true, phrases are stripped of quotes and treated as normal words
|
||||
* @param int $minLength The minimum length a sequence of characters has to be before it is considered a word
|
||||
* @return array
|
||||
*/
|
||||
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=3 )
|
||||
{
|
||||
if ( static::termIsPhrase( $term ) )
|
||||
{
|
||||
if ( ! $ignorePhrase )
|
||||
{
|
||||
return array( $term );
|
||||
}
|
||||
else
|
||||
{
|
||||
$term = str_replace( '"', '', $term );
|
||||
}
|
||||
}
|
||||
|
||||
$theWords = array();
|
||||
foreach( explode( ' ', $term ) as $_term )
|
||||
{
|
||||
if ( mb_strlen( $_term ) > $minLength )
|
||||
{
|
||||
$theWords[] = $_term;
|
||||
}
|
||||
}
|
||||
|
||||
return $theWords;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result
|
||||
*/
|
||||
class _Content extends \IPS\Content\Search\Result
|
||||
{
|
||||
/**
|
||||
* Index Data
|
||||
*/
|
||||
protected $indexData;
|
||||
|
||||
/**
|
||||
* Author Data
|
||||
*/
|
||||
protected $authorData;
|
||||
|
||||
/**
|
||||
* Item Data
|
||||
*/
|
||||
protected $itemData;
|
||||
|
||||
/**
|
||||
* Author Data
|
||||
*/
|
||||
protected $containerData;
|
||||
|
||||
/**
|
||||
* Review Rating
|
||||
*/
|
||||
protected $reviewRating;
|
||||
|
||||
/**
|
||||
* If the user has posted in the item
|
||||
*/
|
||||
protected $iPostedIn;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $indexData Data from index
|
||||
* @param array $authorData Basic data about the author. Only includes columns returned by \IPS\Member::columnsForPhoto()
|
||||
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
|
||||
* @param array|NULL $containerData Basic data about the author. Only includes columns returned by container::basicDataColumns()
|
||||
* @param array $reputationData Array of people who have given reputation and the reputation they gave
|
||||
* @param int|NULL $reviewRating If this is a review, the rating
|
||||
* @param bool $iPostedIn If the user has posted in the item
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( array $indexData, array $authorData, array $itemData, array $containerData = NULL, array $reputationData, $reviewRating = NULL, $iPostedIn = FALSE )
|
||||
{
|
||||
$this->createdDate = \IPS\DateTime::ts( $indexData['index_date_created'] );
|
||||
$this->lastUpdatedDate = \IPS\DateTime::ts( $indexData['index_date_updated'] );
|
||||
$this->indexData = $indexData;
|
||||
$this->authorData = $authorData;
|
||||
$this->itemData = $itemData;
|
||||
$this->containerData = $containerData;
|
||||
$this->reputationData = $reputationData;
|
||||
$this->reviewRating = $reviewRating;
|
||||
$this->iPostedIn = $iPostedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML
|
||||
*
|
||||
* @param string $view 'expanded' or 'condensed'
|
||||
* @param bool $asItem Displaying results as items?
|
||||
* @return string
|
||||
*/
|
||||
public function html( $view = 'expanded', $asItem = FALSE )
|
||||
{
|
||||
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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to RSS feed
|
||||
*
|
||||
* @param \IPS\Xml\Rss $document Document to add to
|
||||
* @return string
|
||||
*/
|
||||
public function addToRssFeed( \IPS\Xml\Rss $document )
|
||||
{
|
||||
$class = $this->indexData['index_class'];
|
||||
$object = $class::load( $this->indexData['index_object_id'] );
|
||||
$document->addItem( $object->mapped('title'), $object->url(), $this->indexData['index_content'], \IPS\DateTime::ts( $this->indexData['index_date_created'] ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result not from Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Result;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result not from Index
|
||||
*/
|
||||
class _Custom extends \IPS\Content\Search\Result
|
||||
{
|
||||
/**
|
||||
* @brief HTML
|
||||
*/
|
||||
protected $html;
|
||||
|
||||
/**
|
||||
* @brief Image
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\DateTime $time The time for this result
|
||||
* @param string $html HTML to display
|
||||
* @param string|NULL $image HTML for image to display
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\DateTime $time, $html, $image = NULL )
|
||||
{
|
||||
$this->createdDate = $time;
|
||||
$this->lastUpdatedDate = $time;
|
||||
$this->html = $html;
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html( $view = 'expanded' )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'streams', 'core' )->extraItem( $this->createdDate, $this->image, $this->html, $view );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search Result
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 15 Sep 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Result
|
||||
*/
|
||||
abstract class _Result
|
||||
{
|
||||
/**
|
||||
* @brief Created Date
|
||||
*/
|
||||
public $createdDate;
|
||||
|
||||
/**
|
||||
* @brief Last Updated Date
|
||||
*/
|
||||
public $lastUpdatedDate;
|
||||
|
||||
/**
|
||||
* Separator for activity streams - "past hour", "today", etc.
|
||||
*
|
||||
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
|
||||
* @return string
|
||||
*/
|
||||
public function streamSeparator( $createdDate=TRUE )
|
||||
{
|
||||
$date = $createdDate ? $this->createdDate : $this->lastUpdatedDate;
|
||||
|
||||
$now = \IPS\DateTime::ts( time() );
|
||||
$yesterday = clone $now;
|
||||
$yesterday = $yesterday->sub( new \DateInterval('P1D') );
|
||||
$diff = $date->diff( $now );
|
||||
if ( $diff->h < 1 && !$diff->d && !$diff->m )
|
||||
{
|
||||
return 'past_hour';
|
||||
}
|
||||
elseif ( $date->format('Y-m-d') == $now->format('Y-m-d') )
|
||||
{
|
||||
return 'today';
|
||||
}
|
||||
elseif ( $date->format('Y-m-d') == $yesterday->format('Y-m-d') )
|
||||
{
|
||||
return 'yesterday';
|
||||
}
|
||||
elseif ( !$diff->y and !$diff->m and $diff->d < 7 )
|
||||
{
|
||||
return 'last_week';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'earlier';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,41 @@ class _Results extends \ArrayIterator
|
||||
*/
|
||||
protected $initated = FALSE;
|
||||
|
||||
/**
|
||||
* @brief Author data
|
||||
*/
|
||||
protected $authorData = array();
|
||||
|
||||
/**
|
||||
* @brief Item titles
|
||||
*/
|
||||
protected $itemData = array();
|
||||
|
||||
/**
|
||||
* @brief Container data
|
||||
*/
|
||||
protected $containerData = array();
|
||||
|
||||
/**
|
||||
* @brief Reputation data
|
||||
*/
|
||||
protected $reputationData = array();
|
||||
|
||||
/**
|
||||
* @brief Review Ratings
|
||||
*/
|
||||
protected $reviewRatings = array();
|
||||
|
||||
/**
|
||||
* @brief Index IDs of items I have posted in
|
||||
*/
|
||||
protected $iPostedIn = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $results The results
|
||||
* @param int $countAllRows Count for all rows
|
||||
* @param array $results The results
|
||||
* @param int $countAllRows Count for all rows
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $results, $countAllRows )
|
||||
@@ -51,34 +81,107 @@ class _Results extends \ArrayIterator
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init( $loadReputation=FALSE )
|
||||
public function init()
|
||||
{
|
||||
$classesToLoad = array( 'IPS\Member' => array() );
|
||||
/* Init */
|
||||
$membersToLoad = array();
|
||||
$itemsToLoad = array();
|
||||
$containersToLoad = array();
|
||||
$reputationIds = array();
|
||||
$reviewRatings = array();
|
||||
$itemIndexIds = array();
|
||||
|
||||
foreach ( $this->getArrayCopy() as $row )
|
||||
/* Loop */
|
||||
foreach ( $this->getArrayCopy() as $result )
|
||||
{
|
||||
$classesToLoad[ 'IPS\Member' ][ $row['index_author'] ] = $row['index_author'];
|
||||
|
||||
$classesToLoad[ $row['index_class'] ][ $row['index_object_id'] ] = $row['index_object_id'];
|
||||
$itemClass = NULL;
|
||||
if ( $row['index_item_id'] and in_array( 'IPS\Content\Comment', class_parents( $row['index_class'] ) ) )
|
||||
if ( $result['index_author'] )
|
||||
{
|
||||
$itemClass = $row['index_class']::$itemClass;
|
||||
$classesToLoad[ $itemClass ][ $row['index_item_id'] ] = $row['index_item_id'];
|
||||
}
|
||||
if ( $row['index_container_id'] )
|
||||
{
|
||||
$containerClass = $itemClass ? $itemClass::$containerNodeClass : $row['index_class']::$containerNodeClass;
|
||||
$classesToLoad[ $containerClass ][ $row['index_container_id'] ] = $row['index_container_id'];
|
||||
$membersToLoad[ $result['index_author'] ] = $result['index_author'];
|
||||
}
|
||||
|
||||
if ( $loadReputation and in_array( 'IPS\Content\Reputation', class_implements( $row['index_class'] ) ) )
|
||||
if ( in_array( 'IPS\Content\Comment', class_parents( $result['index_class'] ) ) )
|
||||
{
|
||||
$reputationIds[ $row['index_class'] ][ $row['index_object_id'] ] = $row['index_object_id'];
|
||||
$commentClass = $result['index_class'];
|
||||
$itemsToLoad[ $commentClass::$itemClass ][ $result['index_item_id'] ] = $result['index_item_id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemsToLoad[ $result['index_class'] ][ $result['index_item_id'] ] = $result['index_item_id'];
|
||||
}
|
||||
|
||||
if ( $result['index_container_id'] )
|
||||
{
|
||||
$class = $result['index_class'];
|
||||
if ( $class == 'IPS\core\Statuses\Status' )
|
||||
{
|
||||
if ( $result['index_container_id'] != $result['index_author'] )
|
||||
{
|
||||
$membersToLoad[ $result['index_container_id'] ] = $result['index_container_id'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
if ( isset( $containerClass::$seoTitleColumn ) )
|
||||
{
|
||||
$containersToLoad[ $itemClass::$containerNodeClass ][ $result['index_container_id'] ] = $result['index_container_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $result['index_class'] ) ) )
|
||||
{
|
||||
$reputationIds[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
|
||||
}
|
||||
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( $result['index_class'] ) ) )
|
||||
{
|
||||
$reviewRatings[ $result['index_class'] ][ $result['index_object_id'] ] = $result['index_object_id'];
|
||||
}
|
||||
|
||||
if ( $result['index_item_index_id'] )
|
||||
{
|
||||
$itemIndexIds[] = $result['index_item_index_id'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Load item data */
|
||||
foreach ( $itemsToLoad as $itemClass => $itemIds )
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( $itemClass::basicDataColumns(), $itemClass::$databaseTable, \IPS\Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnId, $itemIds ) )->setKeyField( $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) as $itemId => $itemData )
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
|
||||
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
$membersToLoad[ $memberId ] = $memberId;
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $itemClass, 'searchResultExtraData' ) )
|
||||
{
|
||||
foreach ( $itemClass::searchResultExtraData( $this->itemData[ $itemClass ] ) as $id => $extraData )
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $id ]['extra'] = $extraData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Load author data */
|
||||
$this->authorData = iterator_to_array( \IPS\Db::i()->select( \IPS\Member::columnsForPhoto(), 'core_members', \IPS\Db::i()->in( 'member_id', $membersToLoad ) )->setKeyField( 'member_id' ) );
|
||||
|
||||
/* Load container data */
|
||||
foreach ( $containersToLoad as $containerClass => $containerIds )
|
||||
{
|
||||
$this->containerData[ $containerClass ] = iterator_to_array( \IPS\Db::i()->select( $containerClass::basicDataColumns(), $containerClass::$databaseTable, \IPS\Db::i()->in( $containerClass::$databasePrefix . $containerClass::$databaseColumnId, $containerIds ) )->setKeyField( $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) );
|
||||
}
|
||||
|
||||
/* Load reputation data */
|
||||
$reputation = array();
|
||||
if ( count( $reputationIds ) )
|
||||
{
|
||||
@@ -103,37 +206,25 @@ class _Results extends \ArrayIterator
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where ) as $rep )
|
||||
foreach ( \IPS\Db::i()->select( array( 'app', 'type', 'type_id', 'member_id', 'rep_rating' ), 'core_reputation_index', $where ) as $rep )
|
||||
{
|
||||
$reputation[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $classesToLoad as $class => $ids )
|
||||
{
|
||||
$ids = array_diff( $ids, $class::multitonIds() );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
|
||||
if ( !empty( $ids ) )
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( '*', $class::$databaseTable, \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnId, $ids ) ) as $row )
|
||||
{
|
||||
$obj = $class::constructFromData( $row );
|
||||
if ( $loadReputation and $obj instanceof \IPS\Content\Reputation )
|
||||
{
|
||||
if ( isset( $reputation[ $class::$application ][ $class::$reputationType ][ $obj->$idColumn ] ) )
|
||||
{
|
||||
$obj->reputation = $reputation[ $class::$application ][ $class::$reputationType ][ $obj->$idColumn ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$obj->reputation = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->reputationData[ $rep['app'] ][ $rep['type'] ][ $rep['type_id'] ][ $rep['member_id'] ] = $rep['rep_rating'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Load rating reviews */
|
||||
foreach ( $reviewRatings as $reviewClass => $reviewIds )
|
||||
{
|
||||
$this->reviewRatings[ $reviewClass ] = iterator_to_array( \IPS\Db::i()->select( array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ), $reviewClass::$databaseTable, \IPS\Db::i()->in( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewIds ) )->setKeyField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId )->setValueField( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['rating'] ) );
|
||||
}
|
||||
|
||||
/* Load data for the "I posted in this" stars */
|
||||
if ( count( $itemIndexIds ) and \IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$this->iPostedIn = iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', \IPS\Member::loggedIn()->member_id ) ), NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT ) );
|
||||
}
|
||||
|
||||
/* Set that we're initiated */
|
||||
$this->initated = TRUE;
|
||||
}
|
||||
|
||||
@@ -144,8 +235,41 @@ class _Results extends \ArrayIterator
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$row = parent::current();
|
||||
return call_user_func( array( $row['index_class'], 'load' ), $row['index_object_id'] );
|
||||
$data = parent::current();
|
||||
|
||||
$class = $data['index_class'];
|
||||
$itemClass = in_array( 'IPS\Content\Comment', class_parents( $class ) ) ? $class::$itemClass : $class;
|
||||
$containerClass = isset( $itemClass::$containerNodeClass ) ? $itemClass::$containerNodeClass : NULL;
|
||||
|
||||
$reputationData = array();
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and isset( $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ] ) )
|
||||
{
|
||||
$reputationData = $this->reputationData[ $class::$application ][ $class::$reputationType ][ $data['index_object_id'] ];
|
||||
}
|
||||
|
||||
$itemData = $this->itemData[ $itemClass ][ $data['index_item_id'] ];
|
||||
if ( $class == 'IPS\core\Statuses\Status' and $data['index_container_id'] != $data['index_author'] )
|
||||
{
|
||||
$itemData['profile'] = $this->authorData[ $data['index_container_id'] ];
|
||||
}
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
if ( isset( $this->authorData[ $memberId ] ) )
|
||||
{
|
||||
$itemData['author'] = $this->authorData[ $memberId ];
|
||||
}
|
||||
}
|
||||
|
||||
return new Result\Content(
|
||||
$data,
|
||||
isset( $this->authorData[ $data['index_author'] ] ) ? $this->authorData[ $data['index_author'] ] : array( 'member_id' => 0, 'name' => \IPS\Member::loggedIn()->language()->addToStack('guest'), 'members_seo_name' => '', 'pp_main_photo' => NULL, 'pp_photo_type' => 'none' ),
|
||||
$itemData,
|
||||
( $containerClass and isset( $this->containerData[ $containerClass ] ) and isset( $this->containerData[ $containerClass ][ $data['index_container_id'] ] ) ) ? $this->containerData[ $containerClass ][ $data['index_container_id'] ] : NULL,
|
||||
$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 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,4 +303,268 @@ class _Results extends \ArrayIterator
|
||||
return parent::count();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add in "extra" items
|
||||
*
|
||||
* @param array $extra Types ('register', 'follow_member', 'follow_content', 'photo', '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
|
||||
* @return array
|
||||
* @note Each thing is limited to 10 items. Even though this may result in accuracies, the limit is necessary so that if there's more extra items that content it doesn't create lots of queries and slow loading
|
||||
*/
|
||||
public function addExtraItems( $extra, \IPS\Member $author = NULL, $lastTime=NULL, $firstTime = NULL )
|
||||
{
|
||||
/* Work out the timestamps */
|
||||
$results = iterator_to_array( $this );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$firstTime = $firstTime->getTimestamp();
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->page ) and \IPS\Request::i()->page != 1 )
|
||||
{
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
$firstTime = $result->createdDate->getTimestamp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( $lastTime )
|
||||
{
|
||||
$lastTime = $lastTime->getTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( $results as $result )
|
||||
{
|
||||
$lastTime = $result->createdDate->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
/* We need at least a last time */
|
||||
if ( !$lastTime )
|
||||
{
|
||||
return $results;
|
||||
}
|
||||
|
||||
/* Get the extra items... */
|
||||
$extraItems = array();
|
||||
if ( $lastTime )
|
||||
{
|
||||
/* Users registered */
|
||||
if ( in_array( 'register', $extra ) )
|
||||
{
|
||||
$where = array( array( 'joined>?', $lastTime ), array( 'name<>?', '' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'joined<?', $firstTime );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( 'core_members.member_id=?', $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( 'core_members.member_id, core_members.name, core_members.members_seo_name, core_members.joined, core_validating.new_reg', 'core_members', $where, 'joined DESC', 10 )
|
||||
->join( 'core_validating', 'core_validating.member_id=core_members.member_id' ) as $member )
|
||||
{
|
||||
if ( empty( $member['new_reg'] ) )
|
||||
{
|
||||
$extraItems[] = new Result\Custom( \IPS\DateTime::ts( $member['joined'] ), \IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Users changed photo */
|
||||
if ( in_array( 'photo', $extra ) )
|
||||
{
|
||||
$where = array( array( 'photo_last_update>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'photo_last_update<?', $firstTime );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( 'member_id=?', $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( array_merge( \IPS\Member::columnsForPhoto(), array( 'photo_last_update' ) ), 'core_members', $where, 'photo_last_update DESC', 10 ) as $member )
|
||||
{
|
||||
$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\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 ) )
|
||||
{
|
||||
$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 ) )
|
||||
{
|
||||
$where[] = array( 'follow_app=? AND follow_area=?', 'core', 'member' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( '( follow_app!=? OR follow_area!=? )', 'core', 'member' );
|
||||
}
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
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 );
|
||||
}
|
||||
elseif ( in_array( 'follow_member', $extra ) )
|
||||
{
|
||||
$where[] = array( 'follow_rel_id=?', $author->member_id );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( 'follow_member_id=?', $author->member_id );
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( \IPS\Db::i()->select( array( 'follow_app', 'follow_area', 'follow_rel_id', 'follow_member_id', 'follow_added' ), 'core_follow', $where, 'follow_added DESC', 10 ) as $follow )
|
||||
{
|
||||
if ( $follow['follow_app'] == 'core' and $follow['follow_area'] == 'member' )
|
||||
{
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $follow['follow_added'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_rel_id'] ) ) ) ) )
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
|
||||
try
|
||||
{
|
||||
$thingBeingFollowed = $class::loadAndCheckPerms( $follow['follow_rel_id'] );
|
||||
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $follow['follow_added'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_followed', FALSE, array( 'htmlsprintf' => array( \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $follow['follow_member_id'] ) ), \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingFollowed->url(), FALSE, $thingBeingFollowed instanceof \IPS\Node\Model ? $thingBeingFollowed->_title : $thingBeingFollowed->mapped('title'), FALSE ) ) ) )
|
||||
);
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Likes */
|
||||
if ( in_array( 'like', $extra ) or in_array( 'rep_neg', $extra ) )
|
||||
{
|
||||
$where = array( array( 'rep_date>?', $lastTime ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'rep_date<?', $firstTime );
|
||||
}
|
||||
if ( !in_array( 'rep_neg', $extra ) or ( \IPS\Settings::i()->reputation_point_types != 'both' and \IPS\Settings::i()->reputation_point_types != 'negative' ) )
|
||||
{
|
||||
$where[] = array( 'rep_rating=1' );
|
||||
}
|
||||
elseif ( !in_array( 'like', $extra ) or ( \IPS\Settings::i()->reputation_point_types == 'negative' ) )
|
||||
{
|
||||
$where[] = array( 'rep_rating=-1' );
|
||||
}
|
||||
if ( $author )
|
||||
{
|
||||
$where[] = array( '( member_id=? OR member_received=? )', $author->member_id, $author->member_id );
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'rep_date DESC', 10 ) as $rep )
|
||||
{
|
||||
try
|
||||
{
|
||||
$thingBeingRepped = NULL;
|
||||
foreach ( \IPS\Application::load( $rep['app'] )->extensions( 'core', 'ContentRouter', TRUE, TRUE ) as $ext )
|
||||
{
|
||||
foreach ( $ext->classes as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $class ) ) and $class::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $class::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $commentClass ) ) and $commentClass::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $commentClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
if ( in_array( 'IPS\Content\Reputation', class_implements( $reviewClass ) ) and $reviewClass::$reputationType == $rep['type'] )
|
||||
{
|
||||
$thingBeingRepped = $reviewClass::loadAndCheckPerms( $rep['type_id'] );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( !$thingBeingRepped )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->reputation_point_types == 'like' )
|
||||
{
|
||||
$lang = 'activity_member_liked';
|
||||
}
|
||||
elseif ( $rep['rep_rating'] > 0 )
|
||||
{
|
||||
$lang = 'activity_rep_pos';
|
||||
}
|
||||
else
|
||||
{
|
||||
$lang = 'activity_rep_neg';
|
||||
}
|
||||
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $rep['rep_date'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( $lang, FALSE, array( 'htmlsprintf' => array(
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLink( \IPS\Member::load( $rep['member_id'] ) ),
|
||||
$thingBeingRepped->indefiniteArticle(),
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->basicUrl( $thingBeingRepped->url(), FALSE, $thingBeingRepped instanceof \IPS\Content\Item ? $thingBeingRepped->mapped('title') : $thingBeingRepped->item()->mapped('title'), FALSE )
|
||||
) ) )
|
||||
);
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge them in */
|
||||
if ( !empty( $extraItems ) )
|
||||
{
|
||||
$results = array_merge( $results, $extraItems );
|
||||
uasort( $results, function( $a, $b )
|
||||
{
|
||||
if ( $a->createdDate->getTimestamp() == $b->createdDate->getTimestamp() )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
elseif( $a->createdDate->getTimestamp() < $b->createdDate->getTimestamp() )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/* And return */
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Sphinx Search Index
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 26 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Sphinx;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphinx Search Index
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* @brief The index table
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
\IPS\Db::i( 'sphinx', array(
|
||||
'sql_host' => \IPS\Settings::i()->search_sphinx_server,
|
||||
'sql_user' => NULL,
|
||||
'sql_pass' => NULL,
|
||||
'sql_database' => NULL,
|
||||
'sql_port' => (int) \IPS\Settings::i()->search_sphinx_port,
|
||||
'sql_socket' => NULL,
|
||||
'sql_utf8mb4' => \IPS\Settings::i()->sql_utf8mb4
|
||||
) );
|
||||
|
||||
$this->table = \IPS\Settings::i()->sphinx_prefix . 'ips';
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$indexData = $this->indexData( $object );
|
||||
if ( $indexData )
|
||||
{
|
||||
$data = array_merge( array( 'id' => static::_getId( $object ), 'index_class_id' => static::_getClassId( get_class( $object ) ) ), $indexData );
|
||||
$data['index_permissions'] = json_encode( explode( ',', $data['index_permissions'] ) );
|
||||
|
||||
if( !$data['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
else
|
||||
{
|
||||
$result = \IPS\Db::i('sphinx')->query( "REPLACE INTO {$this->table} ( " . implode( ', ', array_keys( $data ) ) . ' ) VALUES ( ' . implode( ', ', array_map( array( $this, '_escapeValue' ), $data ) ) . ' )' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to remove
|
||||
* @return void
|
||||
*/
|
||||
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $object ) );
|
||||
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
|
||||
foreach ( \IPS\Db::i()->select( $commentClass::$databasePrefix . $commentClass::$databaseColumnId, $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $object->$idColumn ) ) as $commentid )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $commentid, $commentClass ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
|
||||
foreach ( \IPS\Db::i()->select( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnId, $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $object->$idColumn ) ) as $reviewid )
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE id=" . static::_getId( $reviewid, $reviewClass ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL )
|
||||
{
|
||||
$where = array( 'index_class_id=' . static::_getClassId( $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = "index_container_id={$containerId}";
|
||||
}
|
||||
|
||||
$where = implode( ' AND ', $where );
|
||||
|
||||
\IPS\Db::i('sphinx')->query( "DELETE FROM {$this->table} WHERE {$where}" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mass Update (when permissions change, for example)
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param int|NULL $itemId The item ID to update, or NULL
|
||||
* @param string|NULL $newPermissions New permissions (if applicable)
|
||||
* @param int|NULL $newHiddenStatus New hidden status (if applicable) special value 2 can be used to indicate hidden only by parent
|
||||
* @param int|NULL $newContainer New container ID (if applicable)
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL )
|
||||
{
|
||||
$where = array( 'index_class_id=' . static::_getClassId( $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = "index_container_id={$containerId}";
|
||||
}
|
||||
if ( $itemId !== NULL )
|
||||
{
|
||||
$where[] = "index_item_id={$itemId}";
|
||||
}
|
||||
|
||||
$update = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$update['index_permissions'] = explode( ',', str_replace( '*', 0, $newPermissions ) );
|
||||
}
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
{
|
||||
$where[] = 'index_hidden=0';
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = 'index_hidden=2';
|
||||
}
|
||||
|
||||
$update['index_hidden'] = $newHiddenStatus;
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$update['index_container_id'] = $newContainer;
|
||||
}
|
||||
|
||||
$set = array();
|
||||
foreach ( $update as $k => $v )
|
||||
{
|
||||
$set[] = "{$k}={$this->_escapeValue($v)}";
|
||||
}
|
||||
$set = implode( ', ', $set );
|
||||
|
||||
$where = implode( ' AND ', $where );
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "UPDATE {$this->table} SET {$set} WHERE {$where}" );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune search index
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $cutoff The date to delete index records from, or NULL to delete all
|
||||
* @return void
|
||||
*/
|
||||
public function prune( \IPS\DateTime $cutoff = NULL )
|
||||
{
|
||||
if ( $cutoff === NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Db::i('sphinx')->query( "TRUNCATE RTINDEX {$this->table}" );
|
||||
}
|
||||
catch( \IPS\Db\Exception $e )
|
||||
{
|
||||
/* If the index does not exist, we should not exit at this point in time */
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \UnexpectedValueException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID for an object
|
||||
*
|
||||
* @param \IPS\Content\Searchable|int $object Item to get ID for
|
||||
* @param string|NULL $class If $object is an ID rather than an object, provide the class
|
||||
* @return int
|
||||
*/
|
||||
public static function _getId( $object, $class = NULL )
|
||||
{
|
||||
if ( is_object( $object ) )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
$id = $object->$idColumn;
|
||||
}
|
||||
else
|
||||
{
|
||||
$id = $object;
|
||||
}
|
||||
|
||||
return ( static::_getClassId( $class ) * 100000000 ) + $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ID for an class
|
||||
*
|
||||
* @param string $class The class
|
||||
* @return int
|
||||
*/
|
||||
public static function _getClassId( $class )
|
||||
{
|
||||
$updateMap = FALSE;
|
||||
|
||||
/* Get the map (Older versions stored this in datastore, so we look there if we don't have it or use a blank array) */
|
||||
if ( !\IPS\Settings::i()->sphinx_id_map )
|
||||
{
|
||||
$map = isset( \IPS\Data\Store::i()->sphinxIdMap ) ? \IPS\Data\Store::i()->sphinxIdMap : array();
|
||||
$updateMap = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
$map = json_decode( \IPS\Settings::i()->sphinx_id_map, TRUE );
|
||||
}
|
||||
|
||||
/* Add it if we don't have it */
|
||||
if ( !in_array( $class, $map ) )
|
||||
{
|
||||
$map[] = $class;
|
||||
$updateMap = TRUE;
|
||||
}
|
||||
|
||||
/* Update the map if necessary */
|
||||
if ( $updateMap )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => json_encode( $map ) ), array( 'conf_key=?', 'sphinx_id_map' ) );
|
||||
unset( \IPS\Data\Store::i()->settings );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return intval( array_search( $class, $map ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for the query (SphinxQL doesn't support prepared statements)
|
||||
*
|
||||
* @param mixed $val The value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function _escapeValue( $val )
|
||||
{
|
||||
if ( is_numeric( $val ) )
|
||||
{
|
||||
return $val;
|
||||
}
|
||||
elseif ( is_array( $val ) )
|
||||
{
|
||||
return '(' . implode( ',', array_map( array( $this, '_escapeValue' ), $val ) ) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
return ( '\'' . \IPS\Db::i( 'sphinx' )->real_escape_string( $val ) . '\'' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Sphinx Search Query
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Social Suite
|
||||
* @since 26 Aug 2014
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Sphinx;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphinx Search Query
|
||||
*/
|
||||
class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
const SUPPORTS_JOIN_FILTERS = FALSE;
|
||||
|
||||
/**
|
||||
* @brief The index table
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause
|
||||
*/
|
||||
protected $where = array();
|
||||
|
||||
/**
|
||||
* @brief The WHERE clause for hidden/unhidden
|
||||
*/
|
||||
protected $hiddenClause = NULL;
|
||||
|
||||
/**
|
||||
* @brief The ORDER BY clause
|
||||
*/
|
||||
protected $order = NULL;
|
||||
|
||||
/**
|
||||
* @brief The offset
|
||||
*/
|
||||
protected $offset = 0;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Member $member The member performing the search
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Member $member )
|
||||
{
|
||||
\IPS\Db::i( 'sphinx', array(
|
||||
'sql_host' => \IPS\Settings::i()->search_sphinx_server,
|
||||
'sql_user' => NULL,
|
||||
'sql_pass' => NULL,
|
||||
'sql_database' => NULL,
|
||||
'sql_port' => (int) \IPS\Settings::i()->search_sphinx_port,
|
||||
'sql_socket' => NULL,
|
||||
'sql_utf8mb4' => \IPS\Settings::i()->sql_utf8mb4
|
||||
) );
|
||||
|
||||
$this->table = \IPS\Settings::i()->sphinx_prefix . 'ips';
|
||||
|
||||
parent::__construct( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type
|
||||
*
|
||||
* @param string $class The type of content to search (including all comment/review classes)
|
||||
* @param array|NULL $containers An array of container IDs to filter by, or NULL to not filter by containers
|
||||
* @param int|NULL $minimumComments The minimum number of comments
|
||||
* @param int|NULL $minimumReviews The minimum number of reviews
|
||||
* @param int|NULL $minimumViews The minimum number of views
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByContent( $class, $containers = NULL, $minimumComments = NULL, $minimumReviews = NULL, $minimumViews = NULL )
|
||||
{
|
||||
/* Classes */
|
||||
$classes = array( \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
|
||||
}
|
||||
$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
|
||||
|
||||
/* Containers */
|
||||
if ( $containers !== NULL )
|
||||
{
|
||||
$this->where[] = 'index_container_id IN(' . implode( ',', array_map( 'intval', $containers ) ) . ' )';
|
||||
}
|
||||
|
||||
/* We don't support anything else */
|
||||
if ( $minimumComments !== NULL or $minimumReviews !== NULL or $minimumViews !== NULL )
|
||||
{
|
||||
throw new \UnexpectedValueException;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content type to exclude
|
||||
*
|
||||
* @param array $classes The types of content to exclude (not including all comment/review classes)
|
||||
* @param bool $allowedContiners If TRUE, $class should be an array of allowed containers for that class - e.g. array( 'IPS\forums\Topic' => array( 1, 2 ) - otherwise, a simple array - e.g. array( 'IPS\forums\Topic' )
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByExcludeContent( array $classes, $allowedContiners=FALSE )
|
||||
{
|
||||
if ( $allowedContiners )
|
||||
{
|
||||
$where = array();
|
||||
foreach ( $classes as $class => $allowed )
|
||||
{
|
||||
if ( count( $allowed ) )
|
||||
{
|
||||
$where[] = '( index_class<>' . \IPS\Content\Search\Sphinx\Index::_getClassId( $class ) . ' OR index_container_id IN( ' . implode( ',', $allowed ) . ' ) )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$excludeAll[] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$finalWhere = array();
|
||||
if ( count( $excludeAll ) )
|
||||
{
|
||||
$finalWhere[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $excludeAll ) ) . ')';
|
||||
}
|
||||
if ( count( $where ) )
|
||||
{
|
||||
$finalWhere[] = implode( ' OR ', $where );
|
||||
}
|
||||
|
||||
$this->where[] = array_merge( array( implode( ' AND ', $finalWhere ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->where[] = 'index_class NOT IN('. implode( ',', array_map( array( 'IPS\Content\Search\Sphinx\Index', '_getClassId' ), $classes ) ) . ')';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by content item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByItem( \IPS\Content\Item $item )
|
||||
{
|
||||
$class = get_class( $item );
|
||||
|
||||
$classes = array();
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$commentClass );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = \IPS\Content\Search\Sphinx\Index::_getClassId( $class::$reviewClass );
|
||||
}
|
||||
$this->where[] = 'index_class_id IN(' . implode( ',', $classes ) . ')';
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$this->where[] = "index_item_id={$item->$idColumn}";
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by author
|
||||
*
|
||||
* @param \IPS\Member $author The author
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByAuthor( \IPS\Member $author )
|
||||
{
|
||||
$this->where[] = "index_author={$author->member_id}";
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by start date
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
|
||||
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByCreateDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
|
||||
{
|
||||
if ( $start )
|
||||
{
|
||||
$this->where[] = 'index_date_created>' . $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$this->where[] = 'index_date_created<' . $end->getTimestamp();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter by last updated date
|
||||
*
|
||||
* @param \IPS\DateTime|NULL $start The start date (only results AFTER this date will be returned)
|
||||
* @param \IPS\DateTime|NULL $end The end date (only results BEFORE this date will be returned)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function filterByLastUpdatedDate( \IPS\DateTime $start = NULL, \IPS\DateTime $end = NULL )
|
||||
{
|
||||
if ( $start )
|
||||
{
|
||||
$this->where[] = 'index_date_updated>' . $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$this->where[] = 'index_date_updated<' . $end->getTimestamp();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hidden status
|
||||
*
|
||||
* @param int|array $statuses The statuses (array of HIDDEN_ constants)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setHiddenFilter( $statuses )
|
||||
{
|
||||
if ( is_null( $statuses ) )
|
||||
{
|
||||
$this->hiddenClause = NULL;
|
||||
}
|
||||
if ( is_array( $statuses ) )
|
||||
{
|
||||
$this->hiddenClause = 'index_hidden IN(' . implode( ',', array_map( 'intval', $statuses ) ) . ' )';
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->hiddenClause = 'index_hidden=' . intval( $statuses );
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page
|
||||
*
|
||||
* @param int $page The page number
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setPage( $page )
|
||||
{
|
||||
$page = $page ?: 1;
|
||||
$this->offset = ( $page - 1 ) * $this->resultsToGet;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set order
|
||||
*
|
||||
* @param int $order Order (see ORDER_ constants)
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setOrder( $order )
|
||||
{
|
||||
switch ( $order )
|
||||
{
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
$this->order = 'index_date_updated DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_CREATED:
|
||||
$this->order = 'index_date_created DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search
|
||||
*
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method \IPS\Content\Search\Index::i()->TERM_OR_TAGS or \IPS\Content\Search\Index::i()->TERM_AND_TAGS
|
||||
* @return \IPS\Content\Search\Results
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
{
|
||||
/* Build the match term */
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$match[] = "( @(index_title,index_content) {$this->escape( $term )} )";
|
||||
}
|
||||
if ( $tags !== NULL )
|
||||
{
|
||||
$match[] = "( @index_tags " . implode( ' | ', array_map( array( $this, 'escape' ), $tags ) ) . ' )';
|
||||
}
|
||||
if ( !empty( $match ) )
|
||||
{
|
||||
array_unshift( $this->where, 'MATCH( \'' . implode( ( $method === static::TERM_OR_TAGS ? ' | ' : ' ' ), $match ) . '\' )' );
|
||||
}
|
||||
|
||||
/* Only get stuff we have permission for */
|
||||
$permissions = array( '\'' . $this->escape( '*' ) . '\'' );
|
||||
foreach ( $this->permissionArray() as $id )
|
||||
{
|
||||
$permissions[] = '\'' . $this->escape( $id ) . '\'';
|
||||
}
|
||||
$permCheck = 'IN(index_permissions,' . implode( ',', $permissions ) . ') AS perm_check';
|
||||
$this->where[] = 'perm_check=1';
|
||||
|
||||
/* Add the hidden clause */
|
||||
if ( $this->hiddenClause )
|
||||
{
|
||||
$this->where[] = $this->hiddenClause;
|
||||
}
|
||||
|
||||
/* Query */
|
||||
$query = \IPS\Db::i('sphinx')->query( "SELECT *, {$permCheck} FROM {$this->table} WHERE " . implode( ' AND ', $this->where ) . ( $this->order ? " ORDER BY {$this->order}" : '' ) . " LIMIT {$this->offset}, {$this->resultsToGet}" );
|
||||
|
||||
/* Get count */
|
||||
$meta = \IPS\Db::i('sphinx')->query('SHOW META');
|
||||
while ( $row = $meta->fetch_assoc() )
|
||||
{
|
||||
if ( $row['Variable_name'] === 'total' )
|
||||
{
|
||||
$count = $row['Value'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* Convert to array */
|
||||
$results = array();
|
||||
while ( $row = $query->fetch_assoc() )
|
||||
{
|
||||
$results[] = $row;
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Content\Search\Results( $results, $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape string for SphinxQL query
|
||||
*
|
||||
* @param string $term Search term
|
||||
* @return string
|
||||
*/
|
||||
protected function escape( $term )
|
||||
{
|
||||
return str_replace( array( '\\', '(',')','|','-','!','@','~','"','&', '/', '^', '$', '=', "'", "\x00", "\n", "\r", "\x1a" ), array( '\\\\', '\\\(','\\\)','\\\|','\\\-','\\\!','\\\@','\\\~','\\\"', '\\\&', '\\\/', '\\\^', '\\\$', '\\\=', "\\'", "\\x00", "\\n", "\\r", "\\x1a" ), $term );
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Reference in new issue
Block a user