Version 5.0.6
This commit is contained in:
1 parent
a3a4a9ccbd
commit
b89858b2ff
1214 files changed
+92828
-14859
No files matched your search
@@ -15,12 +15,14 @@ namespace IPS\Content;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\Text;
|
||||
use IPS\Lang;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Output;
|
||||
use IPS\Output\UI\UiExtension;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use OutOfRangeException;
|
||||
@@ -129,8 +131,47 @@ trait ClubContainer
|
||||
*/
|
||||
public function clubForm( Form $form, Club $club ): void
|
||||
{
|
||||
$itemClass = static::$contentItemClass;
|
||||
$form->add( new Text( 'club_node_name', $this->_id ? $this->_title : Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
|
||||
$this->_clubForm( $form, $club );
|
||||
|
||||
/* Only do this if the form is still empty */
|
||||
if( empty( $form->elements ) )
|
||||
{
|
||||
$itemClass = static::$contentItemClass;
|
||||
$form->add( new Text( 'club_node_name', $this->_id ? $this->_title : Member::loggedIn()->language()->addToStack( static::clubFrontTitle() ), TRUE, array( 'maxLength' => 255 ) ) );
|
||||
}
|
||||
|
||||
$form->add( new Radio( 'club_node_creators', $this->_id ? $this->creators() : null, false, [ 'options' => [ 0 => 'club_node_creators_0', 1 => 'club_node_creators_1' ] ] ) );
|
||||
|
||||
/* Now loop through and add all the elements to the form */
|
||||
if( static::$canBeExtended )
|
||||
{
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
{
|
||||
foreach( $extension->clubFormElements( $this, $club ) as $element )
|
||||
{
|
||||
if( is_object( $element ) )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->addHtml( $element );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class-specific method for each Model to implement
|
||||
*
|
||||
* @param Form $form
|
||||
* @param Club $club
|
||||
* @return void
|
||||
*/
|
||||
public function _clubForm( Form $form, Club $club ) : void
|
||||
{
|
||||
/* Intentionally blank */
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,6 +197,15 @@ trait ClubContainer
|
||||
$this->$clubIdColumn = $club->id;
|
||||
}
|
||||
|
||||
/* Loop through each extension and update the values; do this before we save */
|
||||
if( static::$canBeExtended )
|
||||
{
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
{
|
||||
$values = $extension->processClubForm( $this, $club, $values );
|
||||
}
|
||||
}
|
||||
|
||||
$this->_saveClubForm( $club, $values );
|
||||
|
||||
$needToUpdatePermissions = FALSE;
|
||||
@@ -169,20 +219,25 @@ trait ClubContainer
|
||||
'node_class' => $nodeClass,
|
||||
'node_id' => $this->_id,
|
||||
'name' => $values['club_node_name'],
|
||||
'public' => $values['club_node_public'] ?? 0
|
||||
'public' => $values['club_node_public'] ?? 0,
|
||||
'creators' => $values['club_node_creators'] ?? 0
|
||||
) );
|
||||
|
||||
$needToUpdatePermissions = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
if( isset( $values['club_node_public'] ) AND $values['club_node_public'] != $this->isPublic() )
|
||||
if( ( isset( $values['club_node_public'] ) and $values['club_node_public'] != $this->isPublic() ) or ( isset( $values['club_node_creators'] ) and $values['club_node_creators'] != $this->creators() ) )
|
||||
{
|
||||
$needToUpdatePermissions = TRUE;
|
||||
}
|
||||
|
||||
$this->save();
|
||||
Db::i()->update( 'core_clubs_node_map', array( 'name' => $values['club_node_name'], 'public' => isset( $values['club_node_public'] ) ? (int) $values['club_node_public'] : 0), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
|
||||
Db::i()->update( 'core_clubs_node_map', array(
|
||||
'name' => $values['club_node_name'],
|
||||
'public' => isset( $values['club_node_public'] ) ? (int) $values['club_node_public'] : 0,
|
||||
'creators' => isset( $values['club_node_creators'] ) ? (int) $values['club_node_creators'] : 0
|
||||
), array( 'club_id=? AND node_class=? AND node_id=?', $club->id, $nodeClass, $this->_id ) );
|
||||
}
|
||||
|
||||
Lang::saveCustom( $itemClass::$application, static::$titleLangPrefix . $this->_id, $values['club_node_name'] );
|
||||
@@ -210,6 +265,15 @@ trait ClubContainer
|
||||
Db::i()->insert( 'core_follow', $follows );
|
||||
}
|
||||
}
|
||||
|
||||
/* At the very end, call the post-save in extensions */
|
||||
if( static::$canBeExtended )
|
||||
{
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
{
|
||||
$values = $extension->clubFormPostSave( $this, $club, $values );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,19 +322,26 @@ trait ClubContainer
|
||||
}
|
||||
elseif ( in_array( $k, array( 'add', 'edit', 'reply', 'review' ) ) )
|
||||
{
|
||||
switch ( $club->type )
|
||||
if( in_array( $k, [ 'add', 'edit' ] ) and $this->creators() == 1 )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
$insert[ 'perm_' . $v ] = 'ca';
|
||||
break;
|
||||
case $club::TYPE_CLOSED:
|
||||
$insert[ 'perm_' . $v ] = ( $this->isPublic() == 2 ) ? "*" : "cm,c{$club->id}";
|
||||
break;
|
||||
case $club::TYPE_OPEN:
|
||||
case $club::TYPE_PRIVATE:
|
||||
case $club::TYPE_READONLY:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
$insert[ 'perm_' . $v ] = 'cm';
|
||||
}
|
||||
else
|
||||
{
|
||||
switch ( $club->type )
|
||||
{
|
||||
case $club::TYPE_PUBLIC:
|
||||
$insert[ 'perm_' . $v ] = 'ca';
|
||||
break;
|
||||
case $club::TYPE_CLOSED:
|
||||
$insert[ 'perm_' . $v ] = ( $this->isPublic() == 2 ) ? "*" : "cm,c{$club->id}";
|
||||
break;
|
||||
case $club::TYPE_OPEN:
|
||||
case $club::TYPE_PRIVATE:
|
||||
case $club::TYPE_READONLY:
|
||||
$insert[ 'perm_' . $v ] = "cm,c{$club->id}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -450,4 +521,21 @@ trait ClubContainer
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Who can create content in this node?
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function creators() : int
|
||||
{
|
||||
try
|
||||
{
|
||||
return (int) Db::i()->select( 'creators', 'core_clubs_node_map', array( 'club_id=? AND node_class=? AND node_id=?', $this->club()->id, get_called_class(), $this->_id ) )->first();
|
||||
}
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ use function get_class;
|
||||
use function in_array;
|
||||
use function intval;
|
||||
use function is_array;
|
||||
use function method_exists;
|
||||
use function substr;
|
||||
use function ucfirst;
|
||||
|
||||
@@ -595,6 +596,15 @@ abstract class Comment extends Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Rebuild club stats? */
|
||||
if( $container = $item->containerWrapper() )
|
||||
{
|
||||
if ( IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) and $club = $container->club() )
|
||||
{
|
||||
$club->updateLastActivityAndItemCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1119,7 +1129,7 @@ abstract class Comment extends Content
|
||||
|
||||
/* 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 */
|
||||
if( IPS::classUsesTrait( $item, Hideable::class ) )
|
||||
if( $item and IPS::classUsesTrait( $item, Hideable::class ) )
|
||||
{
|
||||
if( $this->hidden() !== -1 AND $this->hidden() !== -2 AND $this->hidden() !== -3 )
|
||||
{
|
||||
@@ -1382,7 +1392,7 @@ abstract class Comment extends Content
|
||||
}
|
||||
|
||||
/* Work out the order */
|
||||
$order = $order ?: ( static::$databasePrefix . static::$databaseColumnMap['date'] . ' DESC' );
|
||||
$order = $order ?: ( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['date'] . ' DESC' );
|
||||
|
||||
/* Exclude hidden comments */
|
||||
if( $includeHiddenComments === Filter::FILTER_AUTOMATIC )
|
||||
@@ -1407,22 +1417,22 @@ abstract class Comment extends Content
|
||||
|
||||
if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['approved'] . '=?', 0 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'] . '=?', 0 );
|
||||
}
|
||||
elseif ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['hidden'] . '=?', 1 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'] . '=?', 1 );
|
||||
}
|
||||
}
|
||||
elseif ( IPS::classUsesTrait( get_called_class(), 'IPS\Content\Hideable' ) and ( $includeHiddenComments === Filter::FILTER_OWN_HIDDEN OR $includeHiddenComments === Filter::FILTER_PUBLIC_ONLY ) )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['approved'] . '=?', 1 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['approved'] . '=?', 1 );
|
||||
}
|
||||
elseif ( isset( static::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['hidden'] . '=?', 0 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['hidden'] . '=?', 0 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1904,7 +1914,7 @@ abstract class Comment extends Content
|
||||
$menu->add( $share );
|
||||
}
|
||||
|
||||
if( method_exists( $this, 'canRecognize') )
|
||||
if( IPS::classUsesTrait( $this, Recognizable::class ) )
|
||||
{
|
||||
if( $this->canRecognize() === TRUE )
|
||||
{
|
||||
@@ -2015,10 +2025,11 @@ abstract class Comment extends Content
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->ip_address AND $member->modPermission('can_use_ip_tools') AND $member->canAccessModule( Module::get( 'core', 'modcp' ) ) )
|
||||
if( isset( static::$databaseColumnMap['ip_address'] ) AND $member->modPermission('can_use_ip_tools') AND $member->canAccessModule( Module::get( 'core', 'modcp' ) ) )
|
||||
{
|
||||
$ipAddressColumn = static::$databaseColumnMap['ip_address'];
|
||||
$menu->addSeparator();
|
||||
$menu->add( new ContentMenuLink( url: Url::internal( 'app=core&module=modcp&controller=modcp&tab=ip_tools&ip=' . $this->ip_address), languageString: Member::loggedIn()->language()->addToStack( 'ip_address' ) . ' ' . $this->ip_address ) );
|
||||
$menu->add( new ContentMenuLink( url: Url::internal( 'app=core&module=modcp&controller=modcp&tab=ip_tools&ip=' . $this->$ipAddressColumn ), languageString: Member::loggedIn()->language()->addToStack( 'ip_address' ) . ' ' . $this->$ipAddressColumn ) );
|
||||
}
|
||||
|
||||
foreach( $this->ui( 'menuItems', array(), TRUE ) as $key => $link )
|
||||
|
||||
@@ -677,6 +677,8 @@ abstract class Content extends ActiveRecord
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_modaction', ['action' => $action, 'item' => $this->apiOutput()], $this->webhookFilters() );
|
||||
|
||||
/* These ones just need a property setting */
|
||||
if ( in_array( $action, array( 'pin', 'unpin', 'feature', 'unfeature', 'lock', 'unlock' ) ) )
|
||||
{
|
||||
@@ -1248,7 +1250,13 @@ abstract class Content extends ActiveRecord
|
||||
* @return void
|
||||
*/
|
||||
public function sendNotifications(): void
|
||||
{
|
||||
{
|
||||
/* Check the bridge */
|
||||
if ( ! Bridge::i()->sendNotifications( $this ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Send quote and mention notifications */
|
||||
$sentTo = $this->sendQuoteAndMentionNotifications();
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ class Controller extends CoverPhotoController
|
||||
|
||||
if( Application::appIsEnabled( 'cloud' ) )
|
||||
{
|
||||
Output::setCacheTime( CloudApplication::getCacheDateForPage( $item->mapped('updated'), ( ( $item::$application === 'forums' and $item->isArchived() ) ? 'archived' : 'content' ) ) );
|
||||
Output::setCacheTime( CloudApplication::getCacheDateForPage( $item->mapped('updated') ?? 0, ( ( $item::$application === 'forums' and $item->isArchived() ) ? 'archived' : 'content' ) ) );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
@@ -574,6 +574,7 @@ class Controller extends CoverPhotoController
|
||||
$showing = intval( Request::i()->showing ?: 0 );
|
||||
foreach ( $newComments as $newComment )
|
||||
{
|
||||
$output[] = $newComment->html();
|
||||
$lastId = ( $newComment->$commentIdColumn > $lastId ) ? $newComment->$commentIdColumn : $lastId;
|
||||
if ( ++$showing >= $perPage )
|
||||
{
|
||||
@@ -1034,6 +1035,9 @@ class Controller extends CoverPhotoController
|
||||
$type = $poll->poll_closed ? 'closed' : 'opened';
|
||||
Session::i()->modLog( 'modlog__poll_' . $type, array( $item->url()->__toString() => FALSE, $item->mapped( 'title' ) => FALSE ), $item );
|
||||
|
||||
/* Fire an event */
|
||||
Event::fire( 'onStateChange', $poll, [ $type ] );
|
||||
|
||||
Output::i()->redirect( $item->url(), $redirectMessage );
|
||||
}
|
||||
else
|
||||
@@ -4077,7 +4081,7 @@ class Controller extends CoverPhotoController
|
||||
try
|
||||
{
|
||||
$object = $commentClass::loadAndCheckPerms( $id );
|
||||
if ( Request::i()->modaction === 'delete' AND $object->isFeatured() )
|
||||
if ( IPS::classUsesTrait( $object, Featurable::class ) and Request::i()->modaction === 'delete' and $object->isFeatured() )
|
||||
{
|
||||
$item->unfeatureComment( $object );
|
||||
}
|
||||
@@ -4806,7 +4810,8 @@ class Controller extends CoverPhotoController
|
||||
}
|
||||
|
||||
/* Check that the object can be promoted */
|
||||
if( ! $objectToPromote->canFeature() )
|
||||
/* @var Content $objectToPromote */
|
||||
if( ! $objectToPromote->canFeature() and !$objectToPromote->isFeatured() )
|
||||
{
|
||||
Output::i()->error( 'no_promote_permission', '2S356/G', 403, '' );
|
||||
}
|
||||
@@ -4909,7 +4914,7 @@ class Controller extends CoverPhotoController
|
||||
Output::i()->error( 'page_not_found', '2C356/5', 404, '' );
|
||||
}
|
||||
|
||||
if ( ! $promote or $promote->added_by != Member::loggedIn()->member_id )
|
||||
if ( ! $promote or !$objectToEdit->canFeature() )
|
||||
{
|
||||
Output::i()->error( 'no_module_permission', '2C366/2', 403, '' );
|
||||
}
|
||||
@@ -4932,7 +4937,7 @@ class Controller extends CoverPhotoController
|
||||
{
|
||||
if ( $images = $item->contentImages( 20 ) )
|
||||
{
|
||||
$form->addHtml( Theme::i()->getTemplate( 'promote' )->promoteDialogImages( $images, $promote ) );
|
||||
$form->addHtml( Theme::i()->getTemplate( 'promote', 'core' )->promoteDialogImages( $images, $promote ) );
|
||||
}
|
||||
}
|
||||
catch( BadMethodCallException ) { }
|
||||
|
||||
@@ -128,16 +128,8 @@ trait Featurable
|
||||
*/
|
||||
public static function featured( $limit=10, $order='RAND()', $thisClassOnly=true ): ActiveRecordIterator
|
||||
{
|
||||
$where = [ [ 'promote_hide=0' ] ];
|
||||
|
||||
if ( $thisClassOnly )
|
||||
{
|
||||
$where[] = [ 'promote_class=?', get_called_class() ];
|
||||
}
|
||||
|
||||
$where = array( array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
|
||||
|
||||
if ( in_array( 'IPS\Content\FuturePublishing', class_implements( get_called_class() ) ) )
|
||||
{
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
|
||||
@@ -293,6 +293,11 @@ trait Helpful
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( ! $member->group['gbw_can_mark_helpful'] )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $member === $this->author() )
|
||||
{
|
||||
return FALSE;
|
||||
|
||||
+53
-29
@@ -12,9 +12,11 @@ namespace IPS\Content;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use DateInvalidOperationException;
|
||||
use Exception;
|
||||
use IPS\Application;
|
||||
use IPS\Content\Search\SearchContent;
|
||||
use IPS\Platform\Bridge;
|
||||
use RuntimeException;
|
||||
use BadMethodCallException;
|
||||
use OutOfRangeException;
|
||||
@@ -129,11 +131,12 @@ trait Hideable
|
||||
$column = static::$databaseColumnMap[ 'hidden' ] ?? static::$databaseColumnMap[ 'approved' ];
|
||||
$this->$column = -2;
|
||||
$this->save();
|
||||
|
||||
|
||||
$item = ( $this instanceof Comment ) ? $this->item() : $this;
|
||||
if ( $this instanceof Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
|
||||
|
||||
/* Update last comment stuff */
|
||||
$item->resyncLastComment();
|
||||
|
||||
@@ -164,7 +167,21 @@ trait Hideable
|
||||
$item->container()->save();
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException ){}
|
||||
catch( BadMethodCallException ){}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If this is an item, just update the container counts */
|
||||
try
|
||||
{
|
||||
$containerColumn = ( $originalState === 1 ) ? '_unapprovedItems' : '_items';
|
||||
if( $item->container()->$containerColumn !== null )
|
||||
{
|
||||
$item->container()->$containerColumn = ( $item->container()->$containerColumn > 0 ) ? ( $item->container()->$containerColumn - 1 ) : 0;
|
||||
$item->container()->save();
|
||||
}
|
||||
}
|
||||
catch( BadMethodCallException ){}
|
||||
}
|
||||
|
||||
if ( IPS::classUsesTrait( $this, 'IPS\Content\Taggable' ) )
|
||||
@@ -395,15 +412,9 @@ trait Hideable
|
||||
$firstColumn = $commentClass::$databaseColumnMap['hidden'];
|
||||
$firstComment->$firstColumn = 2;
|
||||
}
|
||||
|
||||
Index::i()->index( $firstComment );
|
||||
}
|
||||
|
||||
Index::i()->index( ( static::$firstCommentRequired AND $firstComment ) ? $firstComment : $this );
|
||||
Index::i()->indexSingleItem( $this );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Either this is a comment / review, or the item doesn't support comments or reviews, so we can just reindex it now. */
|
||||
Index::i()->index( $this );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +465,12 @@ trait Hideable
|
||||
if( SearchContent::isSearchable( $this ) )
|
||||
{
|
||||
Index::i()->index( $this );
|
||||
|
||||
if ( $this instanceof Item )
|
||||
{
|
||||
/* Now re-index all the reviews and comments */
|
||||
Index::i()->indexSingleItem( $this );
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear the item stats */
|
||||
@@ -609,12 +626,6 @@ trait Hideable
|
||||
Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
|
||||
}
|
||||
|
||||
/* Update search index */
|
||||
if( SearchContent::isSearchable( $this ) )
|
||||
{
|
||||
Index::i()->index( $this );
|
||||
}
|
||||
|
||||
/* Update report center stuff */
|
||||
if ( IPS::classUsesTrait( $this, 'IPS\Content\Reportable' ) )
|
||||
{
|
||||
@@ -714,18 +725,24 @@ trait Hideable
|
||||
$column = $className::$databaseColumnMap['hidden'];
|
||||
$firstComment->$column = 0;
|
||||
}
|
||||
|
||||
Index::i()->index( $firstComment );
|
||||
}
|
||||
|
||||
Index::i()->index( ( static::$firstCommentRequired AND $firstComment ) ? $firstComment : $this );
|
||||
Index::i()->indexSingleItem( $this );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Either this is a comment / review, or the item doesn't support comments or reviews, so we can just reindex it now. */
|
||||
Index::i()->index( $this );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Update search index */
|
||||
if( SearchContent::isSearchable( $this ) )
|
||||
{
|
||||
Index::i()->index( $this );
|
||||
|
||||
if ( $this instanceof Item )
|
||||
{
|
||||
/* Now re-index all the reviews and comments */
|
||||
Index::i()->indexSingleItem( $this );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -919,14 +936,21 @@ trait Hideable
|
||||
$notification->recipients->attach( $member );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send Unapproved Notification
|
||||
*
|
||||
* @return void
|
||||
* @return void
|
||||
* @throws DateInvalidOperationException
|
||||
*/
|
||||
public function sendUnapprovedNotification(): void
|
||||
{
|
||||
/* Check the bridge */
|
||||
if ( ! Bridge::i()->sendUnapprovedNotification( $this ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$moderators = array( 'g' => array(), 'm' => array() );
|
||||
foreach(Db::i()->select( '*', 'core_moderators' ) AS $mod )
|
||||
{
|
||||
@@ -940,7 +964,7 @@ trait Hideable
|
||||
else
|
||||
{
|
||||
$perms = json_decode( $mod['perms'], TRUE );
|
||||
|
||||
|
||||
foreach ( array( 'canView' => 'can_view_hidden_', 'canApprove' => 'can_unhide_' ) as $varKey => $modPermKey )
|
||||
{
|
||||
if ( isset( $perms[ $modPermKey . 'content' ] ) AND $perms[ $modPermKey . 'content' ] )
|
||||
@@ -948,7 +972,7 @@ trait Hideable
|
||||
$$varKey = TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
try
|
||||
{
|
||||
$container = ( $this instanceof Comment ) ? $this->item()->container() : $this->container();
|
||||
|
||||
+75
-36
@@ -276,11 +276,8 @@ abstract class Item extends Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Now loop through and add all the elements to the form */
|
||||
foreach( UiExtension::i()->run( $item ?: get_called_class(), 'formElements', array( $container ) ) as $element )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
/* Extensions */
|
||||
static::extendForm( $form, $item, $container );
|
||||
|
||||
return $form;
|
||||
}
|
||||
@@ -296,6 +293,24 @@ abstract class Item extends Content
|
||||
return static::buildCreateForm( $this->containerWrapper(), $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracting this to a separate method so that we can make sure we
|
||||
* call all extensions everywhere it's necessary
|
||||
*
|
||||
* @param Form $form
|
||||
* @param Item|null $item
|
||||
* @param Model|null $container
|
||||
* @return void
|
||||
*/
|
||||
public static function extendForm( Form $form, ?Item $item=null, ?Model $container=null ) : void
|
||||
{
|
||||
/* Now loop through and add all the elements to the form */
|
||||
foreach( UiExtension::i()->run( $item ?: get_called_class(), 'formElements', array( $container ) ) as $element )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generic object
|
||||
*
|
||||
@@ -1101,6 +1116,15 @@ abstract class Item extends Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Rebuild club stats, but only if we don't enforce a comment as Comment::postCreate() will update */
|
||||
if( ! static::$firstCommentRequired and $container = $this->containerWrapper() )
|
||||
{
|
||||
if ( IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) and $club = $container->club() )
|
||||
{
|
||||
$club->updateLastActivityAndItemCount();
|
||||
}
|
||||
}
|
||||
|
||||
Event::fire( 'onCreateOrEdit', $this, array( $values, TRUE ) );
|
||||
|
||||
$this->ui( 'formPostSave', array( $values ) );
|
||||
@@ -1230,6 +1254,8 @@ abstract class Item extends Content
|
||||
Event::fire( 'onCreateOrEdit', $this, array( $values ) );
|
||||
|
||||
$this->ui( 'formPostSave', array( $values ) );
|
||||
|
||||
Webhook::fire( str_replace( '\\', '', substr( get_called_class(), 3 ) ) . '_edit', $this, $this->webhookFilters() );
|
||||
}
|
||||
|
||||
/* Holds the old content for edit logging */
|
||||
@@ -2790,7 +2816,7 @@ abstract class Item extends Content
|
||||
* @param array $where Where clause
|
||||
* @param string|null $order MySQL ORDER BY clause (NULL to order by date)
|
||||
* @param int|array|null $limit Limit clause
|
||||
* @param string $permissionKey A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index or NULL to ignore permissions
|
||||
* @param string|null $permissionKey A key which has a value in the permission map (either of the container or of this class) matching a column ID in core_permission_index or NULL to ignore permissions
|
||||
* @param int|bool|null $includeHiddenItems Include hidden items? NULL to detect if currently logged in member has permission, -1 to return public content only, TRUE to return unapproved content and FALSE to only return unapproved content the viewing member submitted
|
||||
* @param int $queryFlags Select bitwise flags
|
||||
* @param Member|null $member The member (NULL to use currently logged in member)
|
||||
@@ -2807,7 +2833,7 @@ abstract class Item extends Content
|
||||
* @param array|null $location Array of item lat and long
|
||||
* @return ActiveRecordIterator|int
|
||||
*/
|
||||
public static function getItemsWithPermission( array $where=array(), string $order=NULL, int|array|null $limit=10, string $permissionKey='read', int|bool|null $includeHiddenItems= Filter::FILTER_AUTOMATIC, int $queryFlags=0, ?Member $member=null, bool $joinContainer=FALSE, bool $joinComments=FALSE, bool $joinReviews=FALSE, bool $countOnly=FALSE, ?array $joins=null, bool|Model $skipPermission=FALSE, bool $joinTags=TRUE, bool $joinAuthor=TRUE, bool $joinLastCommenter=TRUE, bool $showMovedLinks=FALSE, ?array $location=null ): ActiveRecordIterator|int
|
||||
public static function getItemsWithPermission( array $where=array(), string $order=NULL, int|array|null $limit=10, ?string $permissionKey='read', int|bool|null $includeHiddenItems= Filter::FILTER_AUTOMATIC, int $queryFlags=0, ?Member $member=null, bool $joinContainer=FALSE, bool $joinComments=FALSE, bool $joinReviews=FALSE, bool $countOnly=FALSE, ?array $joins=null, bool|Model $skipPermission=FALSE, bool $joinTags=TRUE, bool $joinAuthor=TRUE, bool $joinLastCommenter=TRUE, bool $showMovedLinks=FALSE, ?array $location=null ): ActiveRecordIterator|int
|
||||
{
|
||||
/* Are we trying to improve count performance? */
|
||||
$countShortcut = FALSE;
|
||||
@@ -2819,7 +2845,7 @@ abstract class Item extends Content
|
||||
$location['lat'] = number_format( $location['lat'], 6, '.', '' );
|
||||
$location['lon'] = number_format( $location['lon'], 6, '.', '' );
|
||||
|
||||
$where[] = array( static::$databasePrefix . 'latitude' . ' IS NOT NULL AND ' . static::$databasePrefix . 'longitude' . ' IS NOT NULL' );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . 'latitude' . ' IS NOT NULL AND ' . static::$databaseTable . '.' . static::$databasePrefix . 'longitude' . ' IS NOT NULL' );
|
||||
$having = array( 'distance < 500' );
|
||||
$order = 'distance ASC';
|
||||
}
|
||||
@@ -3079,7 +3105,7 @@ abstract class Item extends Content
|
||||
}
|
||||
}
|
||||
|
||||
$groupBy = ( $joinComments ? static::$databasePrefix . static::$databaseColumnId : NULL );
|
||||
$groupBy = ( $joinComments ? static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnId : NULL );
|
||||
/* @var $databaseColumnMap array */
|
||||
/* Build the select clause */
|
||||
if( $countOnly )
|
||||
@@ -3213,7 +3239,7 @@ abstract class Item extends Content
|
||||
|
||||
if ( isset( $location['lat'] ) and isset( $location['lon'] ) and is_numeric( $location['lat'] ) and is_numeric( $location['lon'] ) )
|
||||
{
|
||||
$selectClause .= ', ( 3959 * acos( cos( radians(' . $location['lat'] . ') ) * cos( radians( ' . static::$databasePrefix . 'latitude' . ' ) ) * cos( radians( ' . static::$databasePrefix . 'longitude' . ' ) - radians( ' . $location['lon'] . ' ) ) + sin( radians( ' . $location['lat'] . ' ) ) * sin( radians( ' . static::$databasePrefix . 'latitude' . ') ) ) ) AS distance';
|
||||
$selectClause .= ', ( 3959 * acos( cos( radians(' . $location['lat'] . ') ) * cos( radians( ' . static::$databaseTable . '.' . static::$databasePrefix . 'latitude' . ' ) ) * cos( radians( ' . static::$databaseTable . '.' . static::$databasePrefix . 'longitude' . ' ) - radians( ' . $location['lon'] . ' ) ) + sin( radians( ' . $location['lat'] . ' ) ) * sin( radians( ' . static::$databaseTable . '.' . static::$databasePrefix . 'latitude' . ') ) ) ) AS distance';
|
||||
}
|
||||
|
||||
if( $joinAuthor and isset( static::$databaseColumnMap['author'] ) )
|
||||
@@ -5938,7 +5964,7 @@ abstract class Item extends Content
|
||||
{
|
||||
if ( !isset( static::$itemCounts[ $_key ] ) )
|
||||
{
|
||||
$_count = static::getItemsWithPermission( array( array( static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id ) ), NULL, 1, 'read', Filter::FILTER_AUTOMATIC, 0, NULL, FALSE, FALSE, FALSE, TRUE );
|
||||
$_count = static::getItemsWithPermission( array( array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id ) ), NULL, 1, 'read', Filter::FILTER_AUTOMATIC, 0, NULL, FALSE, FALSE, FALSE, TRUE );
|
||||
|
||||
$_key = md5( get_class( $container ) . $container->_id );
|
||||
static::$itemCounts[ $_key ][ $container->_id ] = $_count;
|
||||
@@ -5966,14 +5992,14 @@ abstract class Item extends Content
|
||||
$commentClass = static::$commentClass;
|
||||
/* @var $databaseColumnMap array */
|
||||
static::$commentCounts[ $_key ] = iterator_to_array( Db::i()->select(
|
||||
'COUNT(*) AS count, ' . static::$databasePrefix . static::$databaseColumnMap['container'],
|
||||
'COUNT(*) AS count, ' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'],
|
||||
$commentClass::$databaseTable,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
static::$databasePrefix . static::$databaseColumnMap['container']
|
||||
)->join( static::$databaseTable, $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=' . static::$databasePrefix . static::$databaseColumnId )
|
||||
->setKeyField( static::$databasePrefix . static::$databaseColumnMap['container'] )
|
||||
static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container']
|
||||
)->join( static::$databaseTable, $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnId )
|
||||
->setKeyField( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] )
|
||||
->setValueField('count') );
|
||||
}
|
||||
|
||||
@@ -5999,14 +6025,14 @@ abstract class Item extends Content
|
||||
/* @var array $databaseColumnMap */
|
||||
$reviewClass = static::$commentClass;
|
||||
static::$reviewCounts[ $_key ] = iterator_to_array( Db::i()->select(
|
||||
'COUNT(*) AS count, ' . static::$databasePrefix . static::$databaseColumnMap['container'],
|
||||
'COUNT(*) AS count, ' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'],
|
||||
$reviewClass::$databaseTable,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
static::$databasePrefix . static::$databaseColumnMap['container']
|
||||
)->join( static::$databaseTable, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=' . static::$databasePrefix . static::$databaseColumnId )
|
||||
->setKeyField( static::$databasePrefix . static::$databaseColumnMap['container'] )
|
||||
static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container']
|
||||
)->join( static::$databaseTable, $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=' . static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnId )
|
||||
->setKeyField( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] )
|
||||
->setValueField('count') );
|
||||
}
|
||||
|
||||
@@ -7129,12 +7155,18 @@ abstract class Item extends Content
|
||||
|
||||
if( $member->modPermission('can_manage_deleted_content') AND $this->hidden() == -2 )
|
||||
{
|
||||
$restore = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restore' ) ), languageString: 'restore_as_visible', identifier: 'restore' );
|
||||
$restore->requiresConfirm( 'restore_as_visible_desc' );
|
||||
$menu->add( $restore );
|
||||
if( IPS::classUsesTrait( $this, Hideable::class ) and $this->canRestore() )
|
||||
{
|
||||
$restore = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restore' ) ), languageString: 'restore_as_visible', identifier: 'restore' );
|
||||
$restore->requiresConfirm( 'restore_as_visible_desc' );
|
||||
$menu->add( $restore );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restoreAsHidden' ) ), 'restore_as_hidden', identifier: 'restore_hidden' ) );
|
||||
}
|
||||
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'restoreAsHidden' ) ), 'restore_as_hidden', identifier: 'restore_hidden' ) );
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'delete', 'immediate' => 1 ) ), 'delete_immediately', identifier: 'delete_immediately' ) );
|
||||
if( $this->canDelete() )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( $this->url()->csrf()->setQueryString( array( 'do' => 'moderate', 'action' => 'delete', 'immediate' => 1 ) ), 'delete_immediately', identifier: 'delete_immediately' ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -7273,22 +7305,29 @@ abstract class Item extends Content
|
||||
{
|
||||
$menu->addSeparator();
|
||||
|
||||
$analytics = new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'analytics' ) ), languageString: 'analytics_and_stats' );
|
||||
$analytics->opensDialog( 'analytics_and_stats', 'large' );
|
||||
$menu->add( $analytics );
|
||||
if( IPS::classUsesTrait( $this, Statistics::class ) )
|
||||
{
|
||||
$analytics = new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'analytics' ) ), languageString: 'analytics_and_stats' );
|
||||
$analytics->opensDialog( 'analytics_and_stats', 'large' );
|
||||
$menu->add( $analytics );
|
||||
}
|
||||
|
||||
$menu->add( new ContentMenuLink( url:$this->url()->setQueryString( array( 'do' => 'modlog' ) ), languageString: 'moderation_history' , opensDialog: true ) );
|
||||
}
|
||||
|
||||
if( $this->canOnMessage( 'add', $member ) )
|
||||
if( IPS::classUsesTrait( $this, MetaData::class ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->setQueryString( array( 'do' => 'messageForm' ) ), languageString: 'add_message' , opensDialog: true ) );
|
||||
}
|
||||
if( $this->canOnMessage( 'add', $member ) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url()->setQueryString( array( 'do' => 'messageForm' ) ), languageString: 'add_message' , opensDialog: true ) );
|
||||
}
|
||||
|
||||
if( $this->canToggleItemModeration( $member ) )
|
||||
{
|
||||
$toggle = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'toggleItemModeration' ) ), languageString: $this->itemModerationEnabled() ?'disable_topic_moderation' : 'enable_topic_moderation' );
|
||||
$toggle->requiresConfirm( $this->itemModerationEnabled() ? 'disable_topic_moderation_confirm' : 'enable_topic_moderation_confirm' );
|
||||
$menu->add( $toggle );
|
||||
if( $this->canToggleItemModeration( $member ) )
|
||||
{
|
||||
$toggle = new ContentMenuLink( url: $this->url()->csrf()->setQueryString( array( 'do' => 'toggleItemModeration' ) ), languageString: $this->itemModerationEnabled() ?'disable_topic_moderation' : 'enable_topic_moderation' );
|
||||
$toggle->requiresConfirm( $this->itemModerationEnabled() ? 'disable_topic_moderation_confirm' : 'enable_topic_moderation_confirm' );
|
||||
$menu->add( $toggle );
|
||||
}
|
||||
}
|
||||
|
||||
if ( method_exists( $this,'availableSavedActions' )AND $actions = $this->availableSavedActions() )
|
||||
@@ -7443,7 +7482,7 @@ abstract class Item extends Content
|
||||
'title' => $this->title ? $this->title : NULL,
|
||||
'container' => $this->containerWrapper()?->apiOutput( $authorizedMember ),
|
||||
'author' => $this->author()->apiOutput( $authorizedMember ),
|
||||
'date' => DateTime::ts( $this->date )->rfc3339(),
|
||||
'date' => ( isset( static::$databaseColumnMap['date'] ) ? DateTime::ts( $this->mapped( 'date' ) )->rfc3339() : null ),
|
||||
'content' => $this->content(),
|
||||
'url' => (string) $this->url()
|
||||
);
|
||||
|
||||
+25
-12
@@ -13,8 +13,8 @@ namespace IPS\Content;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use Exception;
|
||||
use IPS\Node\CustomBadge;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\core\CustomBadge;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Db;
|
||||
use IPS\File;
|
||||
@@ -43,6 +43,8 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class Reaction extends Model
|
||||
{
|
||||
use CustomBadge;
|
||||
|
||||
/**
|
||||
* @brief Database Table
|
||||
*/
|
||||
@@ -103,6 +105,18 @@ class Reaction extends Model
|
||||
*/
|
||||
public static array $icons = array();
|
||||
|
||||
/**
|
||||
* @brief Disable the number overlay for custom badges
|
||||
*/
|
||||
public static bool $customBadgeNumberOverlay = false;
|
||||
|
||||
/**
|
||||
* @brief Toggle off these fields when generating a custom badge
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public static array $customBadgeToggles = [ 'reaction_icon' ];
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
@@ -111,10 +125,14 @@ class Reaction extends Model
|
||||
*/
|
||||
public function form( Form &$form ): void
|
||||
{
|
||||
/* Allow SVGs without the obscure hash removing the file extension */
|
||||
File::$safeFileExtensions[] = 'svg';
|
||||
|
||||
$form->add( new Translatable( 'reaction_title', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'reaction_title_' . $this->id : NULL ) ) ) );
|
||||
$form->add( new Radio( 'reaction_value', $this->id ? $this->value : 1, TRUE, array( 'options' => array( 1 => 'positive', 0 => 'neutral', -1 => 'negative' ) ) ) );
|
||||
CustomBadge::getRecordBadge( $this )
|
||||
->addBadgeFieldsToForm( $form, (bool) $this->use_custom, ['reaction_icon'], includeNumberOverlay: false );
|
||||
|
||||
parent::form( $form );
|
||||
|
||||
$form->add( new Upload( 'reaction_icon', $this->id ? File::get( 'core_Reaction', (string) $this->icon ) : NULL, false, array( 'image' => TRUE, 'storageExtension' => 'core_Reaction', 'storageContainer' => 'reactions', 'obscure' => FALSE ), null, null, null, 'reaction_icon' ) );
|
||||
}
|
||||
|
||||
@@ -131,12 +149,7 @@ class Reaction extends Model
|
||||
$this->save();
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'badge_use_custom', $values ) )
|
||||
{
|
||||
$values['use_custom'] = $values['badge_use_custom'];
|
||||
}
|
||||
|
||||
CustomBadge::saveRecordBadge( $this, CustomBadge::generateBadgeFromFormValues( $values ) );
|
||||
$values['use_custom'] = ( isset( $values['custombadge_use_custom'] ) and $values['custombadge_use_custom'] );
|
||||
|
||||
Lang::saveCustom( 'core', 'reaction_title_' . $this->id, $values['reaction_title'] );
|
||||
unset( $values['reaction_title'] );
|
||||
@@ -240,12 +253,12 @@ class Reaction extends Model
|
||||
*/
|
||||
public function get__icon(): mixed
|
||||
{
|
||||
if ( !isset( static::$icons[ $this->id ] ) )
|
||||
if( $this->use_custom and $badge = $this->getRecordBadge() )
|
||||
{
|
||||
static::$icons[ $this->id ] = $this->use_custom ? CustomBadge::getRecordBadge( $this )->url() : File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
return $badge->file()?->url;
|
||||
}
|
||||
|
||||
return static::$icons[ $this->id ];
|
||||
return File::get( 'core_Reaction', $this->_data['icon'] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,9 +92,9 @@ trait Recognizable
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
|
||||
/* Already recognized? */
|
||||
if ( isset( $this->recognized ) )
|
||||
if ( $this->recognized )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
@@ -137,7 +137,7 @@ trait Recognizable
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ( $member->modPermission('can_recognize_content') != '*' AND ! count( $member->modPermission('can_recognize_content_options') ) )
|
||||
if ( ! $member->modPermission('can_recognize_content') AND ! count( $member->modPermission('can_recognize_content_options') ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use IPS\Content\Item;
|
||||
use IPS\Content\Search\Index as SearchIndex;
|
||||
use IPS\Content\Search\Elastic\Exception as ElasticException;
|
||||
use IPS\Content;
|
||||
use IPS\Content\Taggable;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Http\Request\Exception as RequestException;
|
||||
@@ -58,8 +59,8 @@ class Index extends SearchIndex
|
||||
/**
|
||||
* @brief Elasticsearch version requirements
|
||||
*/
|
||||
const MINIMUM_VERSION = '7.2.0';
|
||||
const UNSUPPORTED_VERSION = '8.0.0';
|
||||
const MINIMUM_VERSION = '8.0.0';
|
||||
const UNSUPPORTED_VERSION = '9.0.0';
|
||||
|
||||
/**
|
||||
* @brief The server URL
|
||||
@@ -101,65 +102,63 @@ class Index extends SearchIndex
|
||||
$definition = array(
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'_doc' => 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' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $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_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
'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' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $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_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$response = Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
$response = Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new RuntimeException;
|
||||
}
|
||||
}
|
||||
catch( Exception )
|
||||
catch( Exception $e )
|
||||
{
|
||||
$response = Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
catch ( Exception $e )
|
||||
@@ -223,7 +222,7 @@ class Index extends SearchIndex
|
||||
|
||||
try
|
||||
{
|
||||
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
|
||||
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/' . $this->getIndexId( $object ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get()->decodeJson();
|
||||
if ( $r['found'] )
|
||||
{
|
||||
$existingData = $r['_source'];
|
||||
@@ -239,12 +238,12 @@ class Index extends SearchIndex
|
||||
|
||||
$resetLastComment = TRUE;
|
||||
}
|
||||
else if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
|
||||
else if ( $indexData['index_hidden'] !== 0 or ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] ) )
|
||||
{
|
||||
/* We have a new "last comment" */
|
||||
/* We have a new "last comment" or we just hid a comment in the feed so reset the last visible comment */
|
||||
$resetLastComment = TRUE;
|
||||
}
|
||||
|
||||
|
||||
/* Insert into index */
|
||||
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $object ) ), LONG_REQUEST_TIMEOUT )
|
||||
->setHeaders( array( 'Content-Type' => 'application/json' ) )
|
||||
@@ -298,7 +297,7 @@ class Index extends SearchIndex
|
||||
|
||||
if( $error = $this->getResponseError( $response ) )
|
||||
{
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['type'] );
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['reason'] ?? '' );
|
||||
}
|
||||
|
||||
$response = $response->decodeJson();
|
||||
@@ -374,8 +373,8 @@ class Index extends SearchIndex
|
||||
'index_comments' => $item->mapped('num_comments'),
|
||||
'index_reviews' => $item->mapped('num_reviews'),
|
||||
'index_participants' => $participants,
|
||||
'index_tags' => $item->tags(),
|
||||
'index_prefix' => $item->prefix(),
|
||||
'index_tags' => ( IPS::classUsesTrait( $item, Taggable::class ) ? $item->tags() : [] ),
|
||||
'index_prefix' => ( IPS::classUsesTrait( $item, Taggable::class ) ? $item->prefix() : null ),
|
||||
'index_item_solved' => $isSolved
|
||||
);
|
||||
}
|
||||
@@ -444,6 +443,11 @@ class Index extends SearchIndex
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
|
||||
if( $error = $this->getResponseError( $r ) )
|
||||
{
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['reason'] ?? '' );
|
||||
}
|
||||
}
|
||||
catch ( RequestException $e )
|
||||
{
|
||||
@@ -455,9 +459,9 @@ class Index extends SearchIndex
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param Content $object Item to add
|
||||
* @return mixed
|
||||
* @return string
|
||||
*/
|
||||
public function getIndexId( Content $object ): mixed
|
||||
public function getIndexId( Content $object ): string
|
||||
{
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
return strtolower( str_replace( '\\', '_', substr( get_class( $object ), 4 ) ) ) . '-' . $object->$databaseColumnId;
|
||||
@@ -517,14 +521,14 @@ class Index extends SearchIndex
|
||||
try
|
||||
{
|
||||
$indexId = strtolower( str_replace( '\\', '_', substr( $class, 4 ) ) ) . '-' . $id;
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $indexId ) )->delete();
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/' . $indexId ) )->delete();
|
||||
|
||||
if ( is_subclass_of( $class, 'IPS\Content\Item' ) )
|
||||
{
|
||||
if ( isset( $class::$commentClass ) )
|
||||
{
|
||||
$commentClass = $class::$commentClass;
|
||||
$response = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
$response = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
@@ -547,7 +551,7 @@ class Index extends SearchIndex
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
{
|
||||
$reviewClass = $class::$reviewClass;
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'must' => array(
|
||||
@@ -619,13 +623,13 @@ class Index extends SearchIndex
|
||||
);
|
||||
}
|
||||
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => $query
|
||||
) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'term' => array(
|
||||
'index_class' => $class
|
||||
@@ -730,7 +734,7 @@ class Index extends SearchIndex
|
||||
|
||||
if ( count( $updates ) )
|
||||
{
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'source' => implode( ' ', $updates ),
|
||||
'lang' => 'painless',
|
||||
@@ -751,7 +755,7 @@ class Index extends SearchIndex
|
||||
)
|
||||
);
|
||||
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'source' => "ctx._source.index_permissions.add( 'm' + ctx._source.index_author );",
|
||||
'lang' => 'painless'
|
||||
@@ -781,7 +785,7 @@ class Index extends SearchIndex
|
||||
);
|
||||
}
|
||||
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false', 'scroll_size' => Settings::i()->search_index_maxresults ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'script' => array(
|
||||
'source' => "ctx._source.index_hidden = params.newHiddenStatus;",
|
||||
'lang' => 'painless',
|
||||
@@ -847,7 +851,7 @@ class Index extends SearchIndex
|
||||
|
||||
try
|
||||
{
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/' . $this->getIndexId( $item ) . '/_update' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'doc' => $update
|
||||
) ) );
|
||||
|
||||
@@ -879,7 +883,7 @@ class Index extends SearchIndex
|
||||
{
|
||||
try
|
||||
{
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_delete_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
'query' => array(
|
||||
'range' => array(
|
||||
'index_date_updated' => array(
|
||||
@@ -941,7 +945,12 @@ class Index extends SearchIndex
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
|
||||
|
||||
if( $error = $this->getResponseError( $r ) )
|
||||
{
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['reason'] ?? '' );
|
||||
}
|
||||
|
||||
/* Get the latest comment */
|
||||
$itemClass = NULL;
|
||||
foreach ( $classes as $class )
|
||||
@@ -980,6 +989,11 @@ class Index extends SearchIndex
|
||||
)
|
||||
) ) );
|
||||
|
||||
if( $error = $this->getResponseError( $r ) )
|
||||
{
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['reason'] ?? '' );
|
||||
}
|
||||
|
||||
/* And set the updated time on the main item (done as _update_by_query because it might not exist if the first comment is required) */
|
||||
$indexDataForLastComment = $this->indexData( $lastComment );
|
||||
$r = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_update_by_query' )->setQueryString( array( 'conflicts' => 'proceed', 'wait_for_completion' => 'false' ) ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->post( json_encode( array(
|
||||
@@ -1013,6 +1027,11 @@ class Index extends SearchIndex
|
||||
)
|
||||
)
|
||||
) ) );
|
||||
|
||||
if( $error = $this->getResponseError( $r ) )
|
||||
{
|
||||
throw new ElasticException( $error['type'] . ' ' . $error['reason'] ?? '' );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch ( OutOfRangeException ) {}
|
||||
@@ -1054,7 +1073,7 @@ class Index extends SearchIndex
|
||||
);
|
||||
|
||||
/* Get the count */
|
||||
$count = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
$count = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'size' => 0,
|
||||
'query' => $query
|
||||
) ) )->decodeJson();
|
||||
@@ -1065,7 +1084,7 @@ class Index extends SearchIndex
|
||||
}
|
||||
|
||||
/* Now get the unique item ids */
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'aggs' => array(
|
||||
'itemIds' => array(
|
||||
'terms' => array(
|
||||
@@ -1107,7 +1126,7 @@ class Index extends SearchIndex
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'query' => array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
@@ -1156,7 +1175,7 @@ class Index extends SearchIndex
|
||||
{
|
||||
try
|
||||
{
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_doc/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
$results = Index::request( $this->url->setPath( $this->url->data[ Url::COMPONENT_PATH ] . '/_search' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->get( json_encode( array(
|
||||
'size' => 1,
|
||||
'sort' => array( array( 'index_date_updated' => 'asc' ) )
|
||||
) ) )->decodeJson();
|
||||
|
||||
@@ -56,10 +56,10 @@ class MassIndexer extends Index
|
||||
$this->data[] = array(
|
||||
'index' => array(
|
||||
'_index' => trim( $this->url->data[ Url::COMPONENT_PATH ], '/' ),
|
||||
'_type' => '_doc',
|
||||
'_id' => $this->getIndexId( $object ),
|
||||
)
|
||||
);
|
||||
|
||||
$this->data[] = $indexData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,9 +348,9 @@ class Query extends SearchQuery
|
||||
|
||||
/**
|
||||
* Stores the "more like this" Content object for search()
|
||||
* @param array|null $moreLikeThis
|
||||
* @param string|null $moreLikeThis
|
||||
*/
|
||||
protected array|null $moreLikeThis = NULL;
|
||||
protected string|null $moreLikeThis = NULL;
|
||||
|
||||
/**
|
||||
* Filter by more like this
|
||||
@@ -924,11 +924,13 @@ class Query extends SearchQuery
|
||||
|
||||
if ( $start )
|
||||
{
|
||||
$range['gt'] = $start->getTimestamp();
|
||||
/* Numeric values are treated as milliseconds since epoch, not seconds https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html */
|
||||
$range['gt'] = (string) $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$range['lt'] = $end->getTimestamp();
|
||||
/* Numeric values are treated as milliseconds since epoch, not seconds https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html */
|
||||
$range['lt'] = (string) $end->getTimestamp();
|
||||
}
|
||||
|
||||
if ( $range )
|
||||
@@ -954,11 +956,13 @@ class Query extends SearchQuery
|
||||
|
||||
if ( $start )
|
||||
{
|
||||
$range['gt'] = $start->getTimestamp();
|
||||
/* Numeric values are treated as milliseconds since epoch, not seconds https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html */
|
||||
$range['gt'] = (string) $start->getTimestamp();
|
||||
}
|
||||
if ( $end )
|
||||
{
|
||||
$range['lt'] = $end->getTimestamp();
|
||||
/* Numeric values are treated as milliseconds since epoch, not seconds https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html */
|
||||
$range['lt'] = (string) $end->getTimestamp();
|
||||
}
|
||||
|
||||
if ( $range )
|
||||
|
||||
@@ -36,6 +36,7 @@ use function chr;
|
||||
use function defined;
|
||||
use function get_class;
|
||||
use function intval;
|
||||
use function rtrim;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -544,9 +545,9 @@ abstract class Index extends Singleton
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param Content $object Item to add
|
||||
* @return mixed
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getIndexId( Content $object ): mixed;
|
||||
abstract public function getIndexId( Content $object ): string;
|
||||
|
||||
/**
|
||||
* Remove item
|
||||
|
||||
@@ -119,8 +119,14 @@ class Index extends SearchIndex
|
||||
|
||||
$resetLastComment = TRUE;
|
||||
}
|
||||
else if ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] )
|
||||
else if ( $indexData['index_hidden'] !== 0 or ( $indexData['index_is_last_comment'] and $indexData['index_item_id'] ) )
|
||||
{
|
||||
if ( $indexData['index_hidden'] !== 0 )
|
||||
{
|
||||
/* We've hidden a comment, so we need to reset the last comment flag */
|
||||
$resetLastComment = true;
|
||||
}
|
||||
|
||||
$classes = array( $class );
|
||||
|
||||
/* If this is the latest comment, unflag what was set before on both item and comment */
|
||||
@@ -248,9 +254,9 @@ class Index extends SearchIndex
|
||||
* Retrieve the search ID for an item
|
||||
*
|
||||
* @param Content $object Item to add
|
||||
* @return mixed
|
||||
* @return string
|
||||
*/
|
||||
public function getIndexId( Content $object ): mixed
|
||||
public function getIndexId( Content $object ): string
|
||||
{
|
||||
$databaseColumnId = $object::$databaseColumnId;
|
||||
return Db::i()->select( 'index_id', 'core_search_index', array( 'index_class=? AND index_object_id=?', get_class( $object ),$object->$databaseColumnId ) )->first();
|
||||
@@ -598,7 +604,7 @@ class Index extends SearchIndex
|
||||
Db::i()->update( 'core_search_index', array( 'index_is_last_comment' => 0 ), array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? AND index_is_last_comment=1', $indexItemId ) );
|
||||
try
|
||||
{
|
||||
$latest = Db::i()->select( 'index_object_id, index_date_updated, index_class', 'core_search_index', array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
|
||||
$latest = Db::i()->select( 'index_object_id, index_date_updated, index_class, index_date_commented', 'core_search_index', array( Db::i()->in( 'index_class', $classes ) . ' AND index_item_id=? and index_hidden=0', $indexItemId ), 'index_date_created DESC', array( 0, 1 ) )->first();
|
||||
}
|
||||
catch( Exception )
|
||||
{
|
||||
|
||||
@@ -486,16 +486,19 @@ class Query extends \IPS\Content\Search\Query
|
||||
/* IN is not a SIN. It's been a long day */
|
||||
$where[] = Db::i()->in( 'index_author', $followed );
|
||||
}
|
||||
else
|
||||
else if ( ! $includeItems and ! $includeContainers )
|
||||
{
|
||||
/* There are no results */
|
||||
/* There are no results, and we only wanted members they followed */
|
||||
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
|
||||
}
|
||||
}
|
||||
catch( UnderflowException )
|
||||
{
|
||||
/* There are no results */
|
||||
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
|
||||
if ( ! $includeItems and ! $includeContainers )
|
||||
{
|
||||
/* There are no results and we only wanted members they followed */
|
||||
$this->where[] = "1=2 /*Filter by followed returned nothing*/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ abstract class SearchContentAbstract
|
||||
*/
|
||||
public function searchIndexContent(): string
|
||||
{
|
||||
return $this->object->mapped('content');
|
||||
return (string) $this->object->mapped('content');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace IPS\Content;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application;
|
||||
use IPS\core\ShareLinks\Service;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Http\Url;
|
||||
@@ -36,7 +37,7 @@ abstract class ShareServices
|
||||
*/
|
||||
public static function services(): array
|
||||
{
|
||||
return [
|
||||
$return = [
|
||||
'Bluesky' => 'IPS\Content\ShareServices\Bluesky',
|
||||
'Facebook' => 'IPS\Content\ShareServices\Facebook',
|
||||
'Linkedin' => 'IPS\Content\ShareServices\Linkedin',
|
||||
@@ -45,6 +46,15 @@ abstract class ShareServices
|
||||
'Email' => 'IPS\Content\ShareServices\Email',
|
||||
'X' => 'IPS\Content\ShareServices\X',
|
||||
];
|
||||
|
||||
/* Load any extensions */
|
||||
foreach ( Application::allExtensions( 'core', 'ShareServices', FALSE, 'core' ) as $key => $extension )
|
||||
{
|
||||
$bits = explode( "_", $key );
|
||||
$return[ $bits[1] ] = $extension::class;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -657,7 +657,7 @@ trait Statistics
|
||||
|
||||
$return[ $k ]['commentUrl'] = (string) $url->url();
|
||||
}
|
||||
catch ( LogicException|BadMethodCallException $e ) { }
|
||||
catch ( LogicException | OutOfRangeException $e ) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+66
-2
@@ -19,7 +19,6 @@ use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application;
|
||||
use IPS\Content;
|
||||
use IPS\core\FrontNavigation;
|
||||
use IPS\Data\Store;
|
||||
use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
@@ -135,6 +134,11 @@ class Tag extends Model
|
||||
*/
|
||||
protected function get__badge(): ?array
|
||||
{
|
||||
if( $this->mergeInProgress() )
|
||||
{
|
||||
return [ 0 => 'ipsBadge ipsBadge--intermediary', 1 => null, 2 => Member::loggedIn()->language()->addToStack( 'tag_merge_processing' ) ];
|
||||
}
|
||||
|
||||
$totalItems = array_sum( $this->totals );
|
||||
$text = Member::loggedIn()->language()->addToStack( 'tag_total_items', true, [ 'pluralize' => [ $totalItems ] ] );
|
||||
Member::loggedIn()->language()->parseOutputForDisplay( $text );
|
||||
@@ -145,6 +149,22 @@ class Tag extends Model
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is enabled
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__enabled(): ?bool
|
||||
{
|
||||
if( $this->mergeInProgress() )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::get__enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get Title
|
||||
*
|
||||
@@ -174,6 +194,17 @@ class Tag extends Model
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get Icon for tree
|
||||
*
|
||||
* @note Return the class for the icon (e.g. 'globe', the 'fa fa-' is added automatically so you do not need this here)
|
||||
* @return mixed
|
||||
*/
|
||||
protected function get__icon(): mixed
|
||||
{
|
||||
return $this->mergeInProgress() ? 'lock' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -191,6 +222,33 @@ class Tag extends Model
|
||||
$this->_data['totals'] = ( is_array( $val ) and count( $val ) ) ? json_encode( $val ) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this node currently queued for deleting or moving content OR is it the target of content queued to be moved from another node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteOrMoveQueued(): bool
|
||||
{
|
||||
if( $this->mergeInProgress() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::deleteOrMoveQueued();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the tag has been flagged for merging.
|
||||
* We need to flag it because this can take a while if there
|
||||
* are a lot of items, and we don't want anyone messing with it
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function mergeInProgress() : bool
|
||||
{
|
||||
return isset( $this->totals['merge'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
@@ -340,6 +398,12 @@ class Tag extends Model
|
||||
*/
|
||||
public function recount() : void
|
||||
{
|
||||
/* If we're in middle of a merge, do nothing */
|
||||
if( $this->mergeInProgress() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$totals = [];
|
||||
$this->last_used = null;
|
||||
foreach( Db::i()->select( 'tag_meta_app, tag_meta_area, count(tag_meta_id) as total, max(tag_added) as tag_added', 'core_tags', [ 'tag_text=?', $this->text ], null, null, [ 'tag_meta_app', 'tag_meta_area' ] ) as $row )
|
||||
@@ -545,7 +609,7 @@ class Tag extends Model
|
||||
public function coverPhoto(): mixed
|
||||
{
|
||||
$photo = parent::coverPhoto();
|
||||
$photo->editable = ( Member::loggedIn()->isAdmin() and Member::loggedIn()->hasAcpRestriction( 'core', 'discovery', 'tags_manage' ) );
|
||||
$photo->editable = Member::loggedIn()->modPermission( 'can_edit_tags' );
|
||||
$photo->overlay = Theme::i()->getTemplate( 'tags', 'core', 'front' )->coverPhotoOverlay( $this );
|
||||
return $photo;
|
||||
}
|
||||
|
||||
@@ -200,6 +200,9 @@ trait Taggable
|
||||
$set = array( $set );
|
||||
}
|
||||
|
||||
/* Weed out duplicates */
|
||||
$set = array_unique( $set );
|
||||
|
||||
$insert = [];
|
||||
foreach ( $set as $key => $tag )
|
||||
{
|
||||
@@ -346,7 +349,7 @@ trait Taggable
|
||||
|
||||
if ( count( $options['autocomplete']['source'] ) )
|
||||
{
|
||||
$containerClass = static::$containerNodeClass;
|
||||
$containerClass = static::$containerNodeClass ?? null;
|
||||
$containerFieldName = static::$formLangPrefix . 'container';
|
||||
$thisClass = get_called_class();
|
||||
return new Text( static::$formLangPrefix . 'tags', $item ? ( $item->prefix() ? array_merge( array( 'prefix' => $item->prefix() ), $item->tags() ) : $item->tags() ) : array(), ( Settings::i()->tags_min and Settings::i()->tags_min_req ) ? ( $container ? TRUE : NULL ) : FALSE, $options, function ( $val ) use ( $container, $containerClass, $containerFieldName, $thisClass ) {
|
||||
@@ -354,7 +357,7 @@ trait Taggable
|
||||
{
|
||||
if ( !$container )
|
||||
{
|
||||
if( $containerClass )
|
||||
if( $containerClass !== null )
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -225,7 +225,8 @@ abstract class Widget extends PermissionCache implements Customizable
|
||||
}
|
||||
|
||||
/* Number to show */
|
||||
$return['show'] = new Number( 'widget_feed_show', $this->configuration['widget_feed_show'] ?? 5, TRUE );
|
||||
$return['show'] = new Number( 'widget_feed_show', $this->configuration['widget_feed_show'] ?? 5, TRUE );
|
||||
$return['offset'] = new Number( 'widget_feed_offset', $this->configuration['widget_feed_offset'] ?? 0, false );
|
||||
|
||||
/* Date restrict */
|
||||
$options = array( 'unlimited' => -1 );
|
||||
@@ -503,13 +504,16 @@ abstract class Widget extends PermissionCache implements Customizable
|
||||
}
|
||||
}
|
||||
|
||||
$offset = $this->configuration['widget_feed_offset'] ?? 0;
|
||||
$limit = ( isset( $this->configuration['widget_feed_show'] ) AND $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5;
|
||||
|
||||
/* 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'] ) ? static::$skipPermissions : 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::$databaseTable . '.' . $class::$databasePrefix . $this->configuration['widget_feed_sort_on'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) ) : ( $class::$databaseTable . '.' . $class::$databasePrefix . $class::$databaseColumnMap['updated'] . ' DESC' ), /* Order */
|
||||
|
||||
( isset( $this->configuration['widget_feed_show'] ) AND $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5, /* Limit */
|
||||
[ $offset, $limit ], /* Limit */
|
||||
'read', /* Permission key to check against */
|
||||
$hidden, /* Whether or not to include hidden items */
|
||||
$class::SELECT_IDS_FIRST,
|
||||
|
||||
@@ -257,6 +257,7 @@ abstract class WidgetComment extends PermissionCache implements Customizable
|
||||
|
||||
/* Number to show */
|
||||
$return['show'] = new Number( 'widget_feed_show', $this->configuration['widget_feed_show'] ?? 5, TRUE );
|
||||
$return['offset'] = new Number( 'widget_feed_offset', $this->configuration['widget_feed_offset'] ?? 0, false );
|
||||
|
||||
/* Sort */
|
||||
$return['sort'] = new Select( 'widget_feed_sort_dir', $this->configuration['widget_feed_sort_dir'] ?? 'desc', FALSE, array(
|
||||
@@ -483,10 +484,13 @@ abstract class WidgetComment extends PermissionCache implements Customizable
|
||||
}
|
||||
}
|
||||
|
||||
$offset = $this->configuration['widget_feed_offset'] ?? 0;
|
||||
$limit = ( isset( $this->configuration['widget_feed_show'] ) AND $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5;
|
||||
|
||||
$items = $class::getItemsWithPermission(
|
||||
$this->buildWhere(),
|
||||
( isset( $this->configuration['widget_feed_sort_on'] ) and isset( $this->configuration['widget_feed_sort_dir'] ) ) ? ( $class::$databasePrefix . $class::$databaseColumnMap['date'] . ' ' . $this->configuration['widget_feed_sort_dir'] ) : NULL,
|
||||
( isset( $this->configuration['widget_feed_show'] ) AND $this->configuration['widget_feed_show'] ) ? $this->configuration['widget_feed_show'] : 5,
|
||||
[ $offset, $limit ],
|
||||
( !isset( $this->configuration['widget_feed_use_perms'] ) or $this->configuration['widget_feed_use_perms'] ) ? static::$permissionCheck : NULL,
|
||||
$hidden
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user