Version 4.3.0
This commit is contained in:
1 parent
96997ddd8e
commit
fe1acf984a
1349 files changed
+116159
-67711
No files matched your search
@@ -25,10 +25,12 @@ class _CommentController extends \IPS\Api\Controller
|
||||
/**
|
||||
* List
|
||||
*
|
||||
* @param array $where Extra WHERE clause
|
||||
* @param array $where Extra WHERE clause
|
||||
* @param string $containerParam The parameter which includes the container values
|
||||
* @param bool $byPassPerms If permissions should be ignored
|
||||
* @return \IPS\Api\PaginatedResponse
|
||||
*/
|
||||
protected function _list( $where = array(), $containerParam = 'categories' )
|
||||
protected function _list( $where = array(), $containerParam = 'categories', $byPassPerms=FALSE )
|
||||
{
|
||||
$class = $this->class;
|
||||
$itemClass = $class::$itemClass;
|
||||
@@ -139,13 +141,27 @@ class _CommentController extends \IPS\Api\Controller
|
||||
}
|
||||
$sortDir = ( isset( \IPS\Request::i()->sortDir ) and in_array( mb_strtolower( \IPS\Request::i()->sortDir ), array( 'asc', 'desc' ) ) ) ? \IPS\Request::i()->sortDir : 'asc';
|
||||
|
||||
/* Get results */
|
||||
if ( $this->member and !$byPassPerms )
|
||||
{
|
||||
$query = $class::getItemsWithPermission( $where, "{$sortBy} {$sortDir}", NULL, 'read', \IPS\Content\Hideable::FILTER_AUTOMATIC, 0, $this->member )->getInnerIterator();
|
||||
$count = $class::getItemsWithPermission( $where, "{$sortBy} {$sortDir}", NULL, 'read', \IPS\Content\Hideable::FILTER_AUTOMATIC, 0, $this->member, FALSE, FALSE, FALSE, TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$query = \IPS\Db::i()->select( '*', $class::$databaseTable, $where, "{$sortBy} {$sortDir}" )->join( $itemClass::$databaseTable, $itemClass::$databasePrefix . $itemClass::$databaseColumnId . '=' . $class::$databasePrefix . $class::$databaseColumnMap['item'] );
|
||||
$count = \IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->join( $itemClass::$databaseTable, $itemClass::$databasePrefix . $itemClass::$databaseColumnId . '=' . $class::$databasePrefix . $class::$databaseColumnMap['item'] )->first();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Api\PaginatedResponse(
|
||||
200,
|
||||
\IPS\Db::i()->select( '*', $class::$databaseTable, $where, "{$sortBy} {$sortDir}" )->join( $itemClass::$databaseTable, $itemClass::$databasePrefix . $itemClass::$databaseColumnId . '=' . $class::$databasePrefix . $class::$databaseColumnMap['item'] ),
|
||||
$query,
|
||||
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
|
||||
$class,
|
||||
\IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->join( $itemClass::$databaseTable, $itemClass::$databasePrefix . $itemClass::$databaseColumnId . '=' . $class::$databasePrefix . $class::$databaseColumnMap['item'] )->first()
|
||||
$count,
|
||||
$this->member,
|
||||
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,7 +175,7 @@ class _CommentController extends \IPS\Api\Controller
|
||||
*/
|
||||
protected function _create( \IPS\Content\Item $item, \IPS\Member $author, $contentParam='content' )
|
||||
{
|
||||
return new \IPS\Api\Response( 201, $this->_createComment( $item, $author, $contentParam )->apiOutput() );
|
||||
return new \IPS\Api\Response( 201, $this->_createComment( $item, $author, $contentParam )->apiOutput( $this->member ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,17 +189,31 @@ class _CommentController extends \IPS\Api\Controller
|
||||
protected function _createComment( \IPS\Content\Item $item, \IPS\Member $author, $contentParam='content' )
|
||||
{
|
||||
/* Work out the date */
|
||||
$date = \IPS\Request::i()->date ? new \IPS\DateTime( \IPS\Request::i()->date ) : \IPS\DateTime::create();
|
||||
$date = ( !$this->member and \IPS\Request::i()->date ) ? new \IPS\DateTime( \IPS\Request::i()->date ) : \IPS\DateTime::create();
|
||||
|
||||
/* Is it hidden? */
|
||||
$hidden = NULL;
|
||||
if ( isset( \IPS\Request::i()->hidden ) and !$this->member )
|
||||
{
|
||||
$hidden = \IPS\Request::i()->hidden;
|
||||
}
|
||||
|
||||
/* Parse */
|
||||
$content = \IPS\Request::i()->$contentParam;
|
||||
if ( $this->member )
|
||||
{
|
||||
$content = \IPS\Text\Parser::parseStatic( $content, TRUE, NULL, $this->member, $item::$application . '_' . mb_ucfirst( $item::$module ) );
|
||||
}
|
||||
|
||||
/* Create post */
|
||||
$class = $this->class;
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( $class ) ) )
|
||||
{
|
||||
$comment = $class::create( $item, \IPS\Request::i()->$contentParam, FALSE, intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, \IPS\Request::i()->ip_address ?: \IPS\Request::i()->ipAddress(), isset( \IPS\Request::i()->hidden ) ? \IPS\Request::i()->hidden : NULL );
|
||||
$comment = $class::create( $item, $content, FALSE, intval( \IPS\Request::i()->rating ), $author->member_id ? NULL : $author->real_name, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ?: \IPS\Request::i()->ipAddress(), $hidden );
|
||||
}
|
||||
else
|
||||
{
|
||||
$comment = $class::create( $item, \IPS\Request::i()->$contentParam, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, \IPS\Request::i()->ip_address ?: \IPS\Request::i()->ipAddress(), isset( \IPS\Request::i()->hidden ) ? \IPS\Request::i()->hidden : NULL );
|
||||
$comment = $class::create( $item, $content, FALSE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date, ( !$this->member and \IPS\Request::i()->ip_address ) ?: \IPS\Request::i()->ipAddress(), $hidden );
|
||||
}
|
||||
|
||||
/* Index */
|
||||
@@ -206,6 +236,12 @@ class _CommentController extends \IPS\Api\Controller
|
||||
\IPS\Content\Search\Index::i()->index( $comment );
|
||||
}
|
||||
|
||||
/* Hide */
|
||||
if ( isset( \IPS\Request::i()->hidden ) and $this->member and \IPS\Request::i()->hidden and $comment->canHide( $this->member ) )
|
||||
{
|
||||
$comment->hide( $this->member );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $comment;
|
||||
}
|
||||
@@ -221,7 +257,7 @@ class _CommentController extends \IPS\Api\Controller
|
||||
protected function _edit( $comment, $contentParam='content' )
|
||||
{
|
||||
/* Hidden */
|
||||
if ( isset( \IPS\Request::i()->hidden ) )
|
||||
if ( !$this->member and isset( \IPS\Request::i()->hidden ) )
|
||||
{
|
||||
if ( \IPS\Request::i()->hidden )
|
||||
{
|
||||
@@ -234,7 +270,7 @@ class _CommentController extends \IPS\Api\Controller
|
||||
}
|
||||
|
||||
/* Change author */
|
||||
if ( isset( \IPS\Request::i()->author ) )
|
||||
if ( !$this->member and isset( \IPS\Request::i()->author ) )
|
||||
{
|
||||
$authorIdColumn = $comment::$databaseColumnMap['author'];
|
||||
$authorNameColumn = $comment::$databaseColumnMap['author_name'];
|
||||
@@ -269,7 +305,14 @@ class _CommentController extends \IPS\Api\Controller
|
||||
if ( isset( \IPS\Request::i()->$contentParam ) )
|
||||
{
|
||||
$contentColumn = $comment::$databaseColumnMap['content'];
|
||||
$comment->$contentColumn = \IPS\Request::i()->$contentParam;
|
||||
|
||||
$content = \IPS\Request::i()->$contentParam;
|
||||
if ( $this->member )
|
||||
{
|
||||
$item = $comment->item();
|
||||
$content = \IPS\Text\Parser::parseStatic( $content, TRUE, NULL, $this->member, $item::$application . '_' . mb_ucfirst( $item::$module ) );
|
||||
}
|
||||
$comment->$contentColumn =$content;
|
||||
}
|
||||
|
||||
/* Rating */
|
||||
@@ -295,7 +338,7 @@ class _CommentController extends \IPS\Api\Controller
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Api\Response( 200, $comment->apiOutput() );
|
||||
return new \IPS\Api\Response( 200, $comment->apiOutput( $this->member ) );
|
||||
|
||||
}
|
||||
}
|
||||
@@ -25,15 +25,17 @@ class _ItemController extends \IPS\Api\Controller
|
||||
/**
|
||||
* List
|
||||
*
|
||||
* @param array $where Extra WHERE clause
|
||||
* @param array $where Extra WHERE clause
|
||||
* @param string $containerParam The parameter which includes the container values
|
||||
* @param bool $byPassPerms If permissions should be ignored
|
||||
* @return \IPS\Api\PaginatedResponse
|
||||
*/
|
||||
protected function _list( $where = array(), $containerParam = 'categories' )
|
||||
protected function _list( $where = array(), $containerParam = 'categories', $byPassPerms=FALSE )
|
||||
{
|
||||
$class = $this->class;
|
||||
|
||||
/* Containers */
|
||||
if ( isset( \IPS\Request::i()->$containerParam ) )
|
||||
if ( $containerParam and isset( \IPS\Request::i()->$containerParam ) )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->in( $class::$databasePrefix . $class::$databaseColumnMap['container'], array_map( 'intval', array_filter( explode( ',', \IPS\Request::i()->$containerParam ) ) ) ) );
|
||||
}
|
||||
@@ -134,13 +136,27 @@ class _ItemController extends \IPS\Api\Controller
|
||||
}
|
||||
$sortDir = ( isset( \IPS\Request::i()->sortDir ) and in_array( mb_strtolower( \IPS\Request::i()->sortDir ), array( 'asc', 'desc' ) ) ) ? \IPS\Request::i()->sortDir : 'asc';
|
||||
|
||||
/* Get results */
|
||||
if ( $this->member and !$byPassPerms )
|
||||
{
|
||||
$query = $class::getItemsWithPermission( $where, "{$sortBy} {$sortDir}", NULL, 'view', \IPS\Content\Hideable::FILTER_AUTOMATIC, 0, $this->member )->getInnerIterator();
|
||||
$count = $class::getItemsWithPermission( $where, "{$sortBy} {$sortDir}", NULL, 'view', \IPS\Content\Hideable::FILTER_AUTOMATIC, 0, $this->member, FALSE, FALSE, FALSE, TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$query = \IPS\Db::i()->select( '*', $class::$databaseTable, $where, "{$sortBy} {$sortDir}" );
|
||||
$count = \IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first();
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Api\PaginatedResponse(
|
||||
200,
|
||||
\IPS\Db::i()->select( '*', $class::$databaseTable, $where, "{$sortBy} {$sortDir}" ),
|
||||
$query,
|
||||
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
|
||||
$class,
|
||||
\IPS\Db::i()->select( 'COUNT(*)', $class::$databaseTable, $where )->first()
|
||||
$count,
|
||||
$this->member,
|
||||
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,16 +169,24 @@ class _ItemController extends \IPS\Api\Controller
|
||||
protected function _view( $id )
|
||||
{
|
||||
$class = $this->class;
|
||||
return new \IPS\Api\Response( 200, $class::load( $id )->apiOutput() );
|
||||
|
||||
$item = $class::load( $id );
|
||||
if ( $this->member and !$item->can( 'read', $this->member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
|
||||
return new \IPS\Api\Response( 200, $item->apiOutput( $this->member ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update item
|
||||
*
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @param string $type add or edit
|
||||
* @return \IPS\Content\Item
|
||||
*/
|
||||
protected function _createOrUpdate( \IPS\Content\Item $item )
|
||||
protected function _createOrUpdate( \IPS\Content\Item $item, $type='add' )
|
||||
{
|
||||
/* Title */
|
||||
if ( isset( \IPS\Request::i()->title ) and isset( $item::$databaseColumnMap['title'] ) )
|
||||
@@ -174,85 +198,98 @@ class _ItemController extends \IPS\Api\Controller
|
||||
/* Tags */
|
||||
if ( ( isset( \IPS\Request::i()->prefix ) or isset( \IPS\Request::i()->tags ) ) and in_array( 'IPS\Content\Tags', class_implements( get_class( $item ) ) ) )
|
||||
{
|
||||
$tags = isset( \IPS\Request::i()->tags ) ? array_filter( explode( ',', \IPS\Request::i()->tags ) ) : $item->tags();
|
||||
if ( isset( \IPS\Request::i()->prefix ) )
|
||||
{
|
||||
if ( \IPS\Request::i()->prefix )
|
||||
if ( !$this->member or $item::canTag( $this->member, $item->containerWrapper() ) )
|
||||
{
|
||||
$tags = isset( \IPS\Request::i()->tags ) ? array_filter( explode( ',', \IPS\Request::i()->tags ) ) : $item->tags();
|
||||
|
||||
if ( !$this->member or $item::canPrefix( $this->member, $item->containerWrapper() ) )
|
||||
{
|
||||
$tags['prefix'] = \IPS\Request::i()->prefix;
|
||||
if ( isset( \IPS\Request::i()->prefix ) )
|
||||
{
|
||||
if ( \IPS\Request::i()->prefix )
|
||||
{
|
||||
$tags['prefix'] = \IPS\Request::i()->prefix;
|
||||
}
|
||||
}
|
||||
elseif ( $existingPrefix = $item->prefix() )
|
||||
{
|
||||
$tags['prefix'] = $existingPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
/* we need to save the item before we set the tags because setTags requires that the item exists */
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
if ( !$item->$idColumn )
|
||||
{
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$item->setTags( $tags );
|
||||
}
|
||||
elseif ( $existingPrefix = $item->prefix() )
|
||||
{
|
||||
$tags['prefix'] = $existingPrefix;
|
||||
}
|
||||
|
||||
/* we need to save the item before we set the tags because setTags requires that the item exists */
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
if ( !$item->$idColumn )
|
||||
{
|
||||
$item->save();
|
||||
}
|
||||
|
||||
$item->setTags( $tags );
|
||||
}
|
||||
|
||||
/* Open/closed */
|
||||
if ( isset( \IPS\Request::i()->locked ) and in_array( 'IPS\Content\Lockable', class_implements( get_class( $item ) ) ) )
|
||||
{
|
||||
if ( isset( $item::$databaseColumnMap['locked'] ) )
|
||||
if ( !$this->member or ( \IPS\Request::i()->locked and $item->canLock( $this->member ) ) or ( !\IPS\Request::i()->locked and $item->canUnlock( $this->member ) ) )
|
||||
{
|
||||
$lockedColumn = $item::$databaseColumnMap['locked'];
|
||||
$item->$lockedColumn = intval( \IPS\Request::i()->locked );
|
||||
}
|
||||
else
|
||||
{
|
||||
$stateColumn = $item::$databaseColumnMap['status'];
|
||||
$item->$stateColumn = \IPS\Request::i()->locked ? 'closed' : 'open';
|
||||
if ( isset( $item::$databaseColumnMap['locked'] ) )
|
||||
{
|
||||
$lockedColumn = $item::$databaseColumnMap['locked'];
|
||||
$item->$lockedColumn = intval( \IPS\Request::i()->locked );
|
||||
}
|
||||
else
|
||||
{
|
||||
$stateColumn = $item::$databaseColumnMap['status'];
|
||||
$item->$stateColumn = \IPS\Request::i()->locked ? 'closed' : 'open';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hidden */
|
||||
if ( isset( \IPS\Request::i()->hidden ) and in_array( 'IPS\Content\Hideable', class_implements( get_class( $item ) ) ) )
|
||||
{
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
if ( \IPS\Request::i()->hidden )
|
||||
if ( !$this->member or ( \IPS\Request::i()->hidden and $item->canHide( $this->member ) ) or ( !\IPS\Request::i()->hidden and $item->canUnhide( $this->member ) ) )
|
||||
{
|
||||
if ( $item->$idColumn )
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
if ( \IPS\Request::i()->hidden )
|
||||
{
|
||||
$item->hide( FALSE );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( $item::$databaseColumnMap['hidden'] ) )
|
||||
if ( $item->$idColumn )
|
||||
{
|
||||
$hiddenColumn = $item::$databaseColumnMap['hidden'];
|
||||
$item->$hiddenColumn = \IPS\Request::i()->hidden;
|
||||
$item->hide( FALSE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$approvedColumn = $item::$databaseColumnMap['approved'];
|
||||
$item->$approvedColumn = ( \IPS\Request::i()->hidden == -1 ) ? -1 : 0;
|
||||
if ( isset( $item::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$hiddenColumn = $item::$databaseColumnMap['hidden'];
|
||||
$item->$hiddenColumn = \IPS\Request::i()->hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
$approvedColumn = $item::$databaseColumnMap['approved'];
|
||||
$item->$approvedColumn = ( \IPS\Request::i()->hidden == -1 ) ? -1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $item->$idColumn )
|
||||
{
|
||||
$item->unhide( FALSE );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( $item::$databaseColumnMap['hidden'] ) )
|
||||
if ( $item->$idColumn )
|
||||
{
|
||||
$hiddenColumn = $item::$databaseColumnMap['hidden'];
|
||||
$item->$hiddenColumn = 0;
|
||||
$item->unhide( FALSE );
|
||||
}
|
||||
else
|
||||
{
|
||||
$approvedColumn = $item::$databaseColumnMap['approved'];
|
||||
$item->$approvedColumn = 1;
|
||||
if ( isset( $item::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$hiddenColumn = $item::$databaseColumnMap['hidden'];
|
||||
$item->$hiddenColumn = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
$approvedColumn = $item::$databaseColumnMap['approved'];
|
||||
$item->$approvedColumn = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,33 +298,52 @@ class _ItemController extends \IPS\Api\Controller
|
||||
/* Pinned */
|
||||
if ( isset( \IPS\Request::i()->pinned ) and in_array( 'IPS\Content\Pinnable', class_implements( get_class( $item ) ) ) )
|
||||
{
|
||||
$pinnedColumn = $item::$databaseColumnMap['pinned'];
|
||||
$item->$pinnedColumn = intval( \IPS\Request::i()->pinned );
|
||||
if ( !$this->member or ( \IPS\Request::i()->pinned and $item->canPin( $this->member ) ) or ( !\IPS\Request::i()->pinned and $item->canUnpin( $this->member ) ) )
|
||||
{
|
||||
$pinnedColumn = $item::$databaseColumnMap['pinned'];
|
||||
$item->$pinnedColumn = intval( \IPS\Request::i()->pinned );
|
||||
}
|
||||
}
|
||||
|
||||
/* Featured */
|
||||
if ( isset( \IPS\Request::i()->featured ) and in_array( 'IPS\Content\Featurable', class_implements( get_class( $item ) ) ) )
|
||||
{
|
||||
$featuredColumn = $item::$databaseColumnMap['featured'];
|
||||
$item->$featuredColumn = intval( \IPS\Request::i()->featured );
|
||||
if ( !$this->member or ( \IPS\Request::i()->featured and $item->canFeature( $this->member ) ) or ( !\IPS\Request::i()->featured and $item->canUnfeature( $this->member ) ) )
|
||||
{
|
||||
$featuredColumn = $item::$databaseColumnMap['featured'];
|
||||
$item->$featuredColumn = intval( \IPS\Request::i()->featured );
|
||||
}
|
||||
}
|
||||
|
||||
/* Update first comment if required */
|
||||
$field = $item::$databaseColumnMap['first_comment_id'];
|
||||
/* Update first comment if required, and it's not a new item */
|
||||
$field = isset( $item::$databaseColumnMap['first_comment_id'] ) ? $item::$databaseColumnMap['first_comment_id'] : NULL;
|
||||
$commentClass = $item::$commentClass;
|
||||
$contentField = $commentClass::$databaseColumnMap['content'];
|
||||
|
||||
if ( $item::$firstCommentRequired AND isset( $item->$field ) AND isset( \IPS\Request::i()->$contentField ) )
|
||||
if ( $item::$firstCommentRequired AND isset( $item->$field ) AND isset( \IPS\Request::i()->$contentField ) AND $type == 'edit' )
|
||||
{
|
||||
$comment = $commentClass::load( $item->$field );
|
||||
$comment->$contentField = \IPS\Request::i()->$contentField;
|
||||
$comment->save();
|
||||
$content = \IPS\Request::i()->$contentField;
|
||||
if ( $this->member )
|
||||
{
|
||||
$content = \IPS\Text\Parser::parseStatic( $content, TRUE, NULL, $this->member, $item::$application . '_' . mb_ucfirst( $item::$module ) );
|
||||
}
|
||||
|
||||
/* Update Search Index of the first item */
|
||||
if ( $item instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $comment );
|
||||
}
|
||||
try
|
||||
{
|
||||
$comment = $commentClass::load( $item->$field );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'NO_FIRST_POST', '1S377/1', 400 );
|
||||
}
|
||||
|
||||
$comment->$contentField = $content;
|
||||
$comment->save();
|
||||
|
||||
/* Update Search Index of the first item */
|
||||
if ( $item instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $comment );
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
@@ -308,18 +364,26 @@ class _ItemController extends \IPS\Api\Controller
|
||||
$class = $this->class;
|
||||
|
||||
/* Work out the date */
|
||||
$date = \IPS\Request::i()->date ? new \IPS\DateTime( \IPS\Request::i()->date ) : \IPS\DateTime::create();
|
||||
$date = ( !$this->member and \IPS\Request::i()->date ) ? new \IPS\DateTime( \IPS\Request::i()->date ) : \IPS\DateTime::create();
|
||||
|
||||
/* Create item */
|
||||
$item = $class::createItem( $author, \IPS\Request::i()->ip_address ?: \IPS\Request::i()->ipAddress(), $date, $container );
|
||||
$this->_createOrUpdate( $item );
|
||||
$item = $class::createItem( $author, ( !$this->member and \IPS\Request::i()->ip_address ) ? \IPS\Request::i()->ip_address : \IPS\Request::i()->ipAddress(), $date, $container );
|
||||
$this->_createOrUpdate( $item, 'add' );
|
||||
$item->save();
|
||||
|
||||
/* Create post */
|
||||
if ( $class::$firstCommentRequired )
|
||||
{
|
||||
{
|
||||
$postContents = \IPS\Request::i()->$firstPostParam;
|
||||
|
||||
if ( $this->member )
|
||||
{
|
||||
$postContents = \IPS\Text\Parser::parseStatic( $postContents, TRUE, NULL, $this->member, $class::$application . '_' . mb_ucfirst( $class::$module ) );
|
||||
}
|
||||
|
||||
$commentClass = $item::$commentClass;
|
||||
$post = $commentClass::create( $item, \IPS\Request::i()->$firstPostParam, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date );
|
||||
$post = $commentClass::create( $item, $postContents, TRUE, $author->member_id ? NULL : $author->real_name, NULL, $author, $date );
|
||||
|
||||
if ( isset( $class::$databaseColumnMap['first_comment_id'] ) )
|
||||
{
|
||||
$firstCommentColumn = $class::$databaseColumnMap['first_comment_id'];
|
||||
@@ -362,6 +426,10 @@ class _ItemController extends \IPS\Api\Controller
|
||||
/* Init */
|
||||
$itemClass = $this->class;
|
||||
$item = $itemClass::load( $id );
|
||||
if ( $this->member and !$item->can( 'read', $this->member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
$itemIdColumn = $itemClass::$databaseColumnId;
|
||||
$where [] = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$itemIdColumn );
|
||||
|
||||
@@ -406,7 +474,9 @@ class _ItemController extends \IPS\Api\Controller
|
||||
\IPS\Db::i()->select( '*', $commentClass::$databaseTable, $where, "{$sortBy} {$sortDir}" ),
|
||||
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
|
||||
$commentClass,
|
||||
\IPS\Db::i()->select( 'COUNT(*)', $commentClass::$databaseTable, $where )->first()
|
||||
\IPS\Db::i()->select( 'COUNT(*)', $commentClass::$databaseTable, $where )->first(),
|
||||
$this->member,
|
||||
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -98,11 +98,12 @@ trait ClubContainer
|
||||
{
|
||||
$nodeClass = get_called_class();
|
||||
$itemClass = $nodeClass::$contentItemClass;
|
||||
$haveId = (bool) $this->_id;
|
||||
|
||||
$this->_saveClubForm( $club, $values );
|
||||
|
||||
$needToUpdatePermissions = FALSE;
|
||||
if ( !$this->_id )
|
||||
if ( !$haveId )
|
||||
{
|
||||
$clubIdColumn = $this->clubIdColumn();
|
||||
$this->$clubIdColumn = $club->id;
|
||||
@@ -129,6 +130,12 @@ trait ClubContainer
|
||||
{
|
||||
$this->setPermissionsToClub( $club );
|
||||
}
|
||||
|
||||
$followApp = $itemClass::$application;
|
||||
$followArea = mb_strtolower( mb_substr( $nodeClass, mb_strrpos( $nodeClass, '\\' ) + 1 ) );
|
||||
$time = time();
|
||||
\IPS\Db::i()->insert( 'core_follow', \IPS\Db::i()->select( "MD5( CONCAT( '{$followApp};{$followArea};{$this->_id};', follow_member_id ) ) AS follow_id, '{$followApp}' AS follow_app, '{$followArea}' AS follow_area, '{$this->_id}' AS follow_rel_id, follow_member_id, follow_is_anon, '{$time}' AS follow_added, follow_notify_do, follow_notify_meta, follow_notify_freq, 0 AS follow_notify_sent, follow_visible", 'core_follow', array(
|
||||
'follow_app=? AND follow_area=? AND follow_rel_id=?', 'core', 'club', $club->id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +171,7 @@ trait ClubContainer
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
case $club::TYPE_OPEN:
|
||||
case $club::TYPE_READONLY:
|
||||
$insert[ 'perm_' . $v ] = '*';
|
||||
break;
|
||||
|
||||
@@ -173,11 +181,28 @@ trait ClubContainer
|
||||
break;
|
||||
}
|
||||
}
|
||||
elseif ( in_array( $k, array( 'add', 'edit', 'reply', 'review' ) ) )
|
||||
{
|
||||
switch ( $club->type )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
$insert[ 'perm_' . $v ] = 'ca';
|
||||
break;
|
||||
|
||||
case $club::TYPE_OPEN:
|
||||
case $club::TYPE_CLOSED:
|
||||
case $club::TYPE_PRIVATE:
|
||||
case $club::TYPE_READONLY:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch ( $club->type )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
case $club::TYPE_READONLY:
|
||||
$insert[ 'perm_' . $v ] = 'ca';
|
||||
break;
|
||||
|
||||
@@ -226,8 +251,40 @@ trait ClubContainer
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnParent . '=?', static::$databaseColumnParentRootValue );
|
||||
}
|
||||
|
||||
$order = static::$databasePrefix . static::clubIdColumn();
|
||||
|
||||
return static::nodesWithPermission( $permissionCheck, $member, $where );
|
||||
if( static::$databaseColumnOrder !== NULL )
|
||||
{
|
||||
$order .= ', ' . static::$databasePrefix . static::$databaseColumnOrder;
|
||||
}
|
||||
|
||||
return static::nodesWithPermission( $permissionCheck, $member, $where, $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached club nodes
|
||||
*/
|
||||
protected static $cachedClubNodes = NULL;
|
||||
|
||||
/**
|
||||
* Fetch All Nodes in Clubs
|
||||
*
|
||||
* @param string|NULL $permissionCheck The permission key to check for or NULl to not check permissions
|
||||
* @param \IPS\Member|NULL $member The member to check permissions for or NULL for the currently logged in member
|
||||
* @param mixed $where Additional WHERE clause
|
||||
* @return array
|
||||
*/
|
||||
public static function clubNodes( $permissionCheck='view', $member=NULL, $where=array() )
|
||||
{
|
||||
if( static::$cachedClubNodes === NULL )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::clubIdColumn() . ' IS NOT NULL' );
|
||||
|
||||
static::$cachedClubNodes = static::nodesWithPermission( $permissionCheck, $member, $where );
|
||||
}
|
||||
|
||||
return static::$cachedClubNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,9 +292,10 @@ trait ClubContainer
|
||||
*
|
||||
* @param string $type 'edit', 'hide', 'unhide', 'delete', etc.
|
||||
* @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member
|
||||
* @param string|NULL $itemClass The item class to check against, or NULL to check against static::$contentItemClass
|
||||
* @return bool
|
||||
*/
|
||||
public function modPermission( $type, \IPS\Member $member )
|
||||
public function modPermission( $type, \IPS\Member $member, $itemClass=NULL )
|
||||
{
|
||||
if ( \IPS\Settings::i()->clubs )
|
||||
{
|
||||
@@ -253,8 +311,8 @@ trait ClubContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::modPermission( $type, $member );
|
||||
|
||||
return parent::modPermission( $type, $member, $itemClass );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+69
-40
@@ -648,29 +648,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can report?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canReport( $member=NULL )
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Content\ReportCenter ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return ( $member->group['g_can_report'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_can_report'] ) ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search Index Permissions
|
||||
*
|
||||
@@ -977,7 +955,7 @@ abstract class _Comment extends \IPS\Content
|
||||
$this->save();
|
||||
|
||||
/* The new item needs to re-claim any attachments associated with this comment */
|
||||
\IPS\Db::i()->update( 'core_attachments_map', array( 'id1' => $item->$idColumn ), array( "location_key=? AND id1=? AND id2=?", $oldItem::$application . '_' . ucfirst( $oldItem::$module ), $oldItem->$idColumn, $this->$commentIdColumn ) );
|
||||
\IPS\Db::i()->update( 'core_attachments_map', array( 'id1' => $item->$idColumn ), array( "location_key=? AND id1=? AND id2=?", $oldItem::$application . '_' . mb_ucfirst( $oldItem::$module ), $oldItem->$idColumn, $this->$commentIdColumn ) );
|
||||
|
||||
/* Update notifications */
|
||||
\IPS\Db::i()->update( 'core_notifications', array( 'item_id' => $item->$idColumn ), array( 'item_class=? and item_id=? and item_sub_class=? and item_sub_id=?', (string) get_class( $item ), $oldItem->$idColumn, (string) get_called_class(), $this->$commentIdColumn ) );
|
||||
@@ -1059,7 +1037,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Unclaim attachments */
|
||||
\IPS\File::unclaimAttachments( $itemClass::$application . '_' . ucfirst( $itemClass::$module ), $itemId, $this->$idColumn );
|
||||
\IPS\File::unclaimAttachments( $itemClass::$application . '_' . mb_ucfirst( $itemClass::$module ), $itemId, $this->$idColumn );
|
||||
|
||||
/* Reduce the number of comment/reviews count on the item but only if the item is unapproved or visible
|
||||
* - hidden as opposed to unapproved items do not get included in either of the unapproved_comments/num_comments columns */
|
||||
@@ -1179,6 +1157,11 @@ abstract class _Comment extends \IPS\Content
|
||||
public function changeAuthor( \IPS\Member $newAuthor )
|
||||
{
|
||||
$oldAuthor = $this->author();
|
||||
|
||||
if( $oldAuthor->member_id == $newAuthor->member_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Update the row */
|
||||
parent::changeAuthor( $newAuthor );
|
||||
@@ -1237,6 +1220,11 @@ abstract class _Comment extends \IPS\Content
|
||||
return \IPS\Theme::i()->getTemplate( 'global', static::$application )->commentTableHeader( $this, $this->item() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached containers we can access
|
||||
*/
|
||||
protected static $permissionSelect = array();
|
||||
|
||||
/**
|
||||
* Get comments based on some arbitrary parameters
|
||||
*
|
||||
@@ -1392,10 +1380,22 @@ abstract class _Comment extends \IPS\Content
|
||||
if ( $joinContainer AND isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$joins[] = array(
|
||||
'from' => $containerClass::$databaseTable,
|
||||
'where' => array_merge( array( array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), $containerWhere )
|
||||
);
|
||||
if( $joins !== NULL )
|
||||
{
|
||||
array_unshift( $joins, array(
|
||||
'from' => $containerClass::$databaseTable,
|
||||
'where' => array_merge( array( array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), $containerWhere )
|
||||
) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$joins = array(
|
||||
array(
|
||||
'from' => $containerClass::$databaseTable,
|
||||
'where' => array_merge( array( array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), $containerWhere )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Build the select clause */
|
||||
@@ -1417,7 +1417,7 @@ abstract class _Comment extends \IPS\Content
|
||||
->join( $itemClass::$databaseTable, array_merge( array( array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['item'] . "=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) ), $itemWhere ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
|
||||
if ( count( $joins ) )
|
||||
if ( $joins !== NULL AND count( $joins ) )
|
||||
{
|
||||
foreach( $joins as $join )
|
||||
{
|
||||
@@ -1428,7 +1428,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
$selectClause = static::$databaseTable . '.*';
|
||||
if ( count( $joins ) )
|
||||
if ( $joins !== NULL AND count( $joins ) )
|
||||
{
|
||||
foreach( $joins as $join )
|
||||
{
|
||||
@@ -1441,15 +1441,43 @@ abstract class _Comment extends \IPS\Content
|
||||
|
||||
if ( in_array( 'IPS\Content\Permissions', class_implements( $itemClass ) ) AND $permissionKey !== NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
|
||||
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
$categories = array();
|
||||
$lookupKey = md5( $containerClass::$permApp . $containerClass::$permType . $permissionKey . json_encode( $member->groups ) );
|
||||
|
||||
if( !isset( static::$permissionSelect[ $lookupKey ] ) )
|
||||
{
|
||||
static::$permissionSelect[ $lookupKey ] = array();
|
||||
$permQuery = \IPS\Db::i()->select( 'perm_type_id', 'core_permission_index', array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND (" . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' )" ) );
|
||||
|
||||
if ( count( $containerWhere ) )
|
||||
{
|
||||
$permQuery->join( $containerClass::$databaseTable, array_merge( $containerWhere, array( 'core_permission_index.perm_type_id=' . $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::$databaseColumnId ) ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
|
||||
foreach( $permQuery as $result )
|
||||
{
|
||||
static::$permissionSelect[ $lookupKey ][] = $result;
|
||||
}
|
||||
}
|
||||
|
||||
$categories = static::$permissionSelect[ $lookupKey ];
|
||||
|
||||
if( count( $categories ) )
|
||||
{
|
||||
$where[] = array( $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . ' IN(' . implode( ',', $categories ) . ')' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . '=0' );
|
||||
}
|
||||
|
||||
$selectClause .= ', ' . $itemClass::$databaseTable . '.*';
|
||||
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, NULL, NULL, $queryFlags )
|
||||
->join( $itemClass::$databaseTable, array_merge( array( array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['item'] . "=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) ), $itemWhere ), 'STRAIGHT_JOIN' )
|
||||
->join( 'core_permission_index', array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'] . ' AND (' . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->groups ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . '=? )', $containerClass::$permApp, $containerClass::$permType, '*' ), 'STRAIGHT_JOIN' );
|
||||
->join( $itemClass::$databaseTable, array_merge( array( array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['item'] . "=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) ), $itemWhere ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1457,7 +1485,7 @@ abstract class _Comment extends \IPS\Content
|
||||
->join( $itemClass::$databaseTable, array_merge( array( array( static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnMap['item'] . "=" . $itemClass::$databaseTable . "." . $itemClass::$databasePrefix . $itemClass::$databaseColumnId ) ), $itemWhere ), 'STRAIGHT_JOIN' );
|
||||
}
|
||||
|
||||
if ( count( $joins ) )
|
||||
if ( $joins !== NULL AND count( $joins ) )
|
||||
{
|
||||
foreach( $joins as $join )
|
||||
{
|
||||
@@ -1514,7 +1542,7 @@ abstract class _Comment extends \IPS\Content
|
||||
$item = $this->item();
|
||||
$attachments = array();
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=? and id2=?', $item::$application . '_' . ucfirst( $item::$module ), $item->$itemIdColumn, $this->$idColumn ) );
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=? and id2=?', $item::$application . '_' . mb_ucfirst( $item::$module ), $item->$itemIdColumn, $this->$idColumn ) );
|
||||
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_attachments', array( array( 'attach_id IN(?)', $internal ), array( 'attach_is_image=1' ) ), 'attach_id ASC', $limit ) as $row )
|
||||
{
|
||||
@@ -1584,6 +1612,7 @@ abstract class _Comment extends \IPS\Content
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse int item_id The ID number of the item this belongs to
|
||||
@@ -1593,14 +1622,14 @@ abstract class _Comment extends \IPS\Content
|
||||
* @apiresponse bool hidden Is hidden?
|
||||
* @apiresponse string url URL to content
|
||||
*/
|
||||
public function apiOutput()
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$itemColumn = static::$databaseColumnMap['item'];
|
||||
return array(
|
||||
'id' => $this->$idColumn,
|
||||
'item_id' => $this->$itemColumn,
|
||||
'author' => $this->author()->apiOutput(),
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => \IPS\DateTime::ts( $this->mapped('date') )->rfc3339(),
|
||||
'content' => $this->content(),
|
||||
'hidden' => (bool) $this->hidden(),
|
||||
|
||||
+329
-117
@@ -36,12 +36,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @brief [Content\Comment] Language prefix for forms
|
||||
*/
|
||||
public static $formLangPrefix = '';
|
||||
|
||||
/**
|
||||
* @brief [Content\Item] Include this content type in user profiles
|
||||
*/
|
||||
public static $includeInUserProfiles = TRUE;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Include In Sitemap
|
||||
*/
|
||||
@@ -52,6 +47,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
protected $reputation;
|
||||
|
||||
/**
|
||||
* @brief Can this content be moderated normally from the front-end (will be FALSE for things like Pages and Commerce Products)
|
||||
*/
|
||||
public static $canBeModeratedFromFrontend = TRUE;
|
||||
|
||||
/**
|
||||
* Should posting this increment the poster's post count?
|
||||
*
|
||||
@@ -71,20 +71,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
*/
|
||||
public static function memberPostCount( \IPS\Member $member )
|
||||
{
|
||||
return static::incrementPostCount() ? \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ) )->first() : 0;
|
||||
return ( isset( static::$databaseColumnMap['author'] ) and static::incrementPostCount() ) ? \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, array( static::$databasePrefix . static::$databaseColumnMap['author'] . '=?', $member->member_id ) )->first() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and check permissions
|
||||
*
|
||||
* @param mixed $id ID
|
||||
* @param \IPS\Member|NULL $member Member, or NULL for logged in member
|
||||
* @return static
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public static function loadAndCheckPerms( $id )
|
||||
public static function loadAndCheckPerms( $id, \IPS\Member $member = NULL )
|
||||
{
|
||||
$obj = static::load( $id );
|
||||
|
||||
if ( !$obj->canView( \IPS\Member::loggedIn() ) )
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
if ( !$obj->canView( $member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
@@ -264,7 +267,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
if ( isset( static::$databaseColumnMap['content'] ) )
|
||||
{
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . ucfirst( static::$module ), $this->$idColumn ) );
|
||||
$internal = \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=? and id1=?', static::$application . '_' . mb_ucfirst( static::$module ), $this->$idColumn ) );
|
||||
}
|
||||
|
||||
if ( $internal )
|
||||
@@ -275,7 +278,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
return count( $attachments ) ? $attachments : NULL; }
|
||||
return count( $attachments ) ? $attachments : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text for use with data-ipsTruncate
|
||||
@@ -293,6 +297,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Convert headers and paragraphs into line breaks or just spaces */
|
||||
$text = str_replace( array( '</p>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>' ), ( $oneLine ? ' ' : '<br>' ), $text );
|
||||
|
||||
if( $oneLine === TRUE )
|
||||
{
|
||||
$text = str_replace( '<br>', ' <br>', $text );
|
||||
}
|
||||
|
||||
/* Add a space at the end of list items to prevent two list items from running into each other */
|
||||
$text = str_replace( '</li>', ' </li>', $text );
|
||||
|
||||
@@ -300,7 +309,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$text = strip_tags( $text, '<br>' );
|
||||
|
||||
/* Remove any <br>s from the start so there isn't just blank space at the top, but maintaining <br>s elsewhere */
|
||||
$text = preg_replace( '/^(\s|<br>|' . chr(0xC2) . chr(0xA0) . ')+?/', '', $text );
|
||||
$text = preg_replace( '/^(\s|<br>|' . chr(0xC2) . chr(0xA0) . ')+/', '', $text );
|
||||
|
||||
/* Return */
|
||||
return $text;
|
||||
@@ -320,13 +329,26 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( 'app=? AND type=? AND type_id=?', static::$application, $this->reactionType(), $this->$idColumn ) );
|
||||
}
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
|
||||
{
|
||||
$this->deleteReport();
|
||||
}
|
||||
|
||||
/* Remove any entries in the promotions table */
|
||||
\IPS\Db::i()->delete( 'core_social_promote', array( 'promote_class=? AND promote_class_id=?', get_class( $this ), $this->$idColumn ) );
|
||||
|
||||
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
|
||||
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
|
||||
}
|
||||
|
||||
parent::delete();
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -473,12 +495,24 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Per-container permission */
|
||||
elseif ( $container )
|
||||
{
|
||||
return $container->modPermission( $type, $member );
|
||||
return $container->modPermission( $type, $member, static::getContainerModPermissionClass() );
|
||||
}
|
||||
|
||||
/* Still here? return false */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the container item class to use for mod permission checks
|
||||
*
|
||||
* @return string|NULL
|
||||
* @note By default we will return NULL and the container check will execute against Node::$contentItemClass, however
|
||||
* in some situations we may need to override this (i.e. for Gallery Albums)
|
||||
*/
|
||||
protected static function getContainerModPermissionClass()
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do Moderator Action
|
||||
@@ -510,7 +544,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$toCheck = 'restore';
|
||||
}
|
||||
|
||||
if ( !call_user_func( array( $this, 'can' . ucfirst( $toCheck ) ), $member ) )
|
||||
if ( !call_user_func( array( $this, 'can' . mb_ucfirst( $toCheck ) ), $member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
@@ -579,7 +613,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_deletion_log', array( "dellog_content_class=? AND dellog_content_id=?", get_class( $this ), $this->$idColumn ) );
|
||||
$this->delete();
|
||||
return;
|
||||
}
|
||||
@@ -608,7 +641,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Move is just a method */
|
||||
elseif ( $action === 'move' )
|
||||
{
|
||||
$args = func_get_args();
|
||||
$args = func_get_args();
|
||||
$this->move( $args[2][0], $args[2][1] );
|
||||
return;
|
||||
}
|
||||
@@ -638,27 +671,6 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$column = static::$databaseColumnMap['approved'];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
if ( $this->container()->_comments !== NULL )
|
||||
{
|
||||
/* Hidden content already decrements */
|
||||
if ( !$this->hidden() )
|
||||
{
|
||||
$this->container()->_comments = ( $this->container()->_comments - $this->mapped('num_comments') );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->container()->_unapprovedComments !== NULL )
|
||||
{
|
||||
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments - $this->mapped( 'unapproved_comments' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
|
||||
$this->$column = -2;
|
||||
$this->save();
|
||||
|
||||
@@ -686,6 +698,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
$this->container()->resetCommentCounts();
|
||||
$this->container()->setLastComment();
|
||||
$this->container()->save();
|
||||
}
|
||||
@@ -755,44 +768,13 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Delete the log */
|
||||
$log->delete();
|
||||
|
||||
/* Log to moderate logs */
|
||||
if ( !$hidden )
|
||||
|
||||
/* Recount the container counters */
|
||||
if( $this->container() )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $this->container() )
|
||||
{
|
||||
if ( $this->container()->_comments !== NULL )
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Item )
|
||||
{
|
||||
$this->container()->_comments = ( $this->container()->_comments + $this->mapped('num_comments') );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->container()->_comments = $this->container()->_comments + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $this->container()->_unapprovedComments !== NULL )
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Item )
|
||||
{
|
||||
$this->container()->_unapprovedComments = ( $this->container()->_unapprovedComments + $this->mapped('unapproved_comments') );
|
||||
}
|
||||
}
|
||||
|
||||
$this->container()->setLastComment();
|
||||
$this->container()->save();
|
||||
|
||||
if ( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$this->item()->rebuildFirstAndLastCommentData();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
$this->container()->resetCommentCounts();
|
||||
$this->container()->setLastComment();
|
||||
$this->container()->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,7 +787,19 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
public function canRestore( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
return \IPS\Member::loggedIn()->modPermission('can_restore_deleted_content');
|
||||
return $member->modPermission('can_manage_deleted_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Give class a chance to inspect and manipulate search engine filters for streams
|
||||
*
|
||||
* @param array $filters Filters to be used for activity stream
|
||||
* @param \IPS\Content\Search\Query $query Search query object
|
||||
* @return void
|
||||
*/
|
||||
public static function searchEngineFiltering( &$filters, &$query )
|
||||
{
|
||||
/* Intentionally left blank but child classes can override */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -866,6 +860,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -924,7 +919,13 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
$this->save();
|
||||
$this->onUnhide( $approving, $member );
|
||||
|
||||
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
|
||||
}
|
||||
|
||||
/* And update the tags perm cache */
|
||||
if ( $this instanceof \IPS\Content\Tags )
|
||||
{
|
||||
@@ -937,6 +938,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
}
|
||||
|
||||
/* Update report center stuff */
|
||||
if ( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
|
||||
{
|
||||
$this->moderated( 'unhide' );
|
||||
}
|
||||
|
||||
/* Send notifications if necessary */
|
||||
if ( $approving )
|
||||
{
|
||||
@@ -944,8 +951,14 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hidden blurb cache
|
||||
*/
|
||||
protected $hiddenBlurb = NULL;
|
||||
|
||||
/**
|
||||
* Blurb for when/why/by whom this content was hidden
|
||||
*
|
||||
@@ -958,16 +971,30 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
try
|
||||
if( $this->hiddenBlurb === NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$log = \IPS\Db::i()->select( '*', 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) )->first();
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb', FALSE, array( 'sprintf' => array( \IPS\Member::load( $log['sdl_obj_member_id'] )->name, \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), $log['sdl_obj_reason'] ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$log = \IPS\Db::i()->select( '*', 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) )->first();
|
||||
|
||||
if ( $log['sdl_obj_member_id'] )
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb', FALSE, array( 'sprintf' => array( \IPS\Member::load( $log['sdl_obj_member_id'] )->name, \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), \IPS\Member::loggedIn()->language()->addToStack( $log['sdl_obj_reason'] ) ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden_blurb_no_member', FALSE, array( 'sprintf' => array( \IPS\DateTime::ts( $log['sdl_obj_date'] )->relative(), \IPS\Member::loggedIn()->language()->addToStack( $log['sdl_obj_reason'] ) ?: \IPS\Member::loggedIn()->language()->addToStack('hidden_no_reason') ) ) );
|
||||
}
|
||||
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->hiddenBlurb;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1005,6 +1032,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return \IPS\core\Promote::canPromote( $member );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Have we already reported?
|
||||
*/
|
||||
protected $alreadyReported = NULL;
|
||||
|
||||
/**
|
||||
* Can report?
|
||||
@@ -1018,13 +1050,13 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* Is this type of comment reportabe? */
|
||||
if ( !( $this instanceof \IPS\Content\ReportCenter ) )
|
||||
if ( !( \IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) ) )
|
||||
{
|
||||
return 'generic_error';
|
||||
}
|
||||
|
||||
/* Can the member report content? */
|
||||
if ( !$member->group['g_can_report'] )
|
||||
if ( $member->group['g_can_report'] != '1' AND !in_array( get_class( $this ), explode( ',', $member->group['g_can_report'] ) ) )
|
||||
{
|
||||
return 'no_module_permission';
|
||||
}
|
||||
@@ -1036,28 +1068,71 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
|
||||
/* Have they already subitted a report? */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$report = \IPS\Db::i()->select( 'id', 'core_rc_index', array( 'class=? AND content_id=?', get_called_class(), $this->$idColumn ) );
|
||||
if ( count( $report ) )
|
||||
if( $this->alreadyReported === TRUE )
|
||||
{
|
||||
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? AND report_by=?', $report->first(), $member->member_id ) );
|
||||
if ( count( $report ) )
|
||||
return 'report_err_already_reported';
|
||||
}
|
||||
elseif( $this->alreadyReported === NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
try
|
||||
{
|
||||
$report = \IPS\Db::i()->select( 'id', 'core_rc_index', array( 'class=? AND content_id=?', get_called_class(), $this->$idColumn ) )->first();
|
||||
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? AND report_by=?', $report, $member->member_id ) )->first();
|
||||
|
||||
if ( \IPS\Settings::i()->automoderation_report_again_mins )
|
||||
{
|
||||
if ( ( ( time() - $report['date_reported'] ) / 60 ) > \IPS\Settings::i()->automoderation_report_again_mins )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
$this->alreadyReported = TRUE;
|
||||
return 'report_err_already_reported';
|
||||
}
|
||||
catch( \UnderflowException $e ){}
|
||||
|
||||
$this->alreadyReported = FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can report or revoke report?
|
||||
* This method will return TRUE if the link to report content should be shown (which can occur even if you have already reported if you have permission to revoke your report)
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
* @note This requires a few queries, so don't run a check in every template
|
||||
*/
|
||||
public function canReportOrRevoke( $member=NULL )
|
||||
{
|
||||
/* If we are allowed to report, then we can return TRUE. */
|
||||
if( $this->canReport( $member ) === TRUE )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
/* If we have already reported but automatic moderation is enabled, show the link so the user can revoke their report. */
|
||||
elseif( $this->alreadyReported === TRUE AND \IPS\Settings::i()->automoderation_enabled )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report
|
||||
*
|
||||
* @param string $reportContent Report content message from member
|
||||
* @return \\IPS\core\Reports\Report
|
||||
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report
|
||||
* @return \IPS\core\Reports\Report
|
||||
* @throws \UnexpectedValueException If there is a permission error - you should only call this method after checking canReport
|
||||
*/
|
||||
public function report( $reportContent )
|
||||
public function report( $reportContent, $reportType=1 )
|
||||
{
|
||||
/* Permission check */
|
||||
if ( $this->canReport() !== TRUE )
|
||||
@@ -1084,6 +1159,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$index->author = (int) $this->author()->member_id;
|
||||
$index->num_reports = 1;
|
||||
$index->num_comments = 0;
|
||||
$index->auto_moderation_exempt = 0;
|
||||
}
|
||||
|
||||
/* Only set this to a new report if it is not already under review */
|
||||
@@ -1100,11 +1176,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
'report' => $reportContent,
|
||||
'report_by' => (int) \IPS\Member::loggedIn()->member_id,
|
||||
'date_reported' => time(),
|
||||
'ip_address' => \IPS\Request::i()->ipAddress()
|
||||
'ip_address' => \IPS\Request::i()->ipAddress(),
|
||||
'report_type' => \IPS\Member::loggedIn()->member_id ? $reportType : 0
|
||||
);
|
||||
|
||||
\IPS\Db::i()->insert( 'core_rc_reports', $reportInsert );
|
||||
|
||||
|
||||
/* Run automatic moderation */
|
||||
$index->runAutomaticModeration();
|
||||
|
||||
/* Send notification to mods */
|
||||
$moderators = array( 'm' => array(), 'g' => array() );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $mod )
|
||||
@@ -1149,6 +1229,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$oldAuthor = $this->author();
|
||||
|
||||
if( $oldAuthor->member_id == $newAuthor->member_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( array( 'author', 'author_name', 'edit_member_name' ) as $k )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap[ $k ] ) )
|
||||
@@ -1211,6 +1296,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Object URL */
|
||||
$indefiniteArticle = static::_indefiniteArticle( $containerData );
|
||||
$definiteArticle = static::_definiteArticle( $containerData );
|
||||
$definiteArticleUc = static::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
|
||||
if ( in_array( 'IPS\Content\Comment', class_parents( get_called_class() ) ) )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
|
||||
@@ -1226,13 +1312,14 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
$indefiniteArticle = $itemClass::_indefiniteArticle( $containerData );
|
||||
$definiteArticle = $itemClass::_definiteArticle( $containerData );
|
||||
$definiteArticleUc = $itemClass::_definiteArticle( $containerData, NULL, array( 'ucfirst' => TRUE ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$objectUrl = $itemUrl;
|
||||
$showRepUrl = $itemUrl->setQueryString( 'do', 'showReactions' );
|
||||
}
|
||||
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle );
|
||||
$articles = array( 'indefinite' => $indefiniteArticle, 'definite' => $definiteArticle, 'definite_uc' => $definiteArticleUc );
|
||||
|
||||
/* Container details */
|
||||
$containerUrl = NULL;
|
||||
@@ -1244,8 +1331,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$containerUrl = $containerClass::urlFromIndexData( $indexData, $itemData, $containerData );
|
||||
}
|
||||
|
||||
/* Reputation */
|
||||
$repCount = array_sum( $reputationData );
|
||||
/* Reputation - if we are showing the total value, then we need to load them up and total up all of the values */
|
||||
if ( \IPS\Settings::i()->reaction_count_display == 'count' )
|
||||
{
|
||||
$repCount = 0;
|
||||
foreach( $reputationData AS $memberId => $reactionId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$repCount += \IPS\Content\Reaction::load( $reactionId )->value;
|
||||
}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$repCount = count( $reputationData );
|
||||
}
|
||||
|
||||
/* Snippet */
|
||||
$snippet = static::searchResultSnippet( $indexData, $authorData, $itemData, $containerData, $reputationData, $reviewRating, $view );
|
||||
@@ -1276,6 +1378,78 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
return $view == 'expanded' ? \IPS\Theme::i()->getTemplate( 'system', 'core', 'front' )->searchResultSnippet( $indexData ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the language string key to use in search results
|
||||
*
|
||||
* @note Normally we show "(user) posted a (thing) in (area)" but sometimes this may not be accurate, so this is abstracted to allow
|
||||
* content classes the ability to override
|
||||
* @param array $authorData Author data
|
||||
* @param array $articles Articles language strings
|
||||
* @param array $indexData Search index data
|
||||
* @param array $itemData Data about the item
|
||||
* @return string
|
||||
*/
|
||||
public static function searchResultSummaryLanguage( $authorData, $articles, $indexData, $itemData )
|
||||
{
|
||||
if( in_array( 'IPS\Content\Comment', class_parents( $indexData['index_class'] ) ) )
|
||||
{
|
||||
if( in_array( 'IPS\Content\Review', class_parents( $indexData['index_class'] ) ) )
|
||||
{
|
||||
if( isset( $itemData['author'] ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_review", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_review", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( static::$firstCommentRequired )
|
||||
{
|
||||
if( $indexData['index_title'] )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( isset( $itemData['author'] ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_reply", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_reply", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( isset( $itemData['author'] ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_other_activity_comment", FALSE, array( 'sprintf' => array( $authorData['name'], $itemData['author']['name'], $articles['definite'] ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_comment", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "user_own_activity_item", FALSE, array( 'sprintf' => array( $authorData['name'], $articles['indefinite'] ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( "generic_activity_item", FALSE, array( 'sprintf' => array( $articles['definite_uc'] ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Return a classname applied to the search result block
|
||||
*/
|
||||
@@ -1334,7 +1508,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
const FOLLOW_PUBLIC = 1;
|
||||
const FOLLOW_ANONYMOUS = 2;
|
||||
|
||||
const NOTIFICATIONS_PER_BATCH = 50;
|
||||
const NOTIFICATIONS_PER_BATCH = \IPS\NOTIFICATIONS_PER_BATCH;
|
||||
|
||||
/**
|
||||
* Send notifications
|
||||
@@ -1360,7 +1534,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Queue if there's lots, or just send them */
|
||||
if ( $count > static::NOTIFICATIONS_PER_BATCH )
|
||||
{
|
||||
\IPS\Task::queue( 'core', 'Follow', array( 'class' => get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo ), 1 );
|
||||
\IPS\Task::queue( 'core', 'Follow', array( 'class' => get_class( $this ), 'item' => $this->$idColumn, 'sentTo' => $sentTo ), 2 );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1543,7 +1717,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
foreach ( $data['quotes'] as $quote )
|
||||
{
|
||||
$member = \IPS\Member::load( $quote );
|
||||
if ( $member->member_id and $member != $this->author() and $this->canView( $member ) )
|
||||
if ( $member->member_id and $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'posts' ) )
|
||||
{
|
||||
$notification->recipients->attach( $member );
|
||||
}
|
||||
@@ -1562,7 +1736,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
foreach ( $data['mentions'] as $mention )
|
||||
{
|
||||
$member = \IPS\Member::load( $mention );
|
||||
if ( $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'mentions' ) )
|
||||
if ( $member->member_id AND $member != $this->author() and $this->canView( $member ) and !$member->isIgnoring( $this->author(), 'mentions' ) )
|
||||
{
|
||||
$notification->recipients->attach( $member );
|
||||
}
|
||||
@@ -1622,6 +1796,39 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\Widget::deleteCaches( NULL, static::$application );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update "currently viewing" session data after moderator actions that invalidate that data for other users
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function adjustSessions()
|
||||
{
|
||||
if( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
}
|
||||
else
|
||||
{
|
||||
$item = $this;
|
||||
}
|
||||
|
||||
/* We have to send a limit even though we want all records because otherwise the Database store does not return all columns */
|
||||
foreach( \IPS\Session\Store::i()->getOnlineUsers( 0, 'desc', array( 0, 5000 ), NULL, TRUE, TRUE ) as $session )
|
||||
{
|
||||
if( mb_strpos( $session['location_url'], (string) $item->url() ) === 0 )
|
||||
{
|
||||
$sessionData = $session;
|
||||
$sessionData['location_url'] = NULL;
|
||||
$sessionData['location_lang'] = NULL;
|
||||
$sessionData['location_data'] = json_encode( array() );
|
||||
$sessionData['current_id'] = 0;
|
||||
$sessionData['location_permissions'] = 0;
|
||||
|
||||
\IPS\Session\Store::i()->updateSession( $sessionData );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch classes from content router
|
||||
*
|
||||
@@ -1755,13 +1962,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Definite Article
|
||||
*
|
||||
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
|
||||
* @param array $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
|
||||
* @param \IPS\Lang|NULL $language The language to use, or NULL for the language of the currently logged in member
|
||||
* @param array $options Options to pass to \IPS\Lang::addToStack
|
||||
* @return string
|
||||
*/
|
||||
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL )
|
||||
public static function _definiteArticle( array $containerData = NULL, \IPS\Lang $lang = NULL, $options = array() )
|
||||
{
|
||||
$lang = $lang ?: \IPS\Member::loggedIn()->language();
|
||||
return $lang->addToStack( '__defart_' . static::$title, FALSE );
|
||||
return $lang->addToStack( '__defart_' . static::$title, FALSE, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1830,22 +2039,25 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
/* !Deprecated */
|
||||
/* The methods below are deprecated and will be removed in Invision Community 4.3 */
|
||||
/* !Search */
|
||||
|
||||
/**
|
||||
* Get reputation count
|
||||
* Title for search index
|
||||
*
|
||||
* @return int
|
||||
* @throws \BadMethodCallException
|
||||
* @return string
|
||||
*/
|
||||
public function reputation()
|
||||
public function searchIndexTitle()
|
||||
{
|
||||
if ( !\IPS\IPS::classUsesTrait( $this, 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
return $this->reactionCount();
|
||||
return $this->mapped('title');
|
||||
}
|
||||
|
||||
/**
|
||||
* Content for search index
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchIndexContent()
|
||||
{
|
||||
return $this->mapped('content');
|
||||
}
|
||||
}
|
||||
+485
-123
@@ -61,6 +61,31 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
|
||||
/* Have we moved? If a user has loaded a forum, and a moderator merges two topics leaving a link, then we need to account for this if the
|
||||
user happens to hover over the topic that was removed before refreshing the page. */
|
||||
if ( isset( $class::$databaseColumnMap['state'] ) AND isset( $class::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
$stateColumn = $class::$databaseColumnMap['state'];
|
||||
$movedToColumn = $class::$databaseColumnMap['moved_to'];
|
||||
$movedTo = explode( '&', $item->$movedToColumn );
|
||||
|
||||
if ( $item->$stateColumn == 'link' OR $item->$stateColumn == 'merged' )
|
||||
{
|
||||
try
|
||||
{
|
||||
$moved = $class::loadAndCheckPerms( $movedTo[0] );
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() AND \IPS\Request::i()->preview )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $moved->url(), '', 301 );
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/* If this is an AJAX request (like the topic hovercard), we don't want to do any of the below like update views and mark read */
|
||||
if ( \IPS\Request::i()->isAjax() and !$this->updateViewsAndMarkersOnAjax )
|
||||
@@ -78,10 +103,24 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and \IPS\CACHE_CONFIG )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
$countUpdated = true;
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +163,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$pageCount = call_user_func( array( $item, "{$paginationType}PageCount" ) );
|
||||
if ( $pageCount and \IPS\Request::i()->page > $pageCount )
|
||||
{
|
||||
\IPS\Output::i()->redirect( call_user_func( array( $item, 'last' . ucfirst( $paginationType ) . 'PageUrl' ) ), NULL, 303 );
|
||||
\IPS\Output::i()->redirect( call_user_func( array( $item, 'last' . mb_ucfirst( $paginationType ) . 'PageUrl' ) ), NULL, 303 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +202,24 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( in_array( 'IPS\Content\Views', class_implements( $class ) ) AND isset( $class::$databaseColumnMap['views'] ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
$countUpdated = false;
|
||||
if ( \IPS\REDIS_ENABLED and \IPS\CACHE_METHOD == 'Redis' and \IPS\CACHE_CONFIG )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'topic_views', 1, $class .'__' . $item->$idColumn );
|
||||
$countUpdated = true;
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_view_updates', array(
|
||||
'classname' => $class,
|
||||
'id' => $item->$idColumn
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark read */
|
||||
@@ -181,24 +234,6 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
|
||||
/* Have we moved? */
|
||||
if ( isset( $class::$databaseColumnMap['state'] ) AND isset( $class::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
$stateColumn = $class::$databaseColumnMap['state'];
|
||||
$movedToColumn = $class::$databaseColumnMap['moved_to'];
|
||||
$movedTo = explode( '&', $item->$movedToColumn );
|
||||
|
||||
if ( $item->$stateColumn == 'link' OR $item->$stateColumn == 'merged' )
|
||||
{
|
||||
try
|
||||
{
|
||||
$moved = $class::loadAndCheckPerms( $movedTo[0] );
|
||||
\IPS\Output::i()->redirect( $moved->url(), '', 301 );
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
/* Set navigation and title */
|
||||
$this->_setBreadcrumbAndTitle( $item, FALSE );
|
||||
|
||||
@@ -209,6 +244,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->metaTags['og:type'] = 'object';
|
||||
\IPS\Output::i()->metaTags['og:url'] = (string) $item->url();
|
||||
\IPS\Output::i()->metaTags['og:description'] = $item->metaDescription();
|
||||
|
||||
/* Facebook Pixel */
|
||||
$itemId = $class::$databaseColumnId;
|
||||
\IPS\core\Facebook\Pixel::i()->PageView = array(
|
||||
'item_id' => $item->$itemId,
|
||||
'item_name' => $item->mapped( 'title' ),
|
||||
'item_type' => isset( $class::$contentType ) ? $class::$contentType : $class::$title,
|
||||
'category_name' => isset( $item::$databaseColumnMap['container'] ) ? $item->container()->_title : NULL
|
||||
);
|
||||
|
||||
if( $item->mapped( 'updated' ) OR $item->mapped( 'last_comment' ) OR $item->mapped( 'last_review' ) )
|
||||
{
|
||||
\IPS\Output::i()->metaTags['og:updated_time'] = \IPS\DateTime::ts( $item->mapped( 'updated' ) ? $item->mapped( 'updated' ) : ( $item->mapped( 'last_comment' ) ? $item->mapped( 'last_comment' ) : $item->mapped( 'last_review' ) ) )->rfc3339();
|
||||
@@ -216,22 +261,19 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$tags = array();
|
||||
|
||||
if( $item->prefix() !== NULL )
|
||||
{
|
||||
$tags[] = $item->prefix();
|
||||
}
|
||||
|
||||
if( $item->tags() !== NULL )
|
||||
{
|
||||
if( $item->prefix() !== NULL )
|
||||
{
|
||||
$tags[] = $item->prefix();
|
||||
}
|
||||
$tags = array_merge( $tags, $item->tags() );
|
||||
}
|
||||
|
||||
foreach( $item->tags() as $tag )
|
||||
{
|
||||
$tags[] = $tag;
|
||||
}
|
||||
|
||||
if( count( $tags ) )
|
||||
{
|
||||
\IPS\Output::i()->metaTags['keywords'] = implode( ', ', $tags );
|
||||
}
|
||||
if( count( $tags ) )
|
||||
{
|
||||
\IPS\Output::i()->metaTags['keywords'] = implode( ', ', $tags );
|
||||
}
|
||||
|
||||
/* Add contextual search options */
|
||||
@@ -260,6 +302,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
*/
|
||||
protected function checkForNewReplies()
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
/* If auto-polling isn't enabled, kill the polling now */
|
||||
if ( !\IPS\Settings::i()->auto_polling_enabled )
|
||||
{
|
||||
@@ -269,6 +313,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
try
|
||||
{
|
||||
/* no need for polling for embeds */
|
||||
if ( !isset( static::$contentModel ) )
|
||||
{
|
||||
\IPS\Output::i()->json( array( 'error' => 'auto_polling_disabled' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$class = static::$contentModel;
|
||||
|
||||
/* no need for polling if the content item doesn't have comments */
|
||||
@@ -322,7 +373,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$itemData = reset( $newComments );
|
||||
$author = $itemData->author();
|
||||
|
||||
$data['name'] = htmlspecialchars( $author->name, \IPS\HTMLENTITIES | ENT_QUOTES, 'UTF-8', FALSE );
|
||||
$data['name'] = htmlspecialchars( $author->name, ENT_DISALLOWED | ENT_QUOTES, 'UTF-8', FALSE );
|
||||
$data['photo'] = (string) $author->photo;
|
||||
}
|
||||
|
||||
@@ -340,13 +391,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
/* Only mark as read if we'll be staying on this page, otherise we'll be marking it read despite
|
||||
the user not having seen everything */
|
||||
if( (int) \IPS\Request::i()->showing + (int) count( $newComments ) <= $class::getCommentsPerPage() ){
|
||||
if( (int) \IPS\Request::i()->showing + (int) count( $newComments ) <= $class::getCommentsPerPage() )
|
||||
{
|
||||
$item->markRead();
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( array(
|
||||
'content' => $output,
|
||||
'content' => \IPS\Output::i()->replaceEmojiWithImages( $output ),
|
||||
'id' => $lastId,
|
||||
'totalCount' => $item->mapped( 'num_comments' ),
|
||||
'totalNewCount' => (int) count( $newComments ),
|
||||
@@ -426,7 +478,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->editContentForm( \IPS\Member::loggedIn()->language()->addToStack( 'edit_title', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $item::$title ) ) ) ), $form, $container );
|
||||
|
||||
if( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'forms', 'core' )->editContentForm( \IPS\Member::loggedIn()->language()->addToStack( 'edit_title', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $item::$title ) ) ) ), $form, $container );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
@@ -468,7 +528,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
/* If the tag form field is generated, create the form, otherwise throw an exception */
|
||||
if( $tagsField = $item::tagsFormField( $item, $container ) )
|
||||
if( $tagsField = $item::tagsFormField( $item, $container, TRUE ) )
|
||||
{
|
||||
$form = new \IPS\Helpers\Form( 'form', \IPS\Member::loggedIn()->language()->checkKeyExists( $item::$formLangPrefix . '_save' ) ? $item::$formLangPrefix . '_save' : 'save' );
|
||||
$form->class = 'ipsForm_vertical ipsForm_fullWidth';
|
||||
@@ -498,7 +558,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
/* Now set the tags */
|
||||
$name = $tagsField->name;
|
||||
\IPS\Request::i()->$name = implode( ',', $existingTags );
|
||||
\IPS\Request::i()->$name = implode( "\n", $existingTags );
|
||||
|
||||
if( isset( $existingTags['prefix'] ) )
|
||||
{
|
||||
@@ -577,7 +637,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
if ( !$item->canEdit() )
|
||||
if ( !$item->canEditTitle() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
@@ -665,7 +725,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->breadcrumb[] = array( $link ? $item->url() : NULL, $item->mapped( 'title' ) );
|
||||
|
||||
$title = ( isset( \IPS\Request::i()->page ) and \IPS\Request::i()->page > 1 ) ? \IPS\Member::loggedIn()->language()->addToStack( 'title_with_page_number', FALSE, array( 'sprintf' => array( $item->mapped( 'title' ), \IPS\Request::i()->page ) ) ) : $item->mapped( 'title' );
|
||||
\IPS\Output::i()->title = $container ? ( $title . ' - ' . $container->_stripTagsTitle ) : $title;
|
||||
\IPS\Output::i()->title = $container ? ( $title . ' - ' . $container->_title ) : $title;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -686,7 +746,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
if ( !$poll->canClose() )
|
||||
{
|
||||
\IPS\Output::i()->error( 'no_permission', '2S136/Z', 403, '' );
|
||||
\IPS\Output::i()->error( 'no_module_permission', '2S136/Z', 403, '' );
|
||||
}
|
||||
|
||||
$newStatus = ( \IPS\Request::i()->value == 1 ? 0 : 1 );
|
||||
@@ -777,7 +837,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
catch( \InvalidArgumentException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'mod_error_invalid_action', '3S136/1A', 403, 'mod_error_invalid_action_admin' );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/1', 404, '' );
|
||||
}
|
||||
@@ -812,7 +876,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $node->can( 'add' ) and $item::modPermission( 'move', \IPS\Member::loggedIn(), $node ) )
|
||||
/* If the item is in a club, only allow moving to other clubs that you moderate */
|
||||
if ( \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Content\ClubContainer' ) and $item->container()->club() )
|
||||
{
|
||||
return $item::modPermission( 'move', \IPS\Member::loggedIn(), $node ) and $node->can( 'add' ) ;
|
||||
}
|
||||
|
||||
if ( $node->can( 'add' ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -841,8 +911,21 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_move_invalid', '1S136/L', 403, '' );
|
||||
}
|
||||
|
||||
|
||||
/* If this item is read, we need to re-mark it as such after moving */
|
||||
if( $item instanceof \IPS\Content\ReadMarkers )
|
||||
{
|
||||
$unread = $item->unread();
|
||||
}
|
||||
|
||||
$item->move( $values['move_to'], isset( $values['move_keep_link'] ) ? $values['move_keep_link'] : FALSE );
|
||||
|
||||
/* Mark it as read */
|
||||
if( $item instanceof \IPS\Content\ReadMarkers and $unread == 0 )
|
||||
{
|
||||
$item->markRead();
|
||||
}
|
||||
|
||||
\IPS\Session::i()->modLog( 'modlog__action_move', array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped( 'title' ) ?: ( method_exists( $item, 'item' ) ? $item->item()->mapped( 'title' ) : NULL ) => FALSE ), $item );
|
||||
|
||||
\IPS\Output::i()->redirect( ( isset( \IPS\Request::i()->returnto ) AND \IPS\Request::i()->returnto == 'item' ) ? $item->url() : $container->url() );
|
||||
@@ -874,43 +957,9 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
$form = new \IPS\Helpers\Form( 'form', 'merge' );
|
||||
$form->class = 'ipsForm_vertical';
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'merge_with', NULL, TRUE, array(), function ( $val ) use ( $class, $item )
|
||||
{
|
||||
/* Load it */
|
||||
try
|
||||
{
|
||||
$toMerge = $class::loadFromUrl( $val );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_bad_item', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $class::$title, FALSE, array( 'strtolower' => TRUE ) ) ) ) ) );
|
||||
}
|
||||
|
||||
/* Make sure it isn't the same */
|
||||
if ( $toMerge == $item )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'cannot_merge_with_self' ) );
|
||||
}
|
||||
|
||||
/* And that we have permission */
|
||||
if ( !$toMerge->canMerge() )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'no_merge_permission', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $class::$title, FALSE, array( 'strtolower' => TRUE ) ) ) ) ) );
|
||||
}
|
||||
|
||||
} ) );
|
||||
\IPS\Member::loggedIn()->language()->words['merge_with_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'merge_with__desc', FALSE, array( 'sprintf' => array( $item->indefiniteArticle(), $item->mapped( 'title' ) ) ) );
|
||||
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Checkbox( 'move_keep_link' ) );
|
||||
|
||||
if ( \IPS\Settings::i()->topic_redirect_prune )
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['move_keep_link_desc'] = \IPS\Member::loggedIn()->language()->addToStack( '_move_keep_link_desc', FALSE, array( 'pluralize' => array( \IPS\Settings::i()->topic_redirect_prune ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
$form = $item->mergeForm();
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->mergeIn( array( $class::loadFromUrl( $values['merge_with'] ) ), isset( $values['move_keep_link'] ) ? $values['move_keep_link'] : FALSE );
|
||||
@@ -921,7 +970,6 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'merge_item', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $class::$title ) ) ) );
|
||||
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
@@ -1021,6 +1069,46 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a report
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteReport()
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
/* Make sure the user confirmed the deletion */
|
||||
\IPS\Request::i()->confirmedDelete();
|
||||
|
||||
try
|
||||
{
|
||||
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'id=? AND report_by=? AND date_reported > ?', \IPS\Request::i()->cid, \IPS\Member::loggedIn()->member_id, time() - ( \IPS\Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'automoderation_cannot_find_report', '2S136/1G', 404, '' );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$index = \IPS\core\Reports\Report::load( $report['rid'] );
|
||||
}
|
||||
catch( \OutofRangeException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'automoderation_cannot_find_report', '2S136/1H', 404, '' );
|
||||
}
|
||||
|
||||
$class = $index->class;
|
||||
|
||||
\IPS\Db::i()->delete( 'core_rc_reports', array( 'id=?', \IPS\Request::i()->cid ) );
|
||||
|
||||
/* Recalculate, we may have dropped below the threshold needed to hide a thing */
|
||||
$index->runAutomaticModeration();
|
||||
|
||||
\IPS\Output::i()->redirect( $class::load( $index->content_id )->url(), 'automoderation_deleted' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Report Item
|
||||
*
|
||||
@@ -1037,23 +1125,48 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
/* Permission check */
|
||||
$canReport = $item->canReport();
|
||||
if ( $canReport !== TRUE )
|
||||
if ( $canReport !== TRUE AND !( $canReport == 'report_err_already_reported' AND \IPS\Settings::i()->automoderation_enabled ) )
|
||||
{
|
||||
\IPS\Output::i()->error( $canReport, '2S136/6', 404, '' );
|
||||
\IPS\Output::i()->error( $canReport, '2S136/6', 403, '' );
|
||||
}
|
||||
|
||||
/* Show form */
|
||||
$form = new \IPS\Helpers\Form( NULL, 'report_submit' );
|
||||
$form->class = 'ipsForm_vertical';
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
|
||||
/* As we group by user id to determine if max points have been reached, guests cannot contribute to counts */
|
||||
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->automoderation_enabled )
|
||||
{
|
||||
/* Has this member already reported this in the past 24 hours */
|
||||
try
|
||||
{
|
||||
$index = \IPS\core\Reports\Report::loadByClassAndId( get_class( $item ), $item->$idColumn );
|
||||
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? and report_by=? and date_reported > ?', $index->id, \IPS\Member::loggedIn()->member_id, time() - ( \IPS\Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
|
||||
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system', 'core' )->reportedAlready( $index, $report, $item );
|
||||
return;
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
|
||||
$options = array( \IPS\core\Reports\Report::TYPE_MESSAGE => \IPS\Member::loggedIn()->language()->addToStack('report_message_item') );
|
||||
foreach( \IPS\core\Reports\Types::roots() as $type )
|
||||
{
|
||||
$options[ $type->id ] = $type->_title;
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'report_type', NULL, FALSE, array( 'options' => $options ) ) );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'report_message', NULL, FALSE, array( 'app' => 'core', 'key' => 'Reports', 'autoSaveKey' => "report-{$class::$application}-{$class::$module}-{$item->$idColumn}", 'minimize' => 'report_message_placeholder' ) ) );
|
||||
|
||||
if( !\IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Captcha );
|
||||
}
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$report = $item->report( $values['report_message'] );
|
||||
$report = $item->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0 );
|
||||
\IPS\File::claimAttachments( "report-{$class::$application}-{$class::$module}-{$item->$idColumn}", $report->id );
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
@@ -1234,6 +1347,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$url = $url->setQueryString( 'reaction', $activeTab );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->metaTags['robots'] = 'noindex';
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->changed ) )
|
||||
{
|
||||
@@ -1263,7 +1378,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
if( !\IPS\Member::loggedIn()->modPermission( 'can_view_moderation_log' ) )
|
||||
{
|
||||
throw new \DomainException;
|
||||
\IPS\Output::i()->error( 'no_module_permission', '2S136/1F', 403, '' );
|
||||
}
|
||||
|
||||
try
|
||||
@@ -1408,7 +1523,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( $comment !== NULL )
|
||||
{
|
||||
\IPS\Output::i()->redirect( $comment->url() );
|
||||
$this->_find( get_class( $comment ), $comment, $item );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1470,7 +1585,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( $comment !== NULL )
|
||||
{
|
||||
\IPS\Output::i()->redirect( $comment->url() );
|
||||
$this->_find( get_class( $comment ), $comment, $item );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1551,6 +1666,108 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/9', 404, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the author of a content item to reply to a review
|
||||
*
|
||||
* @param bool $editing TRUE if we are editing our response
|
||||
* @return void
|
||||
*/
|
||||
protected function _respond( $editing=FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Init */
|
||||
$class = static::$contentModel;
|
||||
$reviewClass = $class::$reviewClass;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$review = $reviewClass::loadAndCheckPerms( \IPS\Request::i()->review );
|
||||
|
||||
/* Are we allowed to respond? */
|
||||
if( $editing === TRUE )
|
||||
{
|
||||
if ( !$review->canEditResponse() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$review->canRespond() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'reviewResponse', $editing ? $review->mapped('author_response') : NULL, TRUE, array(
|
||||
'app' => 'core',
|
||||
'key' => 'ReviewResponses',
|
||||
'autoSaveKey' => 'reviewResponse-' . $class::$application . '/' . $class::$module . '-' . \IPS\Request::i()->review,
|
||||
'attachIds' => array( \IPS\Request::i()->id, \IPS\Request::i()->review, get_class( $item ) )
|
||||
) ) );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$review->author_response = $values['reviewResponse'];
|
||||
$review->save();
|
||||
|
||||
\IPS\Output::i()->redirect( $review->url() );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->metaTags['robots'] = 'noindex';
|
||||
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
}
|
||||
catch ( \LogicException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/1I', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow the author of a content item to edit their review response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _editResponse()
|
||||
{
|
||||
return $this->_respond( TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a review response
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _deleteResponse()
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
try
|
||||
{
|
||||
/* Init */
|
||||
$class = static::$contentModel;
|
||||
$reviewClass = $class::$reviewClass;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$review = $reviewClass::loadAndCheckPerms( \IPS\Request::i()->review );
|
||||
|
||||
/* Are we allowed to delete responses? */
|
||||
if ( !$review->canDeleteResponse() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
$review->author_response = NULL;
|
||||
$review->save();
|
||||
|
||||
\IPS\Output::i()->redirect( $review->url() );
|
||||
}
|
||||
catch ( \LogicException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/1J', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Message Form
|
||||
@@ -1560,14 +1777,27 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function messageForm()
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$current = NULL;
|
||||
|
||||
$metaData = $item->getMeta();
|
||||
if ( isset( \IPS\Request::i()->meta_id ) AND isset( $metaData['core_ContentMessages'][ \IPS\Request::i()->meta_id ] ) )
|
||||
try
|
||||
{
|
||||
$current = $metaData['core_ContentMessages'][ \IPS\Request::i()->meta_id ];
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
|
||||
$current = NULL;
|
||||
$metaData = $item->getMeta();
|
||||
if ( isset( \IPS\Request::i()->meta_id ) AND isset( $metaData['core_ContentMessages'][ \IPS\Request::i()->meta_id ] ) )
|
||||
{
|
||||
$current = $metaData['core_ContentMessages'][ \IPS\Request::i()->meta_id ];
|
||||
}
|
||||
|
||||
if ( !$item->canOnMessage( $current ? 'edit' : 'add' ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/1D', 404, '' );
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'message', $current ? $current['message'] : NULL , TRUE, array( 'app' => 'core', 'key' => 'Meta', 'autoSaveKey' => $current ? "meta-message-" . \IPS\Request::i()->meta_id : "meta-message-new", 'attachIds' => $current ? array( \IPS\Request::i()->meta_id, NULL, 'message' ) : NULL ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Custom( 'message_color', isset( $current['color'] ) ? $current['color'] : 'none', FALSE, array( 'getHtml' => function( $element )
|
||||
@@ -1613,8 +1843,19 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
try
|
||||
{
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
if ( !$item->canOnMessage('delete') )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Output::i()->error( 'node_error', '2S136/1E', 404, '' );
|
||||
}
|
||||
|
||||
$item->deleteMessage( \IPS\Request::i()->meta_id );
|
||||
|
||||
@@ -1897,9 +2138,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$form = new \IPS\Helpers\Form( 'form', false );
|
||||
$form->class = 'ipsForm_vertical';
|
||||
|
||||
if ( in_array( 'IPS\Content\Review', class_parents( $comment ) ) )
|
||||
{
|
||||
$ratingField = $commentClass::$databaseColumnMap['rating'];
|
||||
$form->add( new \IPS\Helpers\Form\Rating( 'rating_value', $comment->$ratingField, TRUE, array( 'max' => \IPS\Settings::i()->reviews_rating_out_of ) ) );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'comment_value', $comment->$valueField, TRUE, array(
|
||||
'app' => $class::$application,
|
||||
'key' => ucfirst( $class::$module ),
|
||||
'key' => mb_ucfirst( $class::$module ),
|
||||
'autoSaveKey' => 'editComment-' . $class::$application . '/' . $class::$module . '-' . $comment->$idField,
|
||||
'attachIds' => $comment->attachmentIds()
|
||||
) ) );
|
||||
@@ -1979,11 +2227,25 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
/* Do it */
|
||||
$comment->editContents( $values['comment_value'] );
|
||||
|
||||
/* Edit rating */
|
||||
$reloadPage = false;
|
||||
if ( isset( $values['rating_value'] ) and in_array( 'IPS\Content\Review', class_parents( $comment ) ) )
|
||||
{
|
||||
/* The star rating changes but is outside of JS scope to change when editing a comment */
|
||||
$ratingField = $comment::$databaseColumnMap['rating'];
|
||||
if ( $comment->$ratingField != $values['rating_value'] )
|
||||
{
|
||||
$reloadPage = true;
|
||||
}
|
||||
|
||||
$comment->editRating( $values['rating_value'] );
|
||||
}
|
||||
|
||||
/* Moderator log */
|
||||
\IPS\Session::i()->modLog( 'modlog__comment_edit', array( $comment->url()->__toString() => FALSE, $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped( 'title' ) => FALSE ), $item );
|
||||
|
||||
/* If this is an AJAX request and the comment hidden status has not changed just output the comment HTML */
|
||||
if ( \IPS\Request::i()->isAjax() AND $isHidden == $comment->hidden() )
|
||||
if ( \IPS\Request::i()->isAjax() AND $isHidden == $comment->hidden() AND $reloadPage === false )
|
||||
{
|
||||
\IPS\Output::i()->output = $comment->html();
|
||||
return;
|
||||
@@ -2083,7 +2345,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \UnderflowException;
|
||||
\IPS\Output::i()->error( 'node_noperm_delete', '3S136/1L', 403, '' );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2147,7 +2409,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \UnexpectedValueException;
|
||||
\IPS\Output::i()->error( 'node_noperm_delete', '3S136/1K', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2340,9 +2602,9 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
/* Permission check */
|
||||
$canReport = $comment->canReport();
|
||||
if ( $canReport !== TRUE )
|
||||
if ( $canReport !== TRUE AND !( $canReport == 'report_err_already_reported' AND \IPS\Settings::i()->automoderation_enabled ) )
|
||||
{
|
||||
\IPS\Output::i()->error( $canReport, '2S136/4', 404, '' );
|
||||
\IPS\Output::i()->error( $canReport, '2S136/4', 403, '' );
|
||||
}
|
||||
|
||||
/* Show form */
|
||||
@@ -2350,6 +2612,30 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$form->class = 'ipsForm_vertical';
|
||||
$itemIdColumn = $class::$databaseColumnId;
|
||||
$idColumn = $comment::$databaseColumnId;
|
||||
|
||||
/* As we group by user id to determine if max points have been reached, guests cannot contribute to counts */
|
||||
if ( \IPS\Member::loggedIn()->member_id and \IPS\Settings::i()->automoderation_enabled )
|
||||
{
|
||||
/* Has this member already reported this in the past 24 hours */
|
||||
try
|
||||
{
|
||||
$index = \IPS\core\Reports\Report::loadByClassAndId( get_class( $comment ), $comment->$idColumn );
|
||||
$report = \IPS\Db::i()->select( '*', 'core_rc_reports', array( 'rid=? and report_by=? and date_reported > ?', $index->id, \IPS\Member::loggedIn()->member_id, time() - ( \IPS\Settings::i()->automoderation_report_again_mins * 60 ) ) )->first();
|
||||
|
||||
\IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system', 'core' )->reportedAlready( $index, $report, $comment );
|
||||
return;
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
|
||||
$options = array( \IPS\core\Reports\Report::TYPE_MESSAGE => \IPS\Member::loggedIn()->language()->addToStack('report_message_comment') );
|
||||
foreach( \IPS\core\Reports\Types::roots() as $type )
|
||||
{
|
||||
$options[ $type->id ] = $type->_title;
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'report_type', NULL, FALSE, array( 'options' => $options ) ) );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'report_message', NULL, FALSE, array( 'app' => 'core', 'key' => 'Reports', 'autoSaveKey' => "report-{$class::$application}-{$class::$module}-{$item->$itemIdColumn}-{$comment->$idColumn}", 'minimize' => 'report_message_placeholder' ) ) );
|
||||
if( !\IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
@@ -2358,7 +2644,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$report = $comment->report( $values['report_message'] );
|
||||
$report = $comment->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0 );
|
||||
\IPS\File::claimAttachments( "report-{$class::$application}-{$class::$module}-{$item->$itemIdColumn}-{$comment->$idColumn}", $report->id );
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
@@ -2438,6 +2724,10 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function _feature( $commentClass, $comment, $item )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
if ( !$item->canFeatureComment() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2548,6 +2838,10 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function _unfeature( $commentClass, $comment, $item )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
if ( !$item->canUnfeatureComment() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -2625,6 +2919,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$url = $url->setQueryString( 'reaction', $activeTab );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->metaTags['robots'] = 'noindex';
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() AND isset( \IPS\Request::i()->changed ) )
|
||||
{
|
||||
@@ -2647,6 +2943,19 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function _multimod( $commentClass, $item )
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
$checkAgainst = \IPS\Request::i()->modaction;
|
||||
|
||||
if( $checkAgainst == 'split' OR $checkAgainst == 'merge' )
|
||||
{
|
||||
$checkAgainst = 'split_merge';
|
||||
}
|
||||
|
||||
if ( !$item::modPermission( $checkAgainst, NULL, $item->containerWrapper() ) )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
|
||||
if ( \IPS\Request::i()->modaction == 'split' )
|
||||
{
|
||||
$form = $this->_splitForm( $item );
|
||||
@@ -2678,6 +2987,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$firstComment = $comment;
|
||||
break;
|
||||
}
|
||||
|
||||
/* If we don't have a $firstComment, something went wrong - perhaps the input multimod comment ids are all from a different topic for instance */
|
||||
if( !isset( $firstComment ) )
|
||||
{
|
||||
$form->error = \IPS\Member::loggedIn()->language()->addToStack( 'mod_error_invalid_action' );
|
||||
goto splitFormError;
|
||||
}
|
||||
|
||||
if ( isset( $values['_split_type'] ) and $values['_split_type'] === 'new' )
|
||||
{
|
||||
@@ -2770,6 +3086,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Label for goto command */
|
||||
splitFormError:
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( call_user_func_array( array( \IPS\Theme::i(), 'getTemplate' ), array( 'forms', 'core' ) ), 'popupTemplate' ) );
|
||||
|
||||
@@ -2830,7 +3148,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Db::i()->update( 'core_attachments_map', array(
|
||||
'id1' => $item->$idColumn,
|
||||
'id2' => $firstComment->$commentIdColumn,
|
||||
), array( 'location_key=? AND id1=? AND id2=?', (string) $item::$application . '_' . ucfirst( $item::$module ), $item->$idColumn, $comment->$commentIdColumn ) );
|
||||
), array( 'location_key=? AND id1=? AND id2=?', (string) $item::$application . '_' . mb_ucfirst( $item::$module ), $item->$idColumn, $comment->$commentIdColumn ) );
|
||||
|
||||
/* Merge likes */
|
||||
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
|
||||
@@ -2843,6 +3161,12 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
catch( \Exception $e ) {}
|
||||
}
|
||||
|
||||
/* Fix duplicated reactions */
|
||||
if ( \IPS\IPS::classUsesTrait( $commentClass, 'IPS\Content\Reactable' ) )
|
||||
{
|
||||
\IPS\Db::i()->query( 'DELETE row1 FROM ' . \IPS\Db::i()->prefix . 'core_reputation_index row1, ' . \IPS\Db::i()->prefix . 'core_reputation_index row2 WHERE row1.id > row2.id AND row1.member_id = row2.member_id AND row1.app = \'' . $item::$application . '\' AND row1.type = \'' . $comment::reactionType() . '\' AND row1.type_id = row2.type_id' );
|
||||
}
|
||||
|
||||
$item->rebuildFirstAndLastCommentData();
|
||||
|
||||
/* Log it */
|
||||
@@ -2999,7 +3323,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
if ( $item::canCreate( \IPS\Member::loggedIn(), $container ) )
|
||||
if ( $item::canCreate( \IPS\Member::loggedIn() ) )
|
||||
{
|
||||
$toAdd = array();
|
||||
$toggles = array();
|
||||
@@ -3008,7 +3332,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
if ( !in_array( $k, array( 'poll', 'content', 'comment_edit_reason', 'comment_log_edit' ) ) )
|
||||
{
|
||||
if ( $k === 'container' )
|
||||
if ( $k === 'container' AND ( $container AND $container->can( 'add' ) ) )
|
||||
{
|
||||
$field->defaultValue = $container;
|
||||
if ( !$field->value )
|
||||
@@ -3037,23 +3361,61 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
foreach ( $toAdd as $field )
|
||||
{
|
||||
if ( $field->name == $item::$formLangPrefix . 'container' )
|
||||
{
|
||||
/* Add a custom permission check for splitting comments */
|
||||
$field->options['permissionCheck'] = function( $node ) use ( $item )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* If the item is in a club, only allow moving to other clubs that you moderate */
|
||||
if ( \IPS\IPS::classUsesTrait( $item->container(), 'IPS\Content\ClubContainer' ) and $item->container()->club() )
|
||||
{
|
||||
return $item::modPermission( 'move', \IPS\Member::loggedIn(), $node ) and $node->can( 'add' ) ;
|
||||
}
|
||||
|
||||
if ( $node->can( 'add' ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch( \OutOfBoundsException $e ) { }
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
$form->add( $field );
|
||||
}
|
||||
}
|
||||
$form->add( new \IPS\Helpers\Form\Url( '_split_into_url', NULL, FALSE, array(), function ( $val ) use ( $item )
|
||||
{
|
||||
try
|
||||
if ( \IPS\Request::i()->_split_type == 'existing' OR !isset( \IPS\Request::i()->_split_type ) )
|
||||
{
|
||||
if ( $val )
|
||||
try
|
||||
{
|
||||
$item::loadFromUrl( $val );
|
||||
/* Validate the URL */
|
||||
$test = $item::loadFromUrl( $val );
|
||||
|
||||
/* Make sure the URL matches the content type we're splitting */
|
||||
foreach( array( 'app', 'module', 'controller') as $index )
|
||||
{
|
||||
if( $test->url()->hiddenQueryString[ $index ] != $val->hiddenQueryString[ $index ] )
|
||||
{
|
||||
throw new \InvalidArgumentException;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_bad_item', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( '__defart_' . $item::$title ) ) ) ) );
|
||||
}
|
||||
|
||||
if( !$test->canMerge() )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'no_merge_permission', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( '__defart_' . $item::$title ) ) ) ) );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_bad_item', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $item::$title ) ) ) ) );
|
||||
}
|
||||
|
||||
}, NULL, NULL, 'split_into_url' ) );
|
||||
|
||||
return $form;
|
||||
@@ -3079,7 +3441,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
|
||||
if( $item->tags() === NULL AND $item->prefix( FALSE ) === NULL )
|
||||
if( !$item instanceof \IPS\Content\Tags OR ( $item->tags() === NULL AND $item->prefix( FALSE ) === NULL ) )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,5 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
interface Featurable
|
||||
{
|
||||
|
||||
}
|
||||
+478
-208
File diff suppressed because it is too large.
Load diff
+191
-109
@@ -33,13 +33,14 @@ class _Facebook extends PromoteAbstract
|
||||
public $defaultSettings = array(
|
||||
'token' => NULL,
|
||||
'owner' => NULL,
|
||||
'page_name' => NULL,
|
||||
'page_name' => NULL, //
|
||||
'permissions' => NULL,
|
||||
'members' => NULL,
|
||||
'page' => NULL,
|
||||
'page' => NULL, //
|
||||
'tags' => NULL,
|
||||
'image' => NULL,
|
||||
'last_sync' => 0
|
||||
'last_sync' => 0,
|
||||
'shareable' => array()
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ class _Facebook extends PromoteAbstract
|
||||
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
|
||||
{
|
||||
/* Fetch again */
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large" )->request()->get();
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large&appsecret_proof=" . static::appSecretProof( $this->settings['token'] ) )->request()->get();
|
||||
|
||||
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
|
||||
$newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
|
||||
@@ -76,11 +77,23 @@ class _Facebook extends PromoteAbstract
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName( $serviceId=NULL )
|
||||
{
|
||||
return $this->settings['page_name'];
|
||||
if ( $serviceId )
|
||||
{
|
||||
$realId = mb_substr( $serviceId, 2 );
|
||||
if ( mb_substr( $serviceId, 0, 2 ) == 'p_' )
|
||||
{
|
||||
return isset( $this->settings['shareable']['pages'][ $realId ]['name'] ) ? $this->settings['shareable']['pages'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
else
|
||||
{
|
||||
return isset( $this->settings['shareable']['groups'][ $realId ]['name'] ) ? $this->settings['shareable']['groups'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,65 +103,38 @@ class _Facebook extends PromoteAbstract
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/permissions" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
return $response['error']['message'];
|
||||
}
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
$got = 0;
|
||||
foreach( $response['data'] as $permission )
|
||||
{
|
||||
if ( in_array( $permission['permission'], array( 'manage_pages', 'publish_pages' ) ) and $permission['status'] == 'granted' )
|
||||
{
|
||||
$got++;
|
||||
}
|
||||
}
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
|
||||
$authorizedScopes = $facebook->authorizedScopes( $this->member );
|
||||
|
||||
return $got === 2;
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
if( $authorizedScopes === NULL )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
foreach( array( 'user_managed_groups', 'manage_pages', 'publish_pages', 'publish_actions' ) as $perm )
|
||||
{
|
||||
if ( ! in_array( $perm, $authorizedScopes ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pages this user manages
|
||||
* Create the appSecretProof key
|
||||
*
|
||||
* @return array
|
||||
* @param string $token The Token
|
||||
* @return string
|
||||
*/
|
||||
public function getPages()
|
||||
protected static function appSecretProof( $token )
|
||||
{
|
||||
$pages = array();
|
||||
try
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/{$this->member->fb_uid}/accounts" )->setQueryString( 'access_token', $this->member->fb_token )->request()->get()->decodeJson();
|
||||
|
||||
if ( !empty( $response['data'] ) )
|
||||
{
|
||||
foreach( $response['data'] as $page )
|
||||
{
|
||||
$pages[ $page['id'] ] = array( $page['name'], $page['access_token'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
return $pages;
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
return hash_hmac( 'sha256', $token, $facebook->settings['client_secret'] );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
@@ -160,33 +146,63 @@ class _Facebook extends PromoteAbstract
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
|
||||
$shareable = array();
|
||||
$_text = '';
|
||||
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
$_text = $text;
|
||||
|
||||
if ( isset( $this->promote->form_data['facebook'] ) AND $settings = $this->promote->form_data['facebook'] )
|
||||
{
|
||||
$shareable = $settings['shareable'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$_text = $text;
|
||||
|
||||
if ( $content )
|
||||
{
|
||||
$_text .= "\n" . $content;
|
||||
}
|
||||
|
||||
if ( $link )
|
||||
{
|
||||
$_text .= "\n" . $link;
|
||||
}
|
||||
|
||||
if ( count( $this->settings['tags'] ) )
|
||||
{
|
||||
$_text .= "\n" . '#' . implode( ' #', $this->settings['tags'] );
|
||||
$_text .= '#' . implode( ' #', $this->settings['tags'] );
|
||||
}
|
||||
}
|
||||
|
||||
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ) );
|
||||
$names = array();
|
||||
$options = array();
|
||||
if ( isset( $this->settings['shareable'] ) )
|
||||
{
|
||||
foreach( $this->settings['shareable']['pages'] as $id => $data )
|
||||
{
|
||||
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
|
||||
foreach( $this->settings['shareable']['groups'] as $id => $data )
|
||||
{
|
||||
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', NULL, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ),
|
||||
new \IPS\Helpers\Form\CheckboxSet( 'promote_facebook_shareable', $shareable, FALSE, array( 'options' => $options ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow for any extra processing
|
||||
*
|
||||
* @param array $values Values from the form isn't it though
|
||||
* @return mixed
|
||||
*/
|
||||
public function processPromoteForm( $values )
|
||||
{
|
||||
if ( isset( $values['promote_facebook_shareable'] ) )
|
||||
{
|
||||
return array( 'shareable' => $values['promote_facebook_shareable'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,30 +214,71 @@ class _Facebook extends PromoteAbstract
|
||||
public function post( $promote )
|
||||
{
|
||||
$photos = $promote->imageObjects();
|
||||
|
||||
/* Get the last 20 items to see if we have a duplicate */
|
||||
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->setQueryString( array( 'limit' => 20, 'access_token' => $this->settings['token'] ) )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $items['data'] ) )
|
||||
|
||||
$settings = $promote->form_data['facebook'];
|
||||
$shareable = $this->settings['shareable'];
|
||||
$responses = array();
|
||||
|
||||
foreach( $settings['shareable'] as $id )
|
||||
{
|
||||
foreach( $items['data'] as $item )
|
||||
$feedId = mb_substr( $id, 2 );
|
||||
|
||||
if ( mb_substr( $id, 0, 2 ) == 'p_' )
|
||||
{
|
||||
$time = new \IPS\DateTime( $item['created_time'] );
|
||||
$now = new \IPS\DateTime();
|
||||
|
||||
/* Only look back past the last 30 mins */
|
||||
if ( intval( $now->diff( $time )->format('%i') ) > 30 )
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $shareable['pages'][ $feedId ]['token'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $this->settings['token'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the post, but not post post as that would be too late
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @param array $photos The photos
|
||||
* @param int $feedId The feed ID to post to. Honestly, it's not rocket science.
|
||||
* @param string $token I'll give you one guess. Does anyone actually read this?
|
||||
* @return stuff
|
||||
*/
|
||||
protected function _post( $promote, $photos, $feedId, $token=NULL )
|
||||
{
|
||||
/* Get the last 20 items to see if we have a duplicate */
|
||||
try
|
||||
{
|
||||
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->setQueryString( $this->_addToken( array(
|
||||
'limit' => 20
|
||||
), $token ) )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $items['data'] ) )
|
||||
{
|
||||
foreach( $items['data'] as $item )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
|
||||
{
|
||||
/* Duplicate */
|
||||
return $item['id'];
|
||||
$time = new \IPS\DateTime( ( isset( $item['created_time'] ) ? $item['created_time'] : $item['updated_time' ] ) );
|
||||
$now = new \IPS\DateTime();
|
||||
|
||||
/* Only look back past the last 30 mins */
|
||||
if ( intval( $now->diff( $time )->format('%i') ) > 30 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
|
||||
{
|
||||
/* Duplicate */
|
||||
return $item['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
if ( ! $photos or count( $photos ) === 1 )
|
||||
{
|
||||
@@ -231,32 +288,30 @@ class _Facebook extends PromoteAbstract
|
||||
if ( $photos )
|
||||
{
|
||||
$thePhoto = array_pop( $photos );
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/photos" )->request()->post( array(
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ),
|
||||
'access_token' => $this->settings['token'],
|
||||
'link' => $promote->short_link
|
||||
) )->decodeJson();
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/feed" )->request()->post( array(
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'access_token' => $this->settings['token'],
|
||||
'link' => $promote->short_link
|
||||
) )->decodeJson();
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $this->response['id'] ) )
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $this->response['id'];
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -268,48 +323,75 @@ class _Facebook extends PromoteAbstract
|
||||
/* We have multiple photos so we first need to create an album, and then upload into that */
|
||||
try
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/albums" )->request()->post( array(
|
||||
'name' => $promote->objectTitle,
|
||||
'access_token' => $this->settings['token'],
|
||||
) )->decodeJson();
|
||||
/* The item may be a comment, which uses a language string for objectTitle - make sure that is parsed before sending */
|
||||
$title = $promote->objectTitle;
|
||||
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $title );
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/albums" )->request()->post( $this->_addToken( array(
|
||||
'name' => $title,
|
||||
), $token ) )->decodeJson();
|
||||
|
||||
|
||||
if ( ! isset( $this->response['id'] ) )
|
||||
if ( ! isset( $response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException('Could not create album');
|
||||
}
|
||||
|
||||
$newAlbumId = $this->response['id'];
|
||||
$newAlbumId = $response['id'];
|
||||
|
||||
foreach( $photos as $photo )
|
||||
{
|
||||
$this->response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( array(
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $photo->url ),
|
||||
'access_token' => $this->settings['token'],
|
||||
) )->decodeJson();
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
return $this->response['id'];
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds 'access_token' to query string if token defined (it is for pages, but not for groups
|
||||
*
|
||||
* @param array $values The values of the array
|
||||
* @param string|NULL $token The token obvs
|
||||
* @return array
|
||||
*/
|
||||
protected function _addToken( $values, $token )
|
||||
{
|
||||
return ( $token ) ? array_merge( $values, array( 'access_token' => $token, 'appsecret_proof' => static::appSecretProof( $token ) ) ) : $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws InvalidArgumentException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
if ( is_array( $data ) and count( $data ) )
|
||||
{
|
||||
/* Get a random URL from the many it was shared to */
|
||||
$array = $data[ array_rand( $data ) ];
|
||||
$data = array_pop( $array );
|
||||
}
|
||||
|
||||
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( 'https://facebook.com/' . $data );
|
||||
|
||||
@@ -45,9 +45,10 @@ class _Internal extends PromoteAbstract
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName( $serviceId=NULL )
|
||||
{
|
||||
return \IPS\Settings::i()->board_name;
|
||||
}
|
||||
@@ -63,10 +64,37 @@ class _Internal extends PromoteAbstract
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
return array( new \IPS\Helpers\Form\TextArea( 'promote_social_content_internal', $content ?: $text, FALSE, array( 'maxLength' => 3000, 'rows' => 6 ) ) );
|
||||
$title = NULL;
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
if ( isset( $this->promote->form_data['internal'] ) AND $settings = $this->promote->form_data['internal'] )
|
||||
{
|
||||
$title = $settings['title'];
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
new \IPS\Helpers\Form\Text( 'promote_social_title_internal', $title ),
|
||||
new \IPS\Helpers\Form\TextArea( 'promote_social_content_internal', $content ?: $text, FALSE, array( 'maxLength' => 3000, 'rows' => 6 ) )
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow for any extra processing
|
||||
*
|
||||
* @param array $values Values from the form isn't it though
|
||||
* @return mixed
|
||||
*/
|
||||
public function processPromoteForm( $values )
|
||||
{
|
||||
if ( isset( $values['promote_social_title_internal'] ) )
|
||||
{
|
||||
return array( 'title' => $values['promote_social_title_internal'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to internal
|
||||
*
|
||||
|
||||
@@ -77,7 +77,7 @@ abstract class _PromoteAbstract extends \IPS\Patterns\ActiveRecord
|
||||
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
|
||||
{
|
||||
/* Initiate an object */
|
||||
$classname = '\\IPS\Content\\Promote\\' . \ucfirst( $data['sharer_key'] );
|
||||
$classname = '\\IPS\Content\\Promote\\' . mb_ucfirst( $data['sharer_key'] );
|
||||
|
||||
if ( !class_exists( $classname ) )
|
||||
{
|
||||
@@ -162,9 +162,10 @@ abstract class _PromoteAbstract extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getName();
|
||||
abstract public function getName( $serviceId=NULL );
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
@@ -174,7 +175,18 @@ abstract class _PromoteAbstract extends \IPS\Patterns\ActiveRecord
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
abstract function getUrl( $data );
|
||||
|
||||
|
||||
/**
|
||||
* Allow for any extra processing
|
||||
*
|
||||
* @param array $values Values from the form isn't it though
|
||||
* @return mixed
|
||||
*/
|
||||
public function processPromoteForm( $values )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk save settings
|
||||
*
|
||||
|
||||
@@ -58,7 +58,7 @@ class _Twitter extends PromoteAbstract
|
||||
{
|
||||
if ( static::$twitter === NULL )
|
||||
{
|
||||
static::$twitter = \IPS\Login::getHandler('Twitter');
|
||||
static::$twitter = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth1\Twitter');
|
||||
}
|
||||
|
||||
return static::$twitter;
|
||||
@@ -90,9 +90,10 @@ class _Twitter extends PromoteAbstract
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
public function getName( $serviceId=NULL )
|
||||
{
|
||||
return $this->settings['name'];
|
||||
}
|
||||
@@ -104,15 +105,8 @@ class _Twitter extends PromoteAbstract
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->twitter()->sendRequest( 'get', 'https://api.twitter.com/1.1/account/verify_credentials.json', array(), $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e ) { }
|
||||
|
||||
return FALSE;
|
||||
$twitter = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth1\Twitter');
|
||||
return $twitter->hasWritePermissions( $this->settings['token'], $this->settings['secret'] );
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +128,7 @@ class _Twitter extends PromoteAbstract
|
||||
$textToActuallyUse .= ' ' . $link;
|
||||
}
|
||||
|
||||
if ( mb_strlen( $textToActuallyUse ) < 140 )
|
||||
if ( mb_strlen( $textToActuallyUse ) < 280 )
|
||||
{
|
||||
/* Got any tags to add? */
|
||||
if ( count( $this->settings['tags'] ) )
|
||||
@@ -142,7 +136,7 @@ class _Twitter extends PromoteAbstract
|
||||
if ( $this->settings['tags_method'] === 'trim' )
|
||||
{
|
||||
$urlLength = mb_strlen( $link ) + 2; // spaces either side
|
||||
$left = 140 - $urlLength;
|
||||
$left = 280 - $urlLength;
|
||||
$tagLength = 0;
|
||||
$tagString = '';
|
||||
$useText = $text;
|
||||
@@ -168,7 +162,7 @@ class _Twitter extends PromoteAbstract
|
||||
{
|
||||
$considerAddingThis = ' #' . $tag;
|
||||
|
||||
if ( mb_strlen( $textToActuallyUse . $considerAddingThis ) < 140 )
|
||||
if ( mb_strlen( $textToActuallyUse . $considerAddingThis ) < 280 )
|
||||
{
|
||||
$textToActuallyUse .= $considerAddingThis;
|
||||
}
|
||||
@@ -185,7 +179,7 @@ class _Twitter extends PromoteAbstract
|
||||
$return[] = \IPS\Theme::i()->getTemplate( 'promote' )->promoteDialogTwitterDuplicate();
|
||||
}
|
||||
|
||||
$return[] = new \IPS\Helpers\Form\TextArea( 'promote_social_content_twitter', $textToActuallyUse, FALSE, array( 'maxLength' => 300, 'rows' => 3 ) );
|
||||
$return[] = new \IPS\Helpers\Form\TextArea( 'promote_social_content_twitter', $textToActuallyUse, FALSE, array( 'maxLength' => 600, 'rows' => 3 ) );
|
||||
|
||||
return $return;
|
||||
}
|
||||
@@ -211,8 +205,8 @@ class _Twitter extends PromoteAbstract
|
||||
/* Twitter can only have a max of 4 images */
|
||||
if ( $done < 4 )
|
||||
{
|
||||
$this->response = $this->twitter()->sendRequest( 'post', 'https://upload.twitter.com/1.1/media/upload.json', array( 'media' => $photo->contents() ), $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
|
||||
$this->response = $this->twitter()->sendMedia( $photo->contents(), $this->settings['token'], $this->settings['secret'] );
|
||||
|
||||
if ( isset( $this->response['media_id_string'] ) )
|
||||
{
|
||||
$mediaIds[] = $this->response['media_id_string'];
|
||||
@@ -241,7 +235,7 @@ class _Twitter extends PromoteAbstract
|
||||
$send['media_ids'] = implode( ',', $mediaIds );
|
||||
}
|
||||
|
||||
$this->response = $this->twitter()->sendRequest( 'post', "https://api.twitter.com/1.1/statuses/update.json", $send, $this->settings['token'], $this->settings['secret'] )->decodeJson();
|
||||
$this->response = $this->twitter()->sendStatus( $send, $this->settings['token'], $this->settings['secret'] );
|
||||
|
||||
if ( isset( $this->response['id_str'] ) )
|
||||
{
|
||||
@@ -249,6 +243,13 @@ class _Twitter extends PromoteAbstract
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Check for specific errors */
|
||||
if ( isset( $this->response['error'] ) )
|
||||
{
|
||||
\IPS\Log::log( $this->response['error'], 'twitter' );
|
||||
throw new \InvalidArgumentException( $this->response['error'] );
|
||||
}
|
||||
|
||||
/* Check for non critical errors we don't need to flag as complete failures */
|
||||
if ( isset( $this->response['errors'] ) )
|
||||
{
|
||||
@@ -272,14 +273,14 @@ class _Twitter extends PromoteAbstract
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e->getMessage(), 'twitter' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+137
-76
@@ -32,6 +32,16 @@ trait Reactable
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction class
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function reactionClass()
|
||||
{
|
||||
return get_called_class();
|
||||
}
|
||||
|
||||
/**
|
||||
* React
|
||||
*
|
||||
@@ -54,9 +64,9 @@ trait Reactable
|
||||
{
|
||||
$owner = $this->owner();
|
||||
}
|
||||
|
||||
|
||||
/* Can we react? */
|
||||
if ( $this->canReact( $member ) === FALSE )
|
||||
if ( !$this->canView( $member ) or !$this->canReact( $member ) or !$reaction->enabled )
|
||||
{
|
||||
throw new \DomainException( 'cannot_react' );
|
||||
}
|
||||
@@ -92,7 +102,10 @@ trait Reactable
|
||||
}
|
||||
|
||||
/* Have we already reacted? */
|
||||
if ( $this->reacted( $member ) )
|
||||
$reacted = $this->reacted( $member );
|
||||
|
||||
/* Remove the initial reaction, if we have reacted */
|
||||
if ( $reacted )
|
||||
{
|
||||
$this->removeReaction( $member );
|
||||
}
|
||||
@@ -107,16 +120,16 @@ trait Reactable
|
||||
'rep_date' => \IPS\DateTime::create()->getTimestamp(),
|
||||
'rep_rating' => $reaction->value,
|
||||
'member_received' => $owner->member_id,
|
||||
'rep_class' => get_class( $this ),
|
||||
'class_type_id_hash' => md5( get_class( $this ) . ':' . $this->$idColumn ),
|
||||
'rep_class' => static::reactionClass(),
|
||||
'class_type_id_hash' => md5( static::reactionClass() . ':' . $this->$idColumn ),
|
||||
'item_id' => $itemId,
|
||||
'reaction' => $reaction->id
|
||||
) );
|
||||
|
||||
/* Send the notification */
|
||||
if ( $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $owner ) )
|
||||
|
||||
/* Send the notification but only if we aren't reacting to our own content, we can view the content, the user isn't ignored and we aren't changing from one reaction to another */
|
||||
if ( $this->author()->member_id AND $this->author() != \IPS\Member::loggedIn() AND $this->canView( $owner ) AND !$reacted AND !$member->isIgnoring( $this->author(), 'posts' ) )
|
||||
{
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, $member ) );
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'new_likes', $this, array( $this, $member ), array(), TRUE, \IPS\Content\Reaction::isLikeMode() ? NULL : 'notification_new_react' );
|
||||
$notification->recipients->attach( $owner );
|
||||
$notification->send();
|
||||
}
|
||||
@@ -126,6 +139,12 @@ trait Reactable
|
||||
$owner->pp_reputation_points += $reaction->value;
|
||||
$owner->save();
|
||||
}
|
||||
|
||||
/* Reset some cached values */
|
||||
$this->_reactionCount = NULL;
|
||||
$this->_reactions = NULL;
|
||||
|
||||
$this->hasReacted[ $member->member_id ] = $reaction;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +162,10 @@ trait Reactable
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$rep = \IPS\Db::i()->select( '*', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first();
|
||||
|
||||
$where = $this->getReactionWhereClause( NULL, FALSE );
|
||||
$where[] = array( 'member_id=?', $member->member_id );
|
||||
$rep = \IPS\Db::i()->select( '*', 'core_reputation_index', $where )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
@@ -165,6 +187,30 @@ trait Reactable
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_reputation_index', array( "id=?", $rep['id'] ) );
|
||||
|
||||
/* Remove Notifications */
|
||||
$memberIds = array();
|
||||
|
||||
foreach( \IPS\Db::i()->select( 'member', 'core_notifications', array( 'notification_key=? AND item_class=? AND item_id=?', 'new_likes', (string) get_class( $this ), (int) $this->$idColumn ) ) as $member )
|
||||
{
|
||||
$memberIds[ $member ] = $member;
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_notifications', array( 'notification_key=? AND item_class=? AND item_id=?', 'new_likes', (string) get_class( $this ), (int) $this->$idColumn ) );
|
||||
|
||||
foreach( $memberIds as $member )
|
||||
{
|
||||
\IPS\Member::load( $member )->recountNotifications();
|
||||
}
|
||||
|
||||
/* Reset some cached values */
|
||||
$this->_reactionCount = NULL;
|
||||
$this->_reactions = NULL;
|
||||
|
||||
if( isset( $this->hasReacted[ $member->member_id ] ) )
|
||||
{
|
||||
unset( $this->hasReacted[ $member->member_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,20 +290,25 @@ trait Reactable
|
||||
|
||||
if ( is_array( $this->reputation ) )
|
||||
{
|
||||
foreach( $this->reputation AS $memberId => $reactionId )
|
||||
if ( $enabledReactions = \IPS\Content\Reaction::enabledReactions() )
|
||||
{
|
||||
try
|
||||
foreach( $this->reputation AS $memberId => $reactionId )
|
||||
{
|
||||
$this->_reactionCount += \IPS\Content\Reaction::load( $reactionId )->value;
|
||||
$this->_reactions[ $memberId ][] = $reactionId;
|
||||
if( isset( $enabledReactions[ $reactionId ] ) )
|
||||
{
|
||||
$this->_reactionCount += $enabledReactions[ $reactionId ]->value;
|
||||
$this->_reactions[ $memberId ][] = $reactionId;
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Set the data in $this->reputation to save queries later */
|
||||
$this->reputation = array();
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
|
||||
{
|
||||
$this->reputation[ $reaction['member_id'] ] = $reaction['reaction'];
|
||||
$this->_reactions[ $reaction['member_id'] ][] = $reaction['reaction'];
|
||||
$this->_reactionCount += $reaction['rep_rating'];
|
||||
}
|
||||
@@ -279,20 +330,30 @@ trait Reactable
|
||||
*/
|
||||
public function reactionCount()
|
||||
{
|
||||
$this->reactions();
|
||||
if( $this->_reactionCount === NULL )
|
||||
{
|
||||
$this->reactions();
|
||||
}
|
||||
|
||||
return $this->_reactionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reaction Where Clause
|
||||
*
|
||||
* @param \IPS\Content\Reaction|array|int|NULL This can be any one of the following: An \IPS\Content\Reaction object, an array of \IPS\Content\Reaction objects, and integer, or an array of integers, or NULL
|
||||
* @param \IPS\Content\Reaction|array|int|NULL $reactions This can be any one of the following: An \IPS\Content\Reaction object, an array of \IPS\Content\Reaction objects, and integer, or an array of integers, or NULL
|
||||
* @param bool $enabledTypesOnly If TRUE, only reactions of the enabled reaction types will be included (must join core_reactions)
|
||||
* @return array
|
||||
*/
|
||||
public function getReactionWhereClause( $reactions = NULL )
|
||||
public function getReactionWhereClause( $reactions = NULL, $enabledTypesOnly=TRUE )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$where = array( array( 'rep_class=? AND type=? AND type_id=? AND reaction_enabled=?', get_class( $this ), static::reactionType(), $this->$idColumn, 1 ) );
|
||||
$where = array( array( 'rep_class=? AND type=? AND type_id=?', static::reactionClass(), static::reactionType(), $this->$idColumn ) );
|
||||
|
||||
if ( $enabledTypesOnly )
|
||||
{
|
||||
$where[] = array( 'reaction_enabled=1' );
|
||||
}
|
||||
|
||||
if ( $reactions !== NULL )
|
||||
{
|
||||
@@ -330,7 +391,7 @@ trait Reactable
|
||||
*/
|
||||
public function reactionTable( $reaction=NULL )
|
||||
{
|
||||
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
|
||||
if ( !\IPS\Member::loggedIn()->group['gbw_view_reps'] or !$this->canView() )
|
||||
{
|
||||
throw new \DomainException;
|
||||
}
|
||||
@@ -358,7 +419,12 @@ trait Reactable
|
||||
|
||||
return $table;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Cached Reacted
|
||||
*/
|
||||
protected $hasReacted = array();
|
||||
|
||||
/**
|
||||
* Has reacted?
|
||||
*
|
||||
@@ -368,30 +434,31 @@ trait Reactable
|
||||
public function reacted( \IPS\Member $member = NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
try
|
||||
|
||||
if( !isset( $this->hasReacted[ $member->member_id ] ) )
|
||||
{
|
||||
if ( is_array( $this->reputation ) )
|
||||
$this->hasReacted[ $member->member_id ] = FALSE;
|
||||
|
||||
try
|
||||
{
|
||||
if ( isset( $this->reputation[ $member->member_id ] ) )
|
||||
if ( is_array( $this->reputation ) )
|
||||
{
|
||||
return \IPS\Content\Reaction::load( $this->reputation[ $member->member_id ] );
|
||||
if ( isset( $this->reputation[ $member->member_id ] ) )
|
||||
{
|
||||
$this->hasReacted[ $member->member_id ] = \IPS\Content\Reaction::load( $this->reputation[ $member->member_id ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return FALSE;
|
||||
$where = $this->getReactionWhereClause( NULL, FALSE );
|
||||
$where[] = array( 'member_id=?', $member->member_id );
|
||||
$this->hasReacted[ $member->member_id ] = \IPS\Content\Reaction::load( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $where )->first() );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return \IPS\Content\Reaction::load( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "member_id=? AND rep_class=? AND type=? AND type_id=?", $member->member_id, get_class( $this ), static::reactionType(), $this->$idColumn ) )->first() );
|
||||
}
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
return FALSE;
|
||||
catch( \UnderflowException $e ){}
|
||||
}
|
||||
|
||||
return $this->hasReacted[ $member->member_id ];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,51 +480,44 @@ trait Reactable
|
||||
if ( count( $this->reactions() ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
|
||||
$app = explode( '\\', get_class( $this ) );
|
||||
if ( \strtolower( $app[1] ) === $app[1] )
|
||||
if ( is_array( $this->reputation ) )
|
||||
{
|
||||
$app = $app[1];
|
||||
foreach( $this->reputation AS $memberId => $reaction )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $reaction ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $reaction ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $reaction ]++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$app = 'core';
|
||||
}
|
||||
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', array( "app=? AND rep_class=? AND type=? AND type_id=?", $app, get_class( $this ), static::reactionType(), $this->$idColumn ) ) AS $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $rep ] ) )
|
||||
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $rep )
|
||||
{
|
||||
$this->reactBlurb[ $rep ] = 0;
|
||||
if ( !isset( $this->reactBlurb[ $rep ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $rep ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $rep ]++;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $rep ]++;
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
@uksort( $this->reactBlurb, function( $a, $b ) {
|
||||
try
|
||||
{
|
||||
$a = \IPS\Content\Reaction::load( $a );
|
||||
$b = \IPS\Content\Reaction::load( $b );
|
||||
}
|
||||
/* One of the reactions does not exist */
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
|
||||
@uksort( $this->reactBlurb, function( $a, $b ) use( $enabledReactions ) {
|
||||
$positionA = $enabledReactions[ $a ]->position;
|
||||
$positionB = $enabledReactions[ $b ]->position;
|
||||
|
||||
if ( $a->position < $b->position )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
elseif ( $a->position == $b->position )
|
||||
if ( $positionA == $positionB )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ( $positionA < $positionB ) ? -1 : 1;
|
||||
} );
|
||||
}
|
||||
else
|
||||
@@ -495,9 +555,9 @@ trait Reactable
|
||||
}
|
||||
|
||||
/* Did anyone like it? */
|
||||
$numberOfLikes = $this->reactionCount(); # int
|
||||
$numberOfLikes = count( $this->reactions() ); # int
|
||||
if ( $numberOfLikes )
|
||||
{
|
||||
{
|
||||
/* Is it just us? */
|
||||
$userLiked = ( $this->reacted() );
|
||||
if ( $userLiked and $numberOfLikes < 2 )
|
||||
@@ -521,11 +581,10 @@ trait Reactable
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
|
||||
/* Some random names */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$i = 0;
|
||||
$peopleToDisplayInSecondaryView = array();
|
||||
/* Figure out our app - we do it this way as content items and nodes will always have a lowercase namespace for the app, so if the match below fails, then 'core' can be assumed */
|
||||
$app = explode( '\\', get_class( $this ) );
|
||||
$app = explode( '\\', static::reactionClass() );
|
||||
if ( \strtolower( $app[1] ) === $app[1] )
|
||||
{
|
||||
$app = $app[1];
|
||||
@@ -534,16 +593,18 @@ trait Reactable
|
||||
{
|
||||
$app = 'core';
|
||||
}
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', array( 'app=? AND type=? AND type_id=? AND member_id!=?', $app, static::reactionType(), $this->$idColumn, ( \IPS\Member::loggedIn()->member_id ) ?: 0 ), 'RAND()', $userLiked ? 17 : 18 ) as $rep )
|
||||
$where = $this->getReactionWhereClause();
|
||||
$where[] = array( 'member_id!=?', \IPS\Member::loggedIn()->member_id ?: 0 );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_reputation_index', $where, 'RAND()', $userLiked ? 17 : 18 )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
if ( $i < ( $userLiked ? 2 : 3 ) )
|
||||
{
|
||||
$peopleToDisplayInMainView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$peopleToDisplayInMainView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
|
||||
$andXOthers--;
|
||||
}
|
||||
else
|
||||
{
|
||||
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE );
|
||||
$peopleToDisplayInSecondaryView[] = htmlspecialchars( \IPS\Member::load( $rep['member_id'] )->name, ENT_QUOTES | ENT_DISALLOWED, 'UTF-8', FALSE );
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
@@ -569,7 +630,7 @@ trait Reactable
|
||||
$this->likeBlurb = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return $this->likeBlurb;
|
||||
}
|
||||
}
|
||||
+51
-28
@@ -76,7 +76,12 @@ class _Reaction extends \IPS\Node\Model
|
||||
* @brief [Node] Enabled/Disabled Column
|
||||
*/
|
||||
public static $databaseColumnEnabledDisabled = 'enabled';
|
||||
|
||||
|
||||
/**
|
||||
* @brief Icon Cache
|
||||
*/
|
||||
public static $icons = array();
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
@@ -131,7 +136,12 @@ class _Reaction extends \IPS\Node\Model
|
||||
*/
|
||||
public function get__icon()
|
||||
{
|
||||
return \IPS\File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
if ( !isset( static::$icons[ $this->id ] ) )
|
||||
{
|
||||
static::$icons[ $this->id ] = \IPS\File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
}
|
||||
|
||||
return static::$icons[ $this->id ];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,13 +177,20 @@ class _Reaction extends \IPS\Node\Model
|
||||
{
|
||||
if ( !count( $where ) )
|
||||
{
|
||||
$return = array();
|
||||
$cacheKey = md5( get_called_class() . $permissionCheck );
|
||||
|
||||
if( isset( static::$rootsResult[ $cacheKey ] ) )
|
||||
{
|
||||
return static::$rootsResult[ $cacheKey ];
|
||||
}
|
||||
|
||||
static::$rootsResult[ $cacheKey ] = array();
|
||||
foreach( static::reactionStore() AS $reaction )
|
||||
{
|
||||
$return[ $reaction['reaction_id'] ] = static::constructFromData( $reaction );
|
||||
static::$rootsResult[ $cacheKey ][ $reaction['reaction_id'] ] = static::constructFromData( $reaction );
|
||||
}
|
||||
|
||||
return $return;
|
||||
return static::$rootsResult[ $cacheKey ];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -196,19 +213,7 @@ class _Reaction extends \IPS\Node\Model
|
||||
|
||||
return parent::get__enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Enabled
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set__enabled( $enabled )
|
||||
{
|
||||
parent::set__enabled( $enabled );
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reaction Store
|
||||
*
|
||||
@@ -231,16 +236,7 @@ class _Reaction extends \IPS\Node\Model
|
||||
*/
|
||||
public static function isLikeMode()
|
||||
{
|
||||
$i = 0;
|
||||
foreach( static::roots() AS $row )
|
||||
{
|
||||
if ( $row->_enabled !== FALSE )
|
||||
{
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
return ( $i == 1 );
|
||||
return (bool) \IPS\Settings::i()->reaction_is_likemode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,6 +278,7 @@ class _Reaction extends \IPS\Node\Model
|
||||
parent::delete();
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
static::updateLikeModeSetting();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +291,7 @@ class _Reaction extends \IPS\Node\Model
|
||||
parent::save();
|
||||
|
||||
unset( \IPS\Data\Store::i()->reactions );
|
||||
static::updateLikeModeSetting();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,4 +329,29 @@ class _Reaction extends \IPS\Node\Model
|
||||
}
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the setting which stores if likemode is enabled ( = only one reaction is being used )
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function updateLikeModeSetting()
|
||||
{
|
||||
\IPS\Settings::i()->changeValues( array( 'reaction_is_likemode' => intval( count( static::enabledReactions() ) == 1 ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return enabled reactions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function enabledReactions()
|
||||
{
|
||||
return array_filter(
|
||||
static::roots(),
|
||||
function( $reaction ){
|
||||
return ( $reaction->_enabled !== FALSE );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Interface for Reportable Comment Models
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 8 Jul 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Reportable Content/Comment Models
|
||||
*
|
||||
* @note Content classes will gain special functionality by implementing this interface
|
||||
*/
|
||||
interface ReportCenter { }
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Reportable Trait
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 December 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reportable Trait
|
||||
*/
|
||||
trait Reportable
|
||||
{
|
||||
/**
|
||||
* Something has been moderated
|
||||
*
|
||||
* @param string $method What just happened? A question I ask myself often
|
||||
* @return void
|
||||
*/
|
||||
public function moderated( $method )
|
||||
{
|
||||
/* If something has been moderated, we should lock down the index to prevent it from being auto_moderated again */
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\core\Reports\Report::loadByClassAndId( get_class( $this ), $this->$idColumn )->lockAutoModeration();
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete reports related to this content item/comment/review
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteReport()
|
||||
{
|
||||
/* If something has been moderated, we should lock down the index to prevent it from being auto_moderated again */
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\core\Reports\Report::loadByClassAndId( get_class( $this ), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
}
|
||||
}
|
||||
+150
-13
@@ -23,10 +23,15 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
abstract class _Review extends \IPS\Content\Comment
|
||||
{
|
||||
/**
|
||||
* @brief [Content\Comment] Comment Template
|
||||
* @brief [Content\Comment] Form Template
|
||||
*/
|
||||
public static $formTemplate = array( array( 'forms', 'core', 'front' ), 'reviewTemplate' );
|
||||
|
||||
/**
|
||||
* @brief [Content\Comment] Reviews Template
|
||||
*/
|
||||
public static $commentTemplate = array( array( 'global', 'core', 'front' ), 'reviewContainer' );
|
||||
|
||||
|
||||
/**
|
||||
* Create first comment (created with content item)
|
||||
*
|
||||
@@ -239,6 +244,28 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->reviewHelpful( $this->mapped('votes_helpful'), $this->mapped('votes_total') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit and existing rating
|
||||
*
|
||||
* @param int $rating The new rating
|
||||
* @return void
|
||||
*/
|
||||
public function editRating( $rating )
|
||||
{
|
||||
/* Review */
|
||||
$ratingField = static::$databaseColumnMap['rating'];
|
||||
$this->$ratingField = $rating;
|
||||
$this->save();
|
||||
|
||||
/* Item */
|
||||
$item = $this->item();
|
||||
$ratingField = $item::$databaseColumnMap['rating'];
|
||||
$item->$ratingField = $item->averageReviewRating() ?: 0;
|
||||
|
||||
$item->resyncLastReview();
|
||||
$item->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncing to run when hiding
|
||||
*
|
||||
@@ -414,6 +441,7 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse int item The ID number of the item this belongs to
|
||||
@@ -425,22 +453,24 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
* @apiresponse string content The content
|
||||
* @apiresponse bool hidden Is hidden?
|
||||
* @apiresponse string url URL to content
|
||||
* @apiresponse string|NULL authorResponse The content item's author's response to the review, if any
|
||||
*/
|
||||
public function apiOutput()
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$itemColumn = static::$databaseColumnMap['item'];
|
||||
return array(
|
||||
'id' => $this->$idColumn,
|
||||
'item_id' => $this->$itemColumn,
|
||||
'author' => $this->author()->apiOutput(),
|
||||
'date' => \IPS\DateTime::ts( $this->mapped('date') )->rfc3339(),
|
||||
'rating' => $this->mapped('rating'),
|
||||
'votesTotal' => $this->mapped('votes_total'),
|
||||
'votesHelpful' => $this->mapped('votes_helpful'),
|
||||
'content' => $this->content(),
|
||||
'hidden' => (bool) $this->hidden(),
|
||||
'url' => (string) $this->url()
|
||||
'id' => $this->$idColumn,
|
||||
'item_id' => $this->$itemColumn,
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => \IPS\DateTime::ts( $this->mapped('date') )->rfc3339(),
|
||||
'rating' => $this->mapped('rating'),
|
||||
'votesTotal' => $this->mapped('votes_total'),
|
||||
'votesHelpful' => $this->mapped('votes_helpful'),
|
||||
'content' => $this->content(),
|
||||
'hidden' => (bool) $this->hidden(),
|
||||
'url' => (string) $this->url(),
|
||||
'authorResponse' => $this->mapped('author_response')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -456,4 +486,111 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core' )->embedReview( $this->item(), $this, $this->url()->setQueryString( $params ), $this->item()->embedImage() );
|
||||
}
|
||||
|
||||
/* !Author responses */
|
||||
|
||||
/**
|
||||
* Has the author responded to this review?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAuthorResponse()
|
||||
{
|
||||
return !is_null( $this->mapped('author_response') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the specified user respond to the review?
|
||||
*
|
||||
* @note Only the author of the content item can respond by default, but this is abstracted so third parties can override
|
||||
* @param \IPS\Member|NULL $member Member to check or NULL for currently logged in member
|
||||
* @return bool
|
||||
*/
|
||||
public function canRespond( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* If we have not responded... */
|
||||
if( !$this->hasAuthorResponse() )
|
||||
{
|
||||
/* ...and we are the author of the content item... */
|
||||
if( $member->member_id == $this->item()->author()->member_id )
|
||||
{
|
||||
/* ...then we can respond to this review. */
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the specified user edit the response to this review?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member Member to check or NULL for currently logged in member
|
||||
* @return bool
|
||||
*/
|
||||
public function canEditResponse( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* If there is an author response... */
|
||||
if( $this->hasAuthorResponse() )
|
||||
{
|
||||
$item = $this->item();
|
||||
|
||||
/* Moderators who can edit content can edit responses */
|
||||
if ( static::modPermission( 'edit', $member, $item->containerWrapper() ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Or maybe the member can edit their own content? */
|
||||
if ( $member->member_id == $item->author()->member_id and ( $member->group['g_edit_posts'] == '1' or in_array( get_class( $item ), explode( ',', $member->group['g_edit_posts'] ) ) ) and ( !( $item instanceof \IPS\Content\Lockable ) or !$item->locked() ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nope, we cannot edit */
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the specified user delete the response to this review?
|
||||
*
|
||||
* @param \IPS\Member|NULL $member Member to check or NULL for currently logged in member
|
||||
* @return bool
|
||||
*/
|
||||
public function canDeleteResponse( $member=NULL )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
/* If there is an author response... */
|
||||
if( $this->hasAuthorResponse() )
|
||||
{
|
||||
$container = NULL;
|
||||
|
||||
try
|
||||
{
|
||||
$container = $this->item()->container();
|
||||
}
|
||||
catch ( \BadMethodCallException $e ) { }
|
||||
|
||||
/* Moderators who can delete content can delete responses */
|
||||
if( static::modPermission( 'delete', $member, $container ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Or maybe the author can delete their own content? */
|
||||
if( $member->member_id and $member->member_id == $this->item()->author()->member_id and ( $member->group['g_delete_own_posts'] == '1' or in_array( get_class( $this->item() ), explode( ',', $member->group['g_delete_own_posts'] ) ) ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Nope, we cannot delete */
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Search results paginated response
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 17 Oct 2017
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Search Query
|
||||
*/
|
||||
class _ApiResponse extends \IPS\Api\PaginatedResponse
|
||||
{
|
||||
/**
|
||||
* Data to output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOutput()
|
||||
{
|
||||
/* Get the query object from the stored select */
|
||||
$queryObject = $this->select[0];
|
||||
|
||||
/* Set pagination */
|
||||
$queryObject->setLimit( $this->resultsPerPage );
|
||||
$queryObject->setPage( $this->page );
|
||||
|
||||
/* Run query */
|
||||
$searchResults = $queryObject->search(
|
||||
$this->select[2],
|
||||
$this->select[3],
|
||||
$this->select[1] + \IPS\Content\Search\Query::TAGS_MATCH_ITEMS_ONLY,
|
||||
$this->select[4]
|
||||
);
|
||||
|
||||
$this->count = $searchResults->count( TRUE );
|
||||
|
||||
$results = array();
|
||||
foreach ( $searchResults as $result )
|
||||
{
|
||||
$resultData = $result->asArray();
|
||||
$indexClass = $resultData['indexData']['index_class'];
|
||||
$itemClass = ( in_array( 'IPS\Content\Comment', class_parents( $indexClass ) ) ) ? $indexClass::$itemClass : $indexClass;
|
||||
|
||||
/* Container details */
|
||||
$containerUrl = NULL;
|
||||
$containerTitle = NULL;
|
||||
if ( isset( $itemClass::$containerNodeClass ) )
|
||||
{
|
||||
$containerClass = $itemClass::$containerNodeClass;
|
||||
$containerTitle = \IPS\Member::loggedIn()->language()->addToStack( $containerClass::$titleLangPrefix . $resultData['indexData']['index_container_id'], 'NULL', array( 'escape' => true ) );
|
||||
$containerUrl = $containerClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'], $resultData['containerData'] );
|
||||
}
|
||||
|
||||
/* Reputation - if we are showing the total value, then we need to load them up and total up all of the values */
|
||||
if ( \IPS\Settings::i()->reaction_count_display == 'count' )
|
||||
{
|
||||
$repCount = 0;
|
||||
foreach( $resultData['reputationData'] AS $memberId => $reactionId )
|
||||
{
|
||||
try
|
||||
{
|
||||
$repCount += \IPS\Content\Reaction::load( $reactionId )->value;
|
||||
}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$repCount = count( $resultData['reputationData'] );
|
||||
}
|
||||
|
||||
$commentCount = NULL;
|
||||
|
||||
if( isset( $itemClass::$databaseColumnMap['num_comments'] ) )
|
||||
{
|
||||
$commentCount = $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_comments'] ];
|
||||
|
||||
if( $itemClass::$firstCommentRequired === TRUE )
|
||||
{
|
||||
$commentCount -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
$reviewCount = NULL;
|
||||
|
||||
if( isset( $itemClass::$databaseColumnMap['num_reviews'] ) )
|
||||
{
|
||||
$reviewCount = $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_reviews'] ];
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'title' => $resultData['itemData'][ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['title'] ],
|
||||
'content' => $resultData['indexData']['index_content'],
|
||||
'class' => $indexClass,
|
||||
'objectId' => $resultData['indexData']['index_object_id'],
|
||||
'itemClass' => $itemClass,
|
||||
'itemId' => $resultData['indexData']['index_item_id'],
|
||||
'started' => $result->createdDate->rfc3339(),
|
||||
'updated' => $result->lastUpdatedDate->rfc3339(),
|
||||
'itemUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] ),
|
||||
'objectUrl' => (string) $itemClass::urlFromIndexData( $resultData['indexData'], $resultData['itemData'] )->setQueryString( array( 'do' => 'findComment', 'comment' => $resultData['indexData']['index_object_id'] ) ),
|
||||
'reputation' => $repCount,
|
||||
'comments' => $commentCount,
|
||||
'reviews' => $reviewCount,
|
||||
'container' => $containerTitle,
|
||||
'containerUrl' => (string) $containerUrl,
|
||||
'author' => $resultData['authorData']['name'],
|
||||
'authorUrl' => $resultData['authorData']['member_id'] ? (string) \IPS\Http\Url::internal( "app=core&module=members&controller=profile&id={$resultData['authorData']['member_id']}", 'front', 'profile', $resultData['authorData']['members_seo_name'] ) : NULL,
|
||||
'authorPhoto' => \IPS\Member::photoUrl( $resultData['authorData'], FALSE ),
|
||||
'authorPhotoThumbnail' => \IPS\Member::photoUrl( $resultData['authorData'] ),
|
||||
'tags' => ( !empty( $resultData['indexData']['index_tags'] ) ) ? explode( ',', $resultData['indexData']['index_tags'] ) : array()
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'page' => $this->page,
|
||||
'perPage' => $this->resultsPerPage,
|
||||
'totalResults' => $this->count,
|
||||
'totalPages' => ceil( $this->count / $this->resultsPerPage ),
|
||||
'results' => $results
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,16 @@ class _ContentFilter
|
||||
*/
|
||||
public $itemIds = array();
|
||||
|
||||
/**
|
||||
* @brief Object ID filter
|
||||
*/
|
||||
public $objectIdFilter = NULL;
|
||||
|
||||
/**
|
||||
* @brief Object IDs
|
||||
*/
|
||||
public $objectIds = array();
|
||||
|
||||
/**
|
||||
* @brief Minimum comments
|
||||
*/
|
||||
@@ -157,6 +167,20 @@ class _ContentFilter
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only include results with specific IDs
|
||||
*
|
||||
* @param array $ids Acceptable object IDs
|
||||
* @return \IPS\Content\Search\ContentFilter (for daisy chaining)
|
||||
*/
|
||||
public function onlyInIds( array $ids )
|
||||
{
|
||||
$this->objectIdFilter = TRUE;
|
||||
$this->objectIds = $ids;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude results in items
|
||||
*
|
||||
|
||||
@@ -0,0 +1,999 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Elasticsearch Search Index
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 31 Oct 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Elastic;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch Search Index
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* @brief The server URL
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Http\Url $url The server URL
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Http\Url $url )
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalize when first setting up
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->url->request()->delete();
|
||||
$this->url->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( array(
|
||||
'mappings' => array(
|
||||
'content' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_views' => array( 'type' => 'long' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get index data
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function indexData( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = parent::indexData( $object ) )
|
||||
{
|
||||
$indexData['index_permissions'] = explode( ',', $indexData['index_permissions'] );
|
||||
$indexData['index_tags'] = $indexData['index_tags'] ? explode( ',', $indexData['index_tags'] ) : array();
|
||||
$indexData['index_is_last_comment'] = (bool) $indexData['index_is_last_comment'];
|
||||
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$indexData += $this->metaCounts( $object );
|
||||
}
|
||||
else
|
||||
{
|
||||
$indexData += $this->metaCounts( $object->item() );
|
||||
}
|
||||
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) )
|
||||
{
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
$this->removeFromSearchIndex( $object );
|
||||
}
|
||||
/* Otherwise, go ahead... */
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$class = get_class( $object );
|
||||
|
||||
/* Do not allow hidden comments to be flagged as latest */
|
||||
if ( $object instanceof \IPS\Content\Comment and $indexData['index_is_last_comment'] and $indexData['index_item_id'] and $indexData['index_hidden'] !== 0 ) //
|
||||
{
|
||||
$indexData['index_is_last_comment'] = false;
|
||||
}
|
||||
|
||||
/* Insert into index */
|
||||
$r = $this->url
|
||||
->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )
|
||||
->request()
|
||||
->setHeaders( array( 'Content-Type' => 'application/json' ) )
|
||||
->put( json_encode( $indexData ) );
|
||||
|
||||
/* Views / Comments / Reviews */
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$item = $object;
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $object->item();
|
||||
}
|
||||
$this->rebuildMetaCounts( $item, $indexData['index_is_last_comment'] ? $this->getIndexId( $object ) : NULL );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update view count
|
||||
*
|
||||
* @param string $class Class to update
|
||||
* @param int $id ID of item
|
||||
* @param int $count Count to update
|
||||
* @throws \OutOfRangeException When table to update no longer exists
|
||||
*/
|
||||
public function updateViewCounts( $class, $id, $count )
|
||||
{
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_views += ". intval( $count ) . ";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $id
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment / review counts for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $item The content item
|
||||
* @return void
|
||||
*/
|
||||
protected function metaCounts( $item )
|
||||
{
|
||||
$databaseColumnId = $item::$databaseColumnId;
|
||||
|
||||
$participants = array( $item->mapped('author') );
|
||||
if ( isset( $item::$commentClass ) )
|
||||
{
|
||||
$commentClass = $item::$commentClass;
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'], $commentClass::$databaseTable, array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
if ( isset( $item::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $item::$reviewClass;
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
$participants = array_values( array_unique( $participants ) );
|
||||
|
||||
return array(
|
||||
'index_views' => $item->mapped('views'),
|
||||
'index_comments' => $item->mapped('num_comments'),
|
||||
'index_reviews' => $item->mapped('num_reviews'),
|
||||
'index_participants' => $participants
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the comment / review counts for an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $item The content item
|
||||
* @param string $lastCommentIndexId The index ID of the last comment, if you want to reset others
|
||||
* @return void
|
||||
*/
|
||||
protected function rebuildMetaCounts( $item, $lastCommentIndexId = NULL )
|
||||
{
|
||||
$databaseColumnId = $item::$databaseColumnId;
|
||||
$class = get_class( $item );
|
||||
$classes = array( $class );
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$classes[] = $class::$commentClass;
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$classes[] = $class::$reviewClass;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$updates = array();
|
||||
foreach ( $this->metaCounts( $item ) as $k => $v )
|
||||
{
|
||||
if ( is_array( $v ) )
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = [" . implode( ',', array_map( 'intval', $v ) ) . "];";
|
||||
}
|
||||
elseif ( is_null( $v ) )
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = null;";
|
||||
}
|
||||
else
|
||||
{
|
||||
$updates[] = "ctx._source.{$k} = " . intval( $v ) . ';';
|
||||
}
|
||||
}
|
||||
if ( !is_null( $lastCommentIndexId ) )
|
||||
{
|
||||
$updates[] = "ctx._source.index_is_last_comment = ( ctx._id == '{$lastCommentIndexId}' );";
|
||||
}
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => implode( ' ', $updates ),
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $item->$databaseColumnId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 \strtolower( str_replace( '\\', '_', \substr( get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to remove
|
||||
* @return void
|
||||
*/
|
||||
public function removeFromSearchIndex( \IPS\Content\Searchable $object )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $object ) )->request()->delete();
|
||||
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$class = get_class( $object );
|
||||
$idColumn = $class::$databaseColumnId;
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
$response = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $commentClass
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $object->$idColumn
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $reviewClass
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $object->$idColumn
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->rebuildMetaCounts( $object->item() );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $containerId or $authorId )
|
||||
{
|
||||
$query = array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $containerId )
|
||||
{
|
||||
$query['bool']['must'][] = array(
|
||||
'term' => array(
|
||||
'index_container_id' => $containerId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $authorId )
|
||||
{
|
||||
$query['bool']['must'][] = array(
|
||||
'term' => array(
|
||||
'index_author' => $authorId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => $query
|
||||
) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @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)
|
||||
* @param bool $addAuthorToPermissions If true, the index_author_id will be added to $newPermissions - used when changing the permissions for a node which allows access only to author's items
|
||||
* @return void
|
||||
*/
|
||||
public function massUpdate( $class, $containerId = NULL, $itemId = NULL, $newPermissions = NULL, $newHiddenStatus = NULL, $newContainer = NULL, $authorId = NULL, $newItemId = NULL, $newItemAuthorId = NULL, $addAuthorToPermissions = FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
$conditions = array();
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
)
|
||||
);
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_container_id' => $containerId
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $itemId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_id' => $itemId
|
||||
)
|
||||
);
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => $authorId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$updates = array();
|
||||
if ( $newPermissions !== NULL )
|
||||
{
|
||||
$updates[] = "ctx._source.index_permissions = " . json_encode( explode( ',', $newPermissions ) ) . ";";
|
||||
}
|
||||
if ( $newContainer )
|
||||
{
|
||||
$updates[] = "ctx._source.index_container_id = " . intval( $newContainer ) . ";";
|
||||
|
||||
if ( $itemClass = ( in_array( 'IPS\Content\Item', class_parents( $class ) ) ? $class : $class::$itemClass ) and $containerClass = $itemClass::$containerNodeClass and \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) and $clubIdColumn = $containerClass::clubIdColumn() )
|
||||
{
|
||||
try
|
||||
{
|
||||
$updates[] = "ctx._source.index_club_id = " . intval( $containerClass::load( $newContainer )->$clubIdColumn ) . ";";
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$updates[] = "ctx._source.index_club_id = null;";
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $newItemId )
|
||||
{
|
||||
$updates[] = "ctx._source.index_item_id = " . intval( $newItemId ) . ";";
|
||||
}
|
||||
if ( $newItemAuthorId )
|
||||
{
|
||||
$updates[] = "ctx._source.index_item_author = " . intval( $newItemAuthorId ) . ";";
|
||||
}
|
||||
|
||||
if ( count( $updates ) )
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => implode( ' ', $updates ),
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
if ( $addAuthorToPermissions )
|
||||
{
|
||||
$addAuthorToPermissionsConditions = $conditions;
|
||||
$addAuthorToPermissionsConditions['must_not'][] = array(
|
||||
'term' => array(
|
||||
'index_author' => 0
|
||||
)
|
||||
);
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_permissions.add( 'm.' + ctx._source.index_author );",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
|
||||
if ( $newHiddenStatus !== NULL )
|
||||
{
|
||||
if ( $newHiddenStatus === 2 )
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => '0'
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$conditions['must'][] = array(
|
||||
'term' => array(
|
||||
'index_item_author' => '2'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_permissions = " . intval( $newHiddenStatus ) . ";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => $conditions
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an arbitary number of elasticsearch conditions into a query
|
||||
*
|
||||
* @param array $conditions Conditions
|
||||
* @return array
|
||||
*/
|
||||
public static function convertConditionsToQuery( $conditions )
|
||||
{
|
||||
if ( count( $conditions ) == 1 )
|
||||
{
|
||||
return $conditions[0];
|
||||
}
|
||||
elseif ( count( $conditions ) == 0 )
|
||||
{
|
||||
return array( 'match_all' => new \StdClass );
|
||||
}
|
||||
else
|
||||
{
|
||||
return array(
|
||||
'bool' => array(
|
||||
'must' => array( $conditions )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 )
|
||||
{
|
||||
$firstComment = $item->comments( 1, 0, 'date', 'asc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
|
||||
$lastComment = $item->comments( 1, 0, 'date', 'desc', NULL, FALSE, NULL, NULL, TRUE, FALSE, FALSE );
|
||||
|
||||
$idColumn = $item::$databaseColumnId;
|
||||
$update = array( 'index_is_last_comment' => false );
|
||||
if ( $item::$firstCommentRequired )
|
||||
{
|
||||
$update['index_title'] = NULL;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/' . $this->getIndexId( $item ) . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'doc' => $update
|
||||
) ) );
|
||||
|
||||
if ( $firstComment )
|
||||
{
|
||||
$this->index( $firstComment );
|
||||
}
|
||||
if ( $lastComment )
|
||||
{
|
||||
$this->index( $lastComment );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 )
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_delete_by_query' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'range' => array(
|
||||
'index_date_updated' => array(
|
||||
'lt' => $cutoff->getTimestamp()
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the last comment flag in any given class/index_item_id
|
||||
*
|
||||
* @param array $classes The classes (when first post is required, this is typically just \IPS\forums\Topic\Post but for others, it will be both item and comment classes)
|
||||
* @param int|NULL $indexItemId The index item ID
|
||||
* @return void
|
||||
*/
|
||||
public function resetLastComment( $classes, $indexItemId )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* Remove the flag */
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_is_last_comment = false;",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
|
||||
/* Get the latest comment */
|
||||
$latest = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_hidden' => 0
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
'sort' => array(
|
||||
array(
|
||||
'index_date_created' => 'desc'
|
||||
)
|
||||
),
|
||||
'size' => 1
|
||||
) ) )->decodeJson();
|
||||
|
||||
/* If we got it... */
|
||||
if ( $latest['hits']['total'] )
|
||||
{
|
||||
$result = $latest['hits']['hits'][0];
|
||||
|
||||
/* Set that it is the latest comment */
|
||||
$r = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/' . $result['_type'] . '/' . $result['_id'] . '/_update' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'doc' => array(
|
||||
'index_is_last_comment' => true
|
||||
)
|
||||
) ) );
|
||||
|
||||
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
|
||||
$this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_update_by_query' )->setQueryString( 'conflicts', 'proceed' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'inline' => "ctx._source.index_date_updated = " . intval( $latest['_source']['index_date_updated'] ) ."; ctx._source.index_date_commented = " . intval( $latest['_source']['index_date_commented'] ) .";",
|
||||
'lang' => 'painless'
|
||||
),
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class' => $classes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_item_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_object_id' => $indexItemId
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_item_index_id' => $itemIndexIds
|
||||
),
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_author' => $member->member_id
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) )->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return array();
|
||||
}
|
||||
|
||||
$iPostedIn = array();
|
||||
foreach ( $results['hits']['hits'] as $result )
|
||||
{
|
||||
$iPostedIn[] = $result['_source']['index_item_index_id'];
|
||||
}
|
||||
|
||||
return $iPostedIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function hashesWithPermission( array $hashes, \IPS\Member $member )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_class_type_id_hash' => $hashes
|
||||
)
|
||||
),
|
||||
array(
|
||||
'terms' => array(
|
||||
'index_permissions' => array_merge( $member->permissionArray(), array( '*' ) )
|
||||
)
|
||||
),
|
||||
array(
|
||||
'term' => array(
|
||||
'index_hidden' => 0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) ) )->decodeJson();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return array();
|
||||
}
|
||||
|
||||
$hashesWithPermission = array();
|
||||
foreach ( $results['hits']['hits'] as $result )
|
||||
{
|
||||
$hashesWithPermission[ $result['_source']['index_class_type_id_hash'] ] = $result['_source']['index_class_type_id_hash'];
|
||||
}
|
||||
|
||||
return $hashesWithPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function firstIndexDate()
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ] . '/content/_search' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'size' => 1,
|
||||
'sort' => array( array( 'index_date_updated' => 'asc' ) )
|
||||
) ) )->decodeJson();
|
||||
|
||||
if ( isset( $results['hits']['hits'][0] ) )
|
||||
{
|
||||
return $results['hits']['hits'][0]['_source']['index_date_updated'];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert terms into stemmed terms for the highlighting JS
|
||||
*
|
||||
* @param array $terms Terms
|
||||
* @return array
|
||||
*/
|
||||
public function stemmedTerms( $terms )
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = $this->url->setPath( '/_analyze' )->request()->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'analyzer' => \IPS\Settings::i()->search_elastic_analyzer,
|
||||
'text' => implode( ' ', $terms )
|
||||
) ) )->decodeJson();
|
||||
|
||||
if ( isset( $results['tokens'] ) )
|
||||
{
|
||||
$stemmed = array();
|
||||
foreach ( $results['tokens'] as $token )
|
||||
{
|
||||
$stemmed[] = $token['token'];
|
||||
}
|
||||
return $stemmed;
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
return $terms;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Elasticsearch Mass Indexer
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 15 Nov 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Search\Elastic;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elasticsearch Mass Indexer
|
||||
*/
|
||||
class _MassIndexer extends \IPS\Content\Search\Elastic\Index
|
||||
{
|
||||
protected $data = array();
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return void
|
||||
*/
|
||||
public function index( \IPS\Content\Searchable $object )
|
||||
{
|
||||
if ( $indexData = $this->indexData( $object ) and $indexData['index_permissions'] )
|
||||
{
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
$parent = $object;
|
||||
}
|
||||
elseif ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$parent = $object->item();
|
||||
}
|
||||
|
||||
$this->data[] = array(
|
||||
'index' => array(
|
||||
'_index' => trim( $this->url->data[ \IPS\Http\Url::COMPONENT_PATH ], '/' ),
|
||||
'_type' => 'content',
|
||||
'_id' => $this->getIndexId( $object ),
|
||||
)
|
||||
);
|
||||
$this->data[] = $indexData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit at the end of the request
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ( $this->data )
|
||||
{
|
||||
$json = array();
|
||||
foreach ( $this->data as $data )
|
||||
{
|
||||
$json[] = json_encode( $data );
|
||||
}
|
||||
$json = implode( "\n", $json );
|
||||
|
||||
$this->url->setPath( '/_bulk' )->request()->setHeaders( array( 'Content-Type' => 'application/x-ndjson' ) )->post( $json . "\n" );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+140
-27
@@ -30,18 +30,53 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @param bool $skipCache Do not use the cached instance if one exists
|
||||
* @return static
|
||||
*/
|
||||
public static function i()
|
||||
public static function i( $skipCache=FALSE )
|
||||
{
|
||||
if( static::$instance === NULL )
|
||||
if( static::$instance === NULL OR $skipCache === TRUE )
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Elastic\Index( \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$instance = new \IPS\Content\Search\Mysql\Index;
|
||||
}
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mass indexer
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function massIndexer()
|
||||
{
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
return new \IPS\Content\Search\Elastic\MassIndexer( \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return static::i();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initalize when first setting up
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
// Does nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and rebuild search index
|
||||
*
|
||||
@@ -96,14 +131,14 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Or if this *is* the first comment, add the title and replace the tags */
|
||||
$title = $object->mapped('title');
|
||||
$title = $object->searchIndexTitle();
|
||||
$isForItem = FALSE;
|
||||
if ( $object instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$itemClass = $class::$itemClass;
|
||||
if ( $itemClass::$firstCommentRequired and $object->isFirst() )
|
||||
{
|
||||
$title = $object->item()->mapped('title');
|
||||
$title = $object->item()->searchIndexTitle();
|
||||
$tags = ( $object->item() instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? implode( ',', array_filter( $object->item()->tags() ) ) : NULL;
|
||||
$prefix = ( $object->item() instanceof \IPS\Content\Tags and \IPS\Settings::i()->tags_enabled ) ? $object->item()->prefix() : NULL;
|
||||
$isForItem = TRUE;
|
||||
@@ -114,10 +149,29 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
if ( $isForItem )
|
||||
{
|
||||
$dateUpdated = $object->item()->mapped('last_comment');
|
||||
$dateCommented = $object->item()->mapped('last_comment');
|
||||
}
|
||||
else
|
||||
{
|
||||
$dateUpdated = ( $object instanceof \IPS\Content\Item ) ? $object->mapped('last_comment') : $object->mapped('date');
|
||||
$dateUpdated = $object->mapped('date');
|
||||
$dateCommented = $object->mapped('date');
|
||||
if ( $object instanceof \IPS\Content\Item )
|
||||
{
|
||||
foreach ( array( 'last_comment', 'last_review', 'updated' ) as $k )
|
||||
{
|
||||
if ( $val = $object->mapped( $k ) )
|
||||
{
|
||||
if ( $val > $dateUpdated )
|
||||
{
|
||||
$dateUpdated = $val;
|
||||
}
|
||||
if ( $k != 'updated' and $val > $dateCommented )
|
||||
{
|
||||
$dateCommented = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Is the the latest content? */
|
||||
@@ -182,7 +236,7 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
}
|
||||
|
||||
/* Strip spoilers */
|
||||
$content = $object->mapped('content');
|
||||
$content = $object->searchIndexContent();
|
||||
if ( preg_match( '#<div\s+?class=["\']ipsSpoiler["\']#', $content ) )
|
||||
{
|
||||
$content = \IPS\Text\Parser::removeElements( $content, array( 'div[class=ipsSpoiler]' ) );
|
||||
@@ -211,6 +265,12 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
{
|
||||
try
|
||||
{
|
||||
/* If the first comment is required and there is no first comment, this is a broken piece of content - do not try to index */
|
||||
if( !$object->item()->firstComment() )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$itemIndexId = $this->getIndexId( $object->item()->firstComment() );
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
@@ -271,24 +331,27 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
|
||||
/* Return */
|
||||
return array(
|
||||
'index_class' => $class,
|
||||
'index_object_id' => $object->$idColumn,
|
||||
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
|
||||
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? get_class( $object->searchIndexContainerClass() ) : get_class( $object->item()->searchIndexContainerClass() ),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
'index_permissions' => $object->searchIndexPermissions(),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ?: $object->mapped('date') ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_prefix' => $prefix,
|
||||
'index_hidden' => $hiddenStatus,
|
||||
'index_item_index_id' => $itemIndexId,
|
||||
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
|
||||
'index_is_last_comment' => $isLastComment,
|
||||
'index_club_id' => $clubId
|
||||
'index_class' => $class,
|
||||
'index_object_id' => $object->$idColumn,
|
||||
'index_item_id' => ( $object instanceof \IPS\Content\Item ) ? $object->$idColumn : $object->mapped('item'),
|
||||
'index_container_class' => ( $object instanceof \IPS\Content\Item ) ? ( $object->searchIndexContainerClass() ? get_class( $object->searchIndexContainerClass() ) : NULL ) : get_class( $object->item()->searchIndexContainerClass() ),
|
||||
'index_container_id' => ( $object instanceof \IPS\Content\Item ) ? (int) $object->searchIndexContainer() : (int) $object->item()->mapped('container'),
|
||||
'index_title' => $title,
|
||||
'index_content' => $content,
|
||||
'index_permissions' => $object->searchIndexPermissions(),
|
||||
'index_date_created' => intval( $object->mapped('date') ),
|
||||
'index_date_updated' => intval( $dateUpdated ),
|
||||
'index_date_commented' => intval( $dateCommented ),
|
||||
'index_author' => (int) $object->mapped('author'),
|
||||
'index_tags' => $tags,
|
||||
'index_prefix' => $prefix,
|
||||
'index_hidden' => $hiddenStatus,
|
||||
'index_item_index_id' => $itemIndexId,
|
||||
'index_item_author' => intval( ( $object instanceof \IPS\Content\Item ) ? $object->mapped('author') : $object->item()->mapped('author') ),
|
||||
'index_is_last_comment' => $isLastComment,
|
||||
'index_club_id' => $clubId,
|
||||
'index_class_type_id_hash' => md5( $class . ':' . $object->$idColumn )
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -300,6 +363,19 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
*/
|
||||
abstract public function index( \IPS\Content\Searchable $object );
|
||||
|
||||
/**
|
||||
* Update view count
|
||||
*
|
||||
* @param string $class Class to update
|
||||
* @param int $id ID of item
|
||||
* @param int $count Count to update
|
||||
* @throws \OutOfRangeException When table to update no longer exists
|
||||
*/
|
||||
public function updateViewCounts( $class, $id, $count )
|
||||
{
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
@@ -320,10 +396,11 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
abstract public function removeClassFromSearchIndex( $class, $containerId=NULL );
|
||||
abstract public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL );
|
||||
|
||||
/**
|
||||
* Removes all content for a specific application from the index (for example, when uninstalling).
|
||||
@@ -405,4 +482,40 @@ abstract class _Index extends \IPS\Patterns\Singleton
|
||||
* @return void
|
||||
*/
|
||||
abstract public function resetLastComment( $class, $indexItemId );
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
abstract public function iPostedIn( array $itemIndexIds, \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
abstract public function hashesWithPermission( array $hashes, \IPS\Member $member );
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
abstract public function firstIndexDate();
|
||||
|
||||
/**
|
||||
* Convert terms into stemmed terms for the highlighting JS
|
||||
*
|
||||
* @param array $terms Terms
|
||||
* @return array
|
||||
*/
|
||||
public function stemmedTerms( $terms )
|
||||
{
|
||||
return $terms;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,30 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
/**
|
||||
* Get index data
|
||||
*
|
||||
* @param \IPS\Content\Searchable $object Item to add
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function indexData( \IPS\Content\Searchable $object )
|
||||
{
|
||||
$indexData = parent::indexData( $object );
|
||||
|
||||
if( $indexData === NULL )
|
||||
{
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
/* The index_title in core_search_index is varchar(255) so we have to limit to this length */
|
||||
if( $indexData['index_title'] !== NULL )
|
||||
{
|
||||
$indexData['index_title'] = mb_substr( $indexData['index_title'], 0, 255 );
|
||||
}
|
||||
|
||||
return $indexData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index an item
|
||||
*
|
||||
@@ -37,9 +61,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
/* If we got the data... */
|
||||
if( $indexData )
|
||||
{
|
||||
/* MySQL specific look up hash for easy referencing across tables */
|
||||
$indexData['index_class_type_id_hash'] = md5( $indexData['index_class'] . ':' . $indexData['index_object_id'] );
|
||||
|
||||
/* If nobody has permission to access it, just remove it */
|
||||
if ( !$indexData['index_permissions'] )
|
||||
{
|
||||
@@ -188,13 +209,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
\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 ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,17 +332,22 @@ class _Index extends \IPS\Content\Search\Index
|
||||
/**
|
||||
* Removes all content for a classs
|
||||
*
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to update, or NULL
|
||||
* @param string $class The class
|
||||
* @param int|NULL $containerId The container ID to delete, or NULL
|
||||
* @param int|NULL $authorId The author ID to delete, or NULL
|
||||
* @return void
|
||||
*/
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL )
|
||||
public function removeClassFromSearchIndex( $class, $containerId=NULL, $authorId=NULL )
|
||||
{
|
||||
$where = array( array( 'index_class=?', $class ) );
|
||||
if ( $containerId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_container_id=?', $containerId );
|
||||
}
|
||||
if ( $authorId !== NULL )
|
||||
{
|
||||
$where[] = array( 'index_author=?', $authorId );
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_search_index_item_map', array( 'index_class=? and index_item_id IN( ? )', $class, \IPS\Db::i()->select( 'index_item_id', 'core_search_index', $where ) ) );
|
||||
\IPS\Db::i()->delete( 'core_search_index_tags', array( 'index_id IN( ? )', \IPS\Db::i()->select( 'index_id', 'core_search_index', $where ) ) );
|
||||
@@ -494,8 +513,45 @@ class _Index extends \IPS\Content\Search\Index
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 1 ), array( 'index_class=? AND index_object_id=?', $latest['index_class'], $latest['index_object_id'] ) );
|
||||
|
||||
/* Now reset the item index with the latest comment time */
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
|
||||
\IPS\Db::i()->update( 'core_search_index', array( 'index_date_updated' => $latest['index_date_updated'], 'index_date_commented' => $latest['index_date_commented'] ), array( \IPS\Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_object_id=?', $indexItemId, $indexItemId ) );
|
||||
}
|
||||
catch( \Exception $ex ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of item index IDs, return the ones that a given member has participated in
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function iPostedIn( array $itemIndexIds, \IPS\Member $member )
|
||||
{
|
||||
return iterator_to_array( \IPS\Db::i()->select( 'index_item_index_id', 'core_search_index', array( array( \IPS\Db::i()->in( 'index_item_index_id', $itemIndexIds ) ), array( 'index_author=?', $member->member_id ) ) )->setKeyField('index_item_index_id') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of "index_class_type_id_hash"s, return the ones that a given member has permission to view
|
||||
*
|
||||
* @param array $itemIndexIds Item index IDs
|
||||
* @param \IPS\Member $member The member
|
||||
* @return array
|
||||
*/
|
||||
public function hashesWithPermission( array $hashes, \IPS\Member $member )
|
||||
{
|
||||
return iterator_to_array( \IPS\Db::i()->select( 'index_class_type_id_hash', array( 'core_search_index', 'si' ), array(
|
||||
array( "( si.index_permissions = '*' OR " . \IPS\Db::i()->findInSet( 'si.index_permissions', \IPS\Member::loggedIn()->permissionArray() ) . ' AND si.index_hidden=0 )' ),
|
||||
array( \IPS\Db::i()->in( 'si.index_class_type_id_hash', $hashes ) )
|
||||
) )->setKeyField('index_class_type_id_hash') );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timestamp of oldest thing in index
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function firstIndexDate()
|
||||
{
|
||||
return \IPS\Db::i()->select( 'MIN(index_date_updated)', 'core_search_index' )->first();
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,10 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_item_id', $filter->itemIds, $filter->itemIdFilter === FALSE );
|
||||
}
|
||||
if ( $filter->objectIdFilter !== NULL )
|
||||
{
|
||||
$clause[] = \IPS\Db::i()->in( 'index_object_id', $filter->objectIds, $filter->objectIdFilter === FALSE );
|
||||
}
|
||||
|
||||
/* Minimum comments/reviews/views? */
|
||||
if ( $filter->minimumComments or $filter->minimumReviews or $filter->minimumViews )
|
||||
@@ -263,12 +267,33 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$this->where[] = array( '( index_author=? OR (' . \IPS\Db::i()->in( 'index_id', iterator_to_array( \IPS\Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_container_id=?', 'IPS\core\Statuses\Status', $member->member_id ) ) ) ) . ') )', $member->member_id );
|
||||
}
|
||||
|
||||
return $this;
|
||||
$filterResult = $this;
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->filterByAuthor( $member );
|
||||
$filterResult = $this->filterByAuthor( $member );
|
||||
}
|
||||
|
||||
/* Get the list of valid classes */
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
foreach ( $object->classes as $class )
|
||||
{
|
||||
if ( in_array( 'IPS\Content\Item', class_parents( $class ) ) )
|
||||
{
|
||||
$classesChecked[] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Give content item classes a chance to inspect and manipulate filters */
|
||||
$filters = array();
|
||||
foreach( $classesChecked as $itemClass )
|
||||
{
|
||||
$itemClass::searchEngineFiltering( $filters, $filterResult );
|
||||
}
|
||||
|
||||
return $filterResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,7 +321,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
if( empty( $exclude ) )
|
||||
{
|
||||
$this->where[] = \IPS\Db::i()->in( 'index_container_class', $classes, TRUE );
|
||||
$this->where[] = '( index_container_class IS NULL OR ' . \IPS\Db::i()->in( 'index_container_class', $classes, TRUE ) . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -309,7 +334,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$exclude[$i] = "'" . \IPS\DB::i()->real_escape_string( $class ) . "'";
|
||||
}
|
||||
|
||||
$this->where[] = '( index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
|
||||
$this->where[] = '( index_container_class IS NULL OR index_container_class NOT IN(' . implode( ',', $classes ) . ') OR index_class IN(' . implode( ',', $exclude ) . ') )';
|
||||
}
|
||||
|
||||
return $this;
|
||||
@@ -767,7 +792,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
public function setOrder( $order )
|
||||
{
|
||||
{
|
||||
switch ( $order )
|
||||
{
|
||||
case static::ORDER_NEWEST_UPDATED:
|
||||
@@ -785,6 +810,10 @@ class _Query extends \IPS\Content\Search\Query
|
||||
case static::ORDER_OLDEST_CREATED:
|
||||
$this->order = 'index_date_created ASC';
|
||||
break;
|
||||
|
||||
case static::ORDER_NEWEST_COMMENTED:
|
||||
$this->order = 'index_date_commented DESC';
|
||||
break;
|
||||
|
||||
case static::ORDER_RELEVANCY:
|
||||
$this->order = 'calcscore DESC';
|
||||
@@ -797,12 +826,13 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/**
|
||||
* Build where
|
||||
*
|
||||
* @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
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
|
||||
* @param string $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms
|
||||
* @return array
|
||||
*/
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1 )
|
||||
protected function _searchWhereClause( $term = NULL, $tags = NULL, $method = 1, $operator = 'and' )
|
||||
{
|
||||
$tagWhere = NULL;
|
||||
$termWhere = NULL;
|
||||
@@ -850,7 +880,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/* Do we have a term? */
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
$termWhere = static::matchClause( $method & static::TERM_TITLES_ONLY ? 'index_title' : 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+' );
|
||||
$termWhere = static::matchClause( $method & static::TERM_TITLES_ONLY ? 'index_title' : 'index_content,index_title', $term, $operator === static::OPERATOR_AND ? '+' : '' );
|
||||
}
|
||||
|
||||
/* Put those two together */
|
||||
@@ -1072,7 +1102,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$extra = $columns . ' LIKE \'%' . \IPS\Db::i()->escape_string( $term ) . '%\'';
|
||||
}
|
||||
|
||||
|
||||
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE) AND (" . $extra . ")", $booleanTerm );
|
||||
}
|
||||
else
|
||||
@@ -1082,6 +1112,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
}
|
||||
else
|
||||
{
|
||||
$likeWhere = array();
|
||||
foreach ( static::termAsWordsArray( $term ) as $word )
|
||||
{
|
||||
/* Add the default operator */
|
||||
@@ -1102,9 +1133,16 @@ class _Query extends \IPS\Content\Search\Query
|
||||
if ( !static::termIsPhrase( $word ) )
|
||||
{
|
||||
/* We can't have any other operators as MySQL will interpret them as a separate word. If they exist, wrap the word in quotes */
|
||||
if ( preg_match( '/^.+[\+\-~<>]/', $word ) )
|
||||
if ( preg_match( '/^.+[\+\-~<>\.]/', $word ) )
|
||||
{
|
||||
$word = "\"" . str_replace( '"', '', ltrim( $word, $defaultOperator ) ) . "\"";
|
||||
$trimmedWord = \IPS\Db::i()->escape_string( str_replace( '"', '', ltrim( $word, $defaultOperator ) ) );
|
||||
$likes = array();
|
||||
foreach( explode( ',', $columns ) AS $column )
|
||||
{
|
||||
$likes[] = $column . ' LIKE \'%' . $trimmedWord . '%\'';
|
||||
}
|
||||
$likeWhere[] = '( ' . implode( ' or ', $likes ) . ' )';
|
||||
continue;
|
||||
}
|
||||
/* Otherwise carry on... */
|
||||
else
|
||||
@@ -1114,75 +1152,107 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
/* Nor is @ or parenthesis (while paranthesis can be used to group words and apply operators to the group,
|
||||
(e.g. "+apple +(>turnover <strudel)") - it's unlikely a user intends this behaviour) */
|
||||
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
|
||||
$word = str_replace( array( '(', ')' ), '', str_replace( '@', ' ', $word ) );
|
||||
|
||||
preg_match( '#^(\+|\-|\*)#', $word, $matches );
|
||||
if ( mb_stripos( $word, "'" ) and isset( $matches[1] ) )
|
||||
{
|
||||
/* Due to MySQL bug (https://bugs.mysql.com/bug.php?id=69932) apostrophes confuse things */
|
||||
preg_match( '#^(\+|\-|\*)#', $word, $matches );
|
||||
|
||||
$word = $matches[1] . '(' . str_replace( $matches[1], '', $word ) . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Add it */
|
||||
$words[] = $word;
|
||||
}
|
||||
$term = implode( ' ', $words );
|
||||
|
||||
/* Return */
|
||||
$return = array();
|
||||
|
||||
if ( $prepared )
|
||||
{
|
||||
if ( count( $words ) > 1 or mb_strstr( $term, '*' ) )
|
||||
/* Force newest as of 4.2.6 to test search results */
|
||||
if ( count( $likeWhere ) and count( $words ) )
|
||||
{
|
||||
return array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
$return = array( implode( ' AND ', $likeWhere ) . " AND MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
else if ( count( $likeWhere ) )
|
||||
{
|
||||
$return = array( implode( ' AND ', $likeWhere ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return array( "MATCH({$columns}) AGAINST (?)", ltrim( $term, $defaultOperator ) );
|
||||
$return = array( "MATCH({$columns}) AGAINST (? IN BOOLEAN MODE)", $term );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( count( $words ) > 1 or mb_strstr( $term, '*' ) )
|
||||
/* Force newest as of 4.2.6 to test search results */
|
||||
if ( count( $likeWhere ) )
|
||||
{
|
||||
return "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) . "' IN BOOLEAN MODE)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( ltrim( $term, $defaultOperator ) ) . "')";
|
||||
$return[] = implode( ' AND ', $likeWhere );
|
||||
}
|
||||
|
||||
$return[] = "MATCH({$columns}) AGAINST ('" . \IPS\Db::i()->escape_string( $term ) . "' IN BOOLEAN MODE)";
|
||||
|
||||
$return = implode( ' AND ', $return );
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants - controls where to search
|
||||
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
|
||||
* @return \IPS\Content\Search\Results
|
||||
*/
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1 )
|
||||
public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
|
||||
{
|
||||
/* What's our operator? */
|
||||
$operator = $operator ?: \IPS\Settings::i()->search_default_operator;
|
||||
|
||||
/* Set the select clause */
|
||||
$select = implode( ', ', $this->select );
|
||||
|
||||
/* Get the where clause */
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method, $operator ) );
|
||||
|
||||
/* Set order clause */
|
||||
$order = $this->order;
|
||||
|
||||
|
||||
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
|
||||
if ( mb_substr( $this->order, 0, 9 ) === 'calcscore' )
|
||||
{
|
||||
/* But we can only do that if there's a term (rather than tag-only) */
|
||||
if ( $term !== NULL )
|
||||
{
|
||||
if ( \IPS\Settings::i()->search_title_boost )
|
||||
{
|
||||
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . '*' . intval( \IPS\Settings::i()->search_title_boost ) . ')'; // The title score times multiplier
|
||||
}
|
||||
else
|
||||
{
|
||||
$titleField = '(' . static::matchClause( 'index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . ')';
|
||||
}
|
||||
|
||||
$select .= ', '
|
||||
. '('
|
||||
. '(' . static::matchClause( 'index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . '*5)' // The title score times 5
|
||||
. $titleField
|
||||
. '+'
|
||||
. '(' . static::matchClause( 'index_content,index_title', $term, $method & static::TERM_OR_MODE ? '' : '+', FALSE ) . ')' // Plus the content score times 1
|
||||
. '(' . static::matchClause( 'index_content,index_title', $term, $operator === static::OPERATOR_AND ? '+' : '', FALSE ) . ')' // Plus the content score times 1
|
||||
. ')'
|
||||
. '/' // Divided by
|
||||
. 'POWER('
|
||||
. '( ( UNIX_TIMESTAMP( NOW() ) - ( CASE WHEN index_date_updated <= UNIX_TIMESTAMP( NOW() ) THEN index_date_updated ELSE 0 END )) / 3600 ) + 2' // The number of days between now and the updated date, plus 2
|
||||
. '( ( UNIX_TIMESTAMP( NOW() ) - ( CASE WHEN index_date_updated <= UNIX_TIMESTAMP( NOW() ) THEN index_date_updated ELSE 0 END )) / 3600 ) + 2' // The number of days between now and the updated date, plus 2
|
||||
. ',1.5)' // To the power of 1.5
|
||||
. ' AS calcscore';
|
||||
}
|
||||
@@ -1205,25 +1275,26 @@ class _Query extends \IPS\Content\Search\Query
|
||||
{
|
||||
$query->forceIndex( $this->forceIndex );
|
||||
}
|
||||
|
||||
|
||||
/* Return */
|
||||
$count = $this->count( $term, $tags, $method, FALSE );
|
||||
$count = $this->count( $term, $tags, $method, $operator, FALSE );
|
||||
return new \IPS\Content\Search\Results( iterator_to_array( $query ), $count );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count
|
||||
*
|
||||
* @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
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
|
||||
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
|
||||
* @param boolean $returnCountAsInt If TRUE, it will return the count as an integer, when FALSE it will return the \IPS\Db\Select object
|
||||
* @return \IPS\Db\Query|int
|
||||
*/
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1, $returnCountAsInt=TRUE )
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1, $operator = NULL, $returnCountAsInt=TRUE )
|
||||
{
|
||||
/* Get the where clause */
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method ) );
|
||||
$where = array_merge( $this->where, $this->_searchWhereClause( $term, $tags, $method, $operator ) );
|
||||
|
||||
/* Construct the query */
|
||||
$query = \IPS\Db::i()->select( 'COUNT(*)', array( 'core_search_index', 'main' ), $where );
|
||||
@@ -1263,7 +1334,7 @@ class _Query extends \IPS\Content\Search\Query
|
||||
return 'newest';
|
||||
}
|
||||
|
||||
return 'relevancy';
|
||||
return parent::getDefaultSortMethod();
|
||||
}
|
||||
|
||||
protected static $tableCount = NULL;
|
||||
@@ -1282,4 +1353,77 @@ class _Query extends \IPS\Content\Search\Query
|
||||
|
||||
return static::$tableCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the term into an array of words
|
||||
*
|
||||
* @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|NULL $minLength The minimum length a sequence of characters has to be before it is considered a word. If null, ft_min_word_len/innodb_ft_min_token_size is used.
|
||||
* @param int|NULL $maxLength The maximum length a sequence of characters can be for it to be considered a word. If null, ft_max_word_len/innodb_ft_max_token_size is used.
|
||||
* @return array
|
||||
*/
|
||||
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=NULL, $maxLength=NULL )
|
||||
{
|
||||
/* If we haven't set a preferred min/max length, use the MySQL configuration */
|
||||
if ( $minLength === NULL or $maxLength === NULL )
|
||||
{
|
||||
/* If we don't already know what they are, get the values from the MySQL configuration */
|
||||
if ( ( $minLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMinWord ) ) or ( $maxLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMaxWord ) ) )
|
||||
{
|
||||
/* The variable we need depends on whether the table is MyISAM or InnoDB */
|
||||
$tableDefinition = \IPS\Db::i()->getTableDefinition('core_search_index');
|
||||
if ( $tableDefinition['engine'] == 'InnoDB' )
|
||||
{
|
||||
$minVariable = 'innodb_ft_min_token_size';
|
||||
$maxVariable = 'innodb_ft_max_token_size';
|
||||
}
|
||||
else
|
||||
{
|
||||
$minVariable = 'ft_min_word_len';
|
||||
$maxVariable = 'ft_max_word_len';
|
||||
}
|
||||
|
||||
/* Now fetch those */
|
||||
try
|
||||
{
|
||||
foreach ( new \IPS\Db\Select( 'SHOW VARIABLES WHERE Variable_Name=? OR Variable_Name=?', array( $minVariable, $maxVariable ), \IPS\Db::i() ) as $row )
|
||||
{
|
||||
if ( $row['Variable_name'] === $minVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = intval( $row['Value'] );
|
||||
}
|
||||
elseif ( $row['Variable_name'] === $maxVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = intval( $row['Value'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \IPS\Db\Exception $e ) { }
|
||||
|
||||
/* If we weren't able to get them, set sensible defaults */
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMinWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = 3;
|
||||
}
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMaxWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = 84;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set */
|
||||
if ( $minLength === NULL )
|
||||
{
|
||||
$minLength = \IPS\Data\Store::i()->mysqlMinWord;
|
||||
}
|
||||
if ( $maxLength === NULL )
|
||||
{
|
||||
$maxLength = \IPS\Data\Store::i()->mysqlMaxWord;
|
||||
}
|
||||
}
|
||||
|
||||
/* And then pass up */
|
||||
return parent::termAsWordsArray( $term, $ignorePhrase, $minLength, $maxLength );
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,14 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
abstract class _Query
|
||||
{
|
||||
const TERM_OR_TAGS = 1;
|
||||
const TERM_AND_TAGS = 2;
|
||||
const TERM_OR_MODE = 4;
|
||||
const TERM_TITLES_ONLY = 8;
|
||||
const TERM_OR_TAGS = 1; // If both tags and a term are provided, match results where either the tags or the term match
|
||||
const TERM_AND_TAGS = 2; // If both tags and a term are provided, match only results where BOTH the tags or the term match
|
||||
const TERM_TITLES_ONLY = 8; // If this flag is set, only titles will be searched
|
||||
const TAGS_MATCH_ITEMS_ONLY = 16;
|
||||
|
||||
const OPERATOR_OR = 'or';
|
||||
const OPERATOR_AND = 'and';
|
||||
|
||||
const HIDDEN_VISIBLE = 0;
|
||||
const HIDDEN_UNAPPROVED = 1;
|
||||
const HIDDEN_HIDDEN = -1;
|
||||
@@ -38,6 +40,7 @@ abstract class _Query
|
||||
const ORDER_RELEVANCY = 3;
|
||||
const ORDER_OLDEST_UPDATED = 4;
|
||||
const ORDER_OLDEST_CREATED = 5;
|
||||
const ORDER_NEWEST_COMMENTED = 6;
|
||||
|
||||
const SUPPORTS_JOIN_FILTERS = TRUE;
|
||||
|
||||
@@ -49,7 +52,14 @@ abstract class _Query
|
||||
*/
|
||||
public static function init( \IPS\Member $member = NULL )
|
||||
{
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
if ( \IPS\Settings::i()->search_method == 'elastic' )
|
||||
{
|
||||
return new \IPS\Content\Search\Elastic\Query( $member ?: \IPS\Member::loggedIn(), \IPS\Http\Url::external( \IPS\Settings::i()->search_elastic_server . '/' . \IPS\Settings::i()->search_elastic_index ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return new \IPS\Content\Search\Mysql\Query( $member ?: \IPS\Member::loggedIn() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +141,16 @@ abstract class _Query
|
||||
*/
|
||||
abstract public function filterByItemAuthor( \IPS\Member $author );
|
||||
|
||||
/**
|
||||
* Filter by container class
|
||||
*
|
||||
* @param array $classes Container classes to exclude from results.
|
||||
* @param array $exclude Content classes to exclude from the filter. For cases where multiple content classes may have the same container class
|
||||
* such as Gallery images, comments and reviews.
|
||||
* @return \IPS\Content\Search\Query (for daisy chaining)
|
||||
*/
|
||||
abstract public function filterByContainerClasses( $classes=array(), $exclude=array() );
|
||||
|
||||
/**
|
||||
* Filter by content the user follows
|
||||
*
|
||||
@@ -223,12 +243,13 @@ abstract class _Query
|
||||
/**
|
||||
* 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 \Traversable
|
||||
* @param string|null $term The term to search for
|
||||
* @param array|null $tags The tags to search for
|
||||
* @param int $method See \IPS\Content\Search\Query::TERM_* contants
|
||||
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
|
||||
* @return \IPS\Content\Search\Results
|
||||
*/
|
||||
abstract public function search( $term = NULL, $tags = NULL, $method = 1 );
|
||||
abstract public function search( $term = NULL, $tags = NULL, $method = 1, $operator = NULL );
|
||||
|
||||
/**
|
||||
* Get the default date cut off
|
||||
@@ -253,14 +274,15 @@ abstract class _Query
|
||||
/**
|
||||
* Get count
|
||||
*
|
||||
* @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
|
||||
* @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
|
||||
* @param string|null $operator If $term contains more than one word, determines if searching for both ("and") or any ("or") of those terms. NULL will go to admin-defined setting
|
||||
* @return int
|
||||
*/
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1 )
|
||||
public function count( $term = NULL, $tags = NULL, $method = 1, $operator = NULL )
|
||||
{
|
||||
return $this->search( $term, $tags, $method )->count( TRUE );
|
||||
return $this->search( $term, $tags, $method, $operator )->count( TRUE );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,63 +307,8 @@ abstract class _Query
|
||||
*/
|
||||
public static function termAsWordsArray( $term, $ignorePhrase=FALSE, $minLength=NULL, $maxLength=NULL )
|
||||
{
|
||||
/* If we haven't set a preferred min/max length, use the MySQL configuration */
|
||||
if ( $minLength === NULL or $maxLength === NULL )
|
||||
{
|
||||
/* If we don't already know what they are, get the values from the MySQL configuration */
|
||||
if ( ( $minLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMinWord ) ) or ( $maxLength === NULL and !isset( \IPS\Data\Store::i()->mysqlMaxWord ) ) )
|
||||
{
|
||||
/* The variable we need depends on whether the table is MyISAM or InnoDB */
|
||||
$tableDefinition = \IPS\Db::i()->getTableDefinition('core_search_index');
|
||||
if ( $tableDefinition['engine'] == 'InnoDB' )
|
||||
{
|
||||
$minVariable = 'innodb_ft_min_token_size';
|
||||
$maxVariable = 'innodb_ft_max_token_size';
|
||||
}
|
||||
else
|
||||
{
|
||||
$minVariable = 'ft_min_word_len';
|
||||
$maxVariable = 'ft_max_word_len';
|
||||
}
|
||||
|
||||
/* Now fetch those */
|
||||
try
|
||||
{
|
||||
foreach ( new \IPS\Db\Select( 'SHOW VARIABLES WHERE Variable_Name=? OR Variable_Name=?', array( $minVariable, $maxVariable ), \IPS\Db::i() ) as $row )
|
||||
{
|
||||
if ( $row['Variable_name'] === $minVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = intval( $row['Value'] );
|
||||
}
|
||||
elseif ( $row['Variable_name'] === $maxVariable )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = intval( $row['Value'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \IPS\Db\Exception $e ) { }
|
||||
|
||||
/* If we weren't able to get them, set sensible defaults */
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMinWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMinWord = 3;
|
||||
}
|
||||
if ( !isset( \IPS\Data\Store::i()->mysqlMaxWord ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->mysqlMaxWord = 84;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set */
|
||||
if ( $minLength === NULL )
|
||||
{
|
||||
$minLength = \IPS\Data\Store::i()->mysqlMinWord;
|
||||
}
|
||||
if ( $maxLength === NULL )
|
||||
{
|
||||
$maxLength = \IPS\Data\Store::i()->mysqlMaxWord;
|
||||
}
|
||||
}
|
||||
$minLength = is_null( $minLength ) ? 0 : $minLength;
|
||||
$maxLength = is_null( $maxLength ) ? 255 : $maxLength;
|
||||
|
||||
/* Sub quotes with smaller than min word strings can cause issues for larger tables, so we convert the sub quote into normal words for a boolean search */
|
||||
if ( ! static::termIsPhrase( $term ) )
|
||||
|
||||
@@ -22,6 +22,29 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
abstract class _Result
|
||||
{
|
||||
/**
|
||||
* Pre-Display logic for search index content
|
||||
*
|
||||
* @param bool $createdDate If TRUE, uses $createdDate, otherwise uses $lastUpdatedDate
|
||||
* @return string
|
||||
*/
|
||||
public static function preDisplay( $content )
|
||||
{
|
||||
/* We cannot use images (or *anything* other than text) in search results as it messes with the truncation
|
||||
logic. All HTML (including emoticons) are already stripped before they go into the search index
|
||||
for this reason, but Emoji (because they're text) won't be. We can't swap the Emoji back out
|
||||
with one of the alternative Emoji style images though because that re-introduces images, so we
|
||||
just strip them out (making them behave like emoticons) */
|
||||
if ( \IPS\Settings::i()->emoji_style == 'twemoji' or \IPS\Settings::i()->emoji_style == 'emojione' )
|
||||
{
|
||||
return preg_replace( '/(?:' . \IPS\Text\Parser::EMOJI_REGEX . '|\x{200d})+/u', '', $content );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Created Date
|
||||
*/
|
||||
@@ -68,4 +91,35 @@ abstract class _Result
|
||||
return 'earlier';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @return array
|
||||
* @apiresponse string title Title of search result
|
||||
* @apiresponse string content Content of search result
|
||||
* @apiresponse string class Content class of search result
|
||||
* @apiresponse int objectId Content ID of search result
|
||||
* @apiresponse string itemClass Content item class of search result (if search result is of a content item, this will match class)
|
||||
* @apiresponse int itemId Content item ID of search result (if search result is of a content item, this will match objectId)
|
||||
* @apiresponse datetime started Datetime search result was submitted
|
||||
* @apiresponse datetime updated Datetime search result was last updated
|
||||
* @apiresponse string itemUrl URL to content item
|
||||
* @apiresponse string objectUrl URL to search result item (if search result is of a content item, this will match itemUrl)
|
||||
* @apiresponse int reputation Number of reputation points for search result
|
||||
* @apiresponse int|null comments Number of comments or replies for search result, or NULL if commenting is not supported
|
||||
* @apiresponse int|null reviews Number of reviews for search result, or NULL if reviewing is not supported
|
||||
* @apiresponse string container Title of container of search result
|
||||
* @apiresponse string containerUrl URL to container of search result
|
||||
* @apiresponse string author Author name of search result
|
||||
* @apiresponse string|null authorUrl URL to author's profile, or NULL if search result was submitted by a guest
|
||||
* @apiresponse string authorPhoto URL to author's profile photo
|
||||
* @apiresponse string authorPhotoThumbnail URL to author's profile photo thumbnail
|
||||
* @apiresponse array tags Array of tags associated with the search result
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL )
|
||||
{
|
||||
/* @note This is only here to populate the AdminCP reference tab - the array of search result entries is built manually */
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$this->itemData[ $itemClass ][ $itemId ] = $itemData;
|
||||
|
||||
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
if ( isset( $itemClass::$databaseColumnMap ) and isset( $itemClass::$databaseColumnMap['author'] ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
$membersToLoad[ $memberId ] = $memberId;
|
||||
@@ -236,16 +236,16 @@ class _Results extends \ArrayIterator
|
||||
/* 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 ) ) )->setKeyField('index_item_index_id') );
|
||||
$this->iPostedIn = \IPS\Content\Search\Index::i()->iPostedIn( $itemIndexIds, \IPS\Member::loggedIn() );
|
||||
}
|
||||
|
||||
/* Load tags and prefixes */
|
||||
if ( count( $itemIndexIds ) )
|
||||
if ( count( $itemIndexIds ) and \IPS\Settings::i()->search_method == 'mysql' )
|
||||
{
|
||||
$this->tags = iterator_to_array( \IPS\Db::i()->select( 'index_id, GROUP_CONCAT(index_tag) as index_tags', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 0' ) ), NULL, NULL, 'index_id' )->setKeyField('index_id') );
|
||||
$this->prefixes = iterator_to_array( \IPS\Db::i()->select( 'index_id, index_tag as index_prefix', 'core_search_index_tags', array( array( \IPS\Db::i()->in( 'index_id', $itemIndexIds ) ), array( 'index_is_prefix = 1' ) ) )->setKeyField('index_id') );
|
||||
}
|
||||
|
||||
|
||||
/* Set that we're initiated */
|
||||
$this->initated = TRUE;
|
||||
}
|
||||
@@ -280,7 +280,8 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
/* This should not happen in production, so debug log and move on to the next item */
|
||||
\IPS\Log::debug( "index_item_id ID " . $data['index_item_id'] . " for " . $itemClass . " has no itemData" , 'searchResults' );
|
||||
return static::next();
|
||||
static::next();
|
||||
return static::current();
|
||||
}
|
||||
|
||||
$itemData = $this->itemData[ $itemClass ][ $data['index_item_id'] ];
|
||||
@@ -288,7 +289,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$itemData['profile'] = $this->authorData[ $data['index_container_id'] ];
|
||||
}
|
||||
if ( isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
if ( isset( $itemClass::$databaseColumnMap['author'] ) and isset( $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ] ) )
|
||||
{
|
||||
$memberId = $itemData[ $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'] ];
|
||||
if ( isset( $this->authorData[ $memberId ] ) )
|
||||
@@ -301,7 +302,7 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$data['index_tags'] = $this->tags[ $data['index_item_index_id'] ]['index_tags'];
|
||||
}
|
||||
|
||||
|
||||
if ( isset( $this->prefixes[ $data['index_item_index_id'] ]) )
|
||||
{
|
||||
$data['index_prefix'] = $this->prefixes[ $data['index_item_index_id'] ]['index_prefix'];
|
||||
@@ -432,7 +433,7 @@ class _Results extends \ArrayIterator
|
||||
/* Users registered */
|
||||
if ( in_array( 'register', $extra ) )
|
||||
{
|
||||
$where = array( array( 'joined>?', $lastTime ), array( 'name<>?', '' ) );
|
||||
$where = array( array( 'joined>?', $lastTime ), array( '( name<>? and email<>? )', '', '' ), array( 'temp_ban!=?', '-1' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'joined<?', $firstTime );
|
||||
@@ -441,12 +442,18 @@ class _Results extends \ArrayIterator
|
||||
{
|
||||
$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 )
|
||||
{
|
||||
|
||||
foreach (
|
||||
\IPS\Db::i()->select( implode( ', ', array_merge( array_map( function( $c ) { return "core_members.{$c}"; }, \IPS\Member::columnsForPhoto() ), array( 'core_members.joined', 'core_validating.new_reg' ) ) ), 'core_members', $where, 'joined DESC', 10 )
|
||||
->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'] ) ) ) );
|
||||
$extraItems[] = new Result\Custom(
|
||||
\IPS\DateTime::ts( $member['joined'] ),
|
||||
\IPS\Member::loggedIn()->language()->addToStack( 'activity_member_joined', FALSE, array( 'htmlsprintf' => \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userLinkFromData( $member['member_id'], $member['name'], $member['members_seo_name'] ) ) ),
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->userPhotoFromData( $member['member_id'], $member['name'], $member['members_seo_name'], \IPS\Member::photoUrl( $member ), 'tiny' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,7 +461,7 @@ class _Results extends \ArrayIterator
|
||||
/* Users changed photo */
|
||||
if ( in_array( 'photo', $extra ) )
|
||||
{
|
||||
$where = array( array( 'photo_last_update>?', $lastTime ) );
|
||||
$where = array( array( 'photo_last_update>? AND photo_last_update>(joined+300)', $lastTime ), array( '( name<>? and email<>? )', '', '' ) );
|
||||
if ( $firstTime )
|
||||
{
|
||||
$where[] = array( 'photo_last_update<?', $firstTime );
|
||||
@@ -525,7 +532,7 @@ class _Results extends \ArrayIterator
|
||||
}
|
||||
else
|
||||
{
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . ucfirst( $follow['follow_area'] );
|
||||
$class = 'IPS\\' . $follow['follow_app'] . '\\' . mb_ucfirst( $follow['follow_area'] );
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -53,7 +53,11 @@ class _Facebook
|
||||
*/
|
||||
public static function canAutoshare()
|
||||
{
|
||||
return (boolean) \IPS\Member::loggedIn()->fb_token AND \IPS\Member::loggedIn()->fb_uid;
|
||||
if ( $method = \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth2\Facebook' ) and $authorizedScopes = $method->authorizedScopes( \IPS\Member::loggedIn() ) )
|
||||
{
|
||||
return in_array( 'publish_actions', $authorizedScopes );
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,41 +69,19 @@ class _Facebook
|
||||
*/
|
||||
public static function publish( $content, $url=null )
|
||||
{
|
||||
if ( static::canAutoshare() )
|
||||
if ( static::canAutoshare() and $method = \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth2\Facebook' ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
/* If we include a URL, we'll post it as a link to the wall, otherwise we'll just update the user's status */
|
||||
if ( $url !== NULL )
|
||||
|
||||
if ( !$method->postToFacebook( \IPS\Member::loggedIn(), $content, $url ) )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . \IPS\Member::loggedIn()->fb_uid . "/feed" )->request()->post( array(
|
||||
'message' => $content,
|
||||
'link' => (string) $url,
|
||||
'access_token' => \IPS\Member::loggedIn()->fb_token,
|
||||
) )->decodeJson();
|
||||
|
||||
if ( ! isset( $response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . \IPS\Member::loggedIn()->fb_uid . "/feed" )->request()->post( array(
|
||||
'message' => $content,
|
||||
'access_token' => \IPS\Member::loggedIn()->fb_token,
|
||||
) )->decodeJson();
|
||||
|
||||
if ( ! isset( $response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch( \IPS\Http\Request\Exception $e )
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( \IPS\Member::loggedIn()->id . ': '. $e->getMessage(), 'facebook' );
|
||||
|
||||
\IPS\Log::log( \IPS\Member::loggedIn()->member_id . ': '. $e->getMessage(), 'facebook' );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
@@ -120,7 +102,7 @@ class _Facebook
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Select( 'fbc_bot_group', \IPS\Settings::i()->fbc_bot_group, TRUE, array( 'options' => array_combine( array_keys( \IPS\Member\Group::groups() ), array_map( function( $_group ) { return (string) $_group; }, \IPS\Member\Group::groups() ) ) ) ) );
|
||||
|
||||
if ( array_key_exists( 'Facebook', \IPS\Login::handlers( TRUE ) ) )
|
||||
if ( \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth2\Facebook' ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'share_autoshare_Facebook', $service->autoshare, false ) );
|
||||
}
|
||||
|
||||
@@ -53,7 +53,11 @@ class _Twitter
|
||||
*/
|
||||
public static function canAutoshare()
|
||||
{
|
||||
return (boolean) \IPS\Member::loggedIn()->twitter_token AND \IPS\Member::loggedIn()->twitter_secret;
|
||||
if ( $method = \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth1\Twitter' ) and $method->canProcess( \IPS\Member::loggedIn() ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,63 +69,20 @@ class _Twitter
|
||||
*/
|
||||
public static function publish( $content, $url=null )
|
||||
{
|
||||
if ( static::canAutoshare() )
|
||||
{
|
||||
$loginHandler = \IPS\Login\LoginAbstract::load('twitter');
|
||||
|
||||
if ( isset( \IPS\Data\Store::i()->twitter_config ) AND isset( \IPS\Data\Store::i()->twitter_config['time'] ) AND ( \IPS\Data\Store::i()->twitter_config['time'] <= time() - 86400 ) )
|
||||
{
|
||||
$maxUrlLen = \IPS\Data\Store::i()->twitter_config['short_url_length'];
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $loginHandler->sendRequest( 'get', "https://api.twitter.com/1.1/help/configuration.json", array(), \IPS\Member::loggedIn()->twitter_token, \IPS\Member::loggedIn()->twitter_secret )->decodeJson();
|
||||
|
||||
if ( isset( $response['short_url_length'] ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->twitter_config = array_merge( $response, array( 'time' => time() ) );
|
||||
|
||||
$maxUrlLen = $response['short_url_length'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
{
|
||||
\IPS\Log::log( \IPS\Member::loggedIn()->id . ': '. $e->getMessage(), 'twitter' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $url !== null )
|
||||
{
|
||||
$content = mb_substr( $content, 0, ( 140 - ( $maxUrlLen + 1 ) ) ) . ' ' . $url;
|
||||
}
|
||||
|
||||
if ( static::canAutoshare() and $method = \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth1\Twitter' ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$response = $loginHandler->sendRequest( 'post', "https://api.twitter.com/1.1/statuses/update.json", array( 'status' => $content ), \IPS\Member::loggedIn()->twitter_token, \IPS\Member::loggedIn()->twitter_secret )->decodeJson();
|
||||
|
||||
if ( isset( $response['id_str'] ) )
|
||||
if ( !$method->postToTwitter( \IPS\Member::loggedIn(), $content, $url ) )
|
||||
{
|
||||
return $response['id_str'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
throw new \Exception;
|
||||
}
|
||||
}
|
||||
catch ( \IPS\Http\Request\Exception $e )
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( \IPS\Member::loggedIn()->id . ': '. $e->getMessage(), 'twitter' );
|
||||
|
||||
\IPS\Log::log( \IPS\Member::loggedIn()->member_id . ': '. $e->getMessage(), 'twitter' );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('twitter_publish_exception') );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -140,7 +101,7 @@ class _Twitter
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'twitter_hashtag', \IPS\Settings::i()->twitter_hashtag, FALSE ) );
|
||||
|
||||
if ( array_key_exists( 'Twitter', \IPS\Login::handlers( TRUE ) ) )
|
||||
if ( \IPS\Login\Handler::findMethod( 'IPS\Login\Handler\OAuth2\Twitter' ) )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'share_autoshare_Twitter', $service->autoshare, false ) );
|
||||
}
|
||||
|
||||
@@ -162,7 +162,27 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
)
|
||||
) ) );
|
||||
|
||||
return $form;
|
||||
/* Tags */
|
||||
if( \IPS\Settings::i()->tags_enabled )
|
||||
{
|
||||
if ( \IPS\Settings::i()->tags_force_lower )
|
||||
{
|
||||
$options['autocomplete']['forceLower'] = TRUE;
|
||||
}
|
||||
|
||||
if ( \IPS\Settings::i()->tags_clean )
|
||||
{
|
||||
$options['autocomplete']['filterProfanity'] = TRUE;
|
||||
}
|
||||
|
||||
$options['autocomplete']['prefix'] = FALSE;
|
||||
$options['autocomplete']['minimized'] = FALSE;
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'widget_feed_tags', ( isset( $this->configuration['widget_feed_tags'] ) ? $this->configuration['widget_feed_tags'] : array( 'tags' => NULL ) ), FALSE, $options ) );
|
||||
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +214,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
|
||||
if ( !isset( $this->configuration['language_key'] ) )
|
||||
{
|
||||
$this->configuration['language_key'] = 'widget_title_' . md5( uniqid() );
|
||||
$this->configuration['language_key'] = 'widget_title_' . md5( mt_rand() );
|
||||
}
|
||||
$values['language_key'] = $this->configuration['language_key'];
|
||||
|
||||
@@ -301,8 +321,8 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
{
|
||||
$class = static::$class;
|
||||
$where = $this->buildWhere();
|
||||
|
||||
if ( isset( $this->configuration['widget_feed_tags'] ) and is_array( $this->configuration['widget_feed_tags'] ) and count( $this->configuration['widget_feed_tags'] ) )
|
||||
|
||||
if ( \IPS\Settings::i()->tags_enabled and isset( $this->configuration['widget_feed_tags'] ) and is_array( $this->configuration['widget_feed_tags'] ) and count( $this->configuration['widget_feed_tags'] ) )
|
||||
{
|
||||
$tagWhere = array();
|
||||
$binds = array( $class::$application, $class::$module );
|
||||
@@ -312,7 +332,7 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
$binds[] = $tag;
|
||||
}
|
||||
|
||||
$where[] = array( $class::$databaseColumnId . ' IN (?)', \IPS\Db::i()->select( 'tag_meta_id', 'core_tags', array( array_merge( array( 'tag_meta_app=? AND tag_meta_area=? AND (' . implode( ' OR ', $tagWhere ) . ')' ), $binds ) ) ) );
|
||||
$where[] = array( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnId . ' IN (?)', \IPS\Db::i()->select( 'tag_meta_id', 'core_tags', array( array_merge( array( 'tag_meta_app=? AND tag_meta_area=? AND (' . implode( ' OR ', $tagWhere ) . ')' ), $binds ) ) ) );
|
||||
}
|
||||
|
||||
/* What visible status are we checking? */
|
||||
@@ -331,15 +351,16 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* As the block config can try and search all rows in topics/records, etc, we can end up with a tmp table and block nested buffer on members table, so we just query members after separately */
|
||||
$skipPerms = ( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? FALSE : TRUE;
|
||||
$items = iterator_to_array( $class::getItemsWithPermission(
|
||||
$where, /* Where clause */
|
||||
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( ( $this->configuration['widget_feed_sort_on'] == '_rand' ) ? $this->configuration['widget_feed_sort_on'] : ( $class::$databasePrefix . $this->configuration['widget_feed_sort_on'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) ) : ( $class::$databasePrefix . $class::$databaseColumnMap['updated'] . ' DESC' ), /* Order */
|
||||
isset( $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5, /* Limit */
|
||||
( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? 'read' : NULL, /* Permission key to check against */
|
||||
$skipPerms ? NULL : 'read', /* Permission key to check against */
|
||||
$hidden, /* Whether or not to include hidden items */
|
||||
0, NULL, FALSE, FALSE, FALSE, FALSE, NULL, FALSE, TRUE, FALSE, FALSE
|
||||
0, NULL, FALSE, FALSE, FALSE, FALSE, NULL, $skipPerms, TRUE, FALSE, FALSE
|
||||
) );
|
||||
|
||||
if ( count( $items ) )
|
||||
|
||||
@@ -268,7 +268,7 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
$where['item'][] = array( \IPS\Db::i()->in( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['container'], $this->configuration['widget_feed_container'] ) );
|
||||
}
|
||||
|
||||
/* Status */
|
||||
/* Locked or open status */
|
||||
if ( isset( $this->configuration['widget_feed_item_status_locked'] ) and in_array( 'IPS\Content\Lockable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_locked'] == 'closed' )
|
||||
@@ -281,6 +281,7 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
}
|
||||
}
|
||||
|
||||
/* Featured or unfeatured */
|
||||
if ( isset( $this->configuration['widget_feed_item_status_featured'] ) and in_array( 'IPS\Content\Featurable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_featured'] == 'notfeatured' )
|
||||
@@ -293,6 +294,7 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
}
|
||||
}
|
||||
|
||||
/* Pinned or unpinned */
|
||||
if ( isset( $this->configuration['widget_feed_item_status_pinned'] ) and in_array( 'IPS\Content\Pinnable', class_implements( $itemClass ) ) )
|
||||
{
|
||||
if ( $this->configuration['widget_feed_item_status_pinned'] == 'notpinned' )
|
||||
@@ -311,7 +313,7 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
$where['item'][] = array( \IPS\Db::i()->in( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['author'], $this->configuration['widget_feed_item_author'] ) );
|
||||
}
|
||||
|
||||
/* Min comments/reviews */
|
||||
/* Min posts/comments/reviews */
|
||||
if ( isset( $this->configuration['widget_feed_item_min_posts'] ) and isset( $this->configuration['widget_feed_item_min_posts'] ) and isset( $this->configuration['widget_feed_item_min_posts'] ) )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databaseTable . '.' . $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['num_comments'] . '>?', (int) $this->configuration['widget_feed_item_min_posts'] );
|
||||
|
||||
Reference in new issue
Block a user