Version 5.0.6
This commit is contained in:
1 parent
a3a4a9ccbd
commit
b89858b2ff
1214 files changed
+92828
-14859
No files matched your search
+3
@@ -12,3 +12,6 @@ represents (i.e. the id of the row in the database). Be sure to add a
|
||||
seperator (like an underscore) at the end. Warning: this directive will
|
||||
not work unless %Attr.IDPrefix is set to a non-empty value!
|
||||
--# vim: et sw=4 sts=4
|
||||
|
||||
|
||||
# 3053798PNZ0EJZ
|
||||
Vendored
+3
@@ -1 +1,4 @@
|
||||
patreon: FlorentMorselli
|
||||
|
||||
|
||||
# 315662YNVPFR37N8S30
|
||||
Vendored
+3
@@ -1,3 +1,6 @@
|
||||
Please do not submit any Pull Requests here. It will be automatically closed.
|
||||
|
||||
You should submit it here: https://github.com/web-token/jwt-framework/pulls
|
||||
|
||||
|
||||
# 921JAOJGJ
|
||||
+381
-322
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,7 @@ namespace IPS\Application;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Application;
|
||||
use RecursiveFilterIterator;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
@@ -34,6 +35,13 @@ class BuilderFilter extends RecursiveFilterIterator
|
||||
*/
|
||||
public function accept(): bool
|
||||
{
|
||||
foreach( Application::allExtensions( 'core', 'Build' ) as $builder )
|
||||
{
|
||||
if( !$builder->accept( $this ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !( $this->isDir() && in_array( $this->getFilename(), $this->getDirectoriesToIgnore() ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -206,14 +206,17 @@ class DateTime extends \DateTime
|
||||
* @param bool $capitalize TRUE if by itself, FALSE if in the middle of a sentence
|
||||
* @param bool $short Whether or not to use the short form
|
||||
* @param Lang|Member|null $memberOrLanguage The language or member to use, or NULL for currently logged in member
|
||||
* @param bool $useTitle Whether to add a title attribute to the <time> element
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function html(bool $capitalize=TRUE, bool $short=FALSE, Lang|Member $memberOrLanguage=NULL ): string
|
||||
public function html(bool $capitalize=TRUE, bool $short=FALSE, Lang|Member $memberOrLanguage=NULL, bool $useTitle = true ): string
|
||||
{
|
||||
$format = $short ? 1 : ( $capitalize ? static::RELATIVE_FORMAT_NORMAL : static::RELATIVE_FORMAT_LOWER );
|
||||
$shortLong = $short ? 'short' : 'long';
|
||||
$title = $useTitle ? "title='{$this}'" : '';
|
||||
|
||||
return "<time datetime='{$this->rfc3339()}' title='{$this}' data-short='" . trim( $this->relative( 1, $memberOrLanguage ) ) . "' class='ipsTime ipsTime--{$shortLong}'><span class='ipsTime__long'>" . trim( $this->relative( $format, $memberOrLanguage ) ) . "</span><span class='ipsTime__short'>" . trim( $this->relative( 1, $memberOrLanguage ) ) . "</span></time>";
|
||||
return "<time datetime='{$this->rfc3339()}' {$title} data-short='" . trim( $this->relative( 1, $memberOrLanguage ) ) . "' class='ipsTime ipsTime--{$shortLong}'><span class='ipsTime__long'>" . trim( $this->relative( $format, $memberOrLanguage ) ) . "</span><span class='ipsTime__short'>" . trim( $this->relative( 1, $memberOrLanguage ) ) . "</span></time>";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -689,7 +689,7 @@ class Admin extends Standard
|
||||
*/
|
||||
final public static function showSwitchLink(): bool
|
||||
{
|
||||
if ( CIC )
|
||||
if ( ! CIC )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -212,10 +212,10 @@ abstract class Dispatcher
|
||||
/**
|
||||
* Check the current location to determine if it's a match
|
||||
*
|
||||
* @param string $location
|
||||
* @param string|null $app
|
||||
* @param string|null $module
|
||||
* @param string|null $controller
|
||||
* @param string $location Dispatcher Location ( admin/front/setup )
|
||||
* @param string|null $app Application Directory
|
||||
* @param string|null $module Module Name
|
||||
* @param string|null $controller Controller Name
|
||||
* @return bool
|
||||
*/
|
||||
public static function checkLocation( string $location, ?string $app = null, ?string $module = null, ?string $controller = null ) : bool
|
||||
|
||||
+39
-20
@@ -162,6 +162,13 @@ class Front extends Standard
|
||||
}
|
||||
catch( OutOfRangeException $e )
|
||||
{
|
||||
/* If we have the converter app, check for redirects */
|
||||
if( Application::appIsEnabled('convert') )
|
||||
{
|
||||
$application = Application::load( 'convert' );
|
||||
$application::checkRedirects();
|
||||
}
|
||||
|
||||
/* Display a 404 */
|
||||
$this->application = Application::load('core');
|
||||
$this->setDefaultModule();
|
||||
@@ -174,6 +181,19 @@ class Front extends Standard
|
||||
Output::i()->error( 'requested_route_404', '1S160/2', 404, '' );
|
||||
}
|
||||
|
||||
/* Loader extension */
|
||||
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
|
||||
{
|
||||
foreach( $loader->js() as $js )
|
||||
{
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, $js );
|
||||
}
|
||||
foreach( $loader->css() as $css )
|
||||
{
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, $css );
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if we're a low quality (no search engine bot) user */
|
||||
if ( ! Member::loggedIn()->member_id and \IPS\Session\Front::i()->userAgent->isLowValue() )
|
||||
{
|
||||
@@ -798,10 +818,14 @@ class Front extends Standard
|
||||
*/
|
||||
protected function adsTxt() : void
|
||||
{
|
||||
if ( Settings::i()->ads_txt_enabled )
|
||||
if ( (int) Settings::i()->ads_txt_enabled == 1 )
|
||||
{
|
||||
Output::i()->sendOutput( Settings::i()->ads_txt, 200, 'text/plain' );
|
||||
}
|
||||
elseif( (int) Settings::i()->ads_txt_enabled == 2 AND !empty( Settings::i()->ads_txt_redirect_url ) )
|
||||
{
|
||||
Output::i()->redirect( Url::external( Settings::i()->ads_txt_redirect_url ) );
|
||||
}
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
/**
|
||||
@@ -841,6 +865,9 @@ Disallow: {$path}*ref=
|
||||
Disallow: {$path}*?forumId*
|
||||
Disallow: {$path}*?&controller=embed
|
||||
|
||||
# Block CDN endpoints
|
||||
Disallow: /cdn-cgi/
|
||||
|
||||
# Sitemap URL
|
||||
Sitemap: {$sitemapUrl}
|
||||
FILE;
|
||||
@@ -868,6 +895,10 @@ FILE;
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'front.js' ) );
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'front_core.js', 'core', 'front' ) );
|
||||
|
||||
if(Theme::i()->getLayoutValue( 'global_view_mode' ) == 'default' ){
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'static/ui/navigationMoreMenu.js', 'core', 'interface' ) );
|
||||
}
|
||||
|
||||
/* Can we edit widget layouts? */
|
||||
if( Member::loggedIn()->modPermission('can_manage_sidebar') )
|
||||
{
|
||||
@@ -880,15 +911,6 @@ FILE;
|
||||
{
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'front_system.js', 'core', 'front' ) );
|
||||
}
|
||||
|
||||
/* Loader extension */
|
||||
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
|
||||
{
|
||||
foreach( $loader->js() as $js )
|
||||
{
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, $js );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,15 +943,6 @@ FILE;
|
||||
{
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, Theme::i()->css( 'styles/caching_log.css', 'core', 'front' ) );
|
||||
}
|
||||
|
||||
/* Loader extension */
|
||||
foreach( Application::allExtensions( 'core', 'Loader' ) as $loader )
|
||||
{
|
||||
foreach( $loader->css() as $css )
|
||||
{
|
||||
Output::i()->cssFiles = array_merge( Output::i()->cssFiles, $css );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -964,8 +977,14 @@ FILE;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If a member is forced to reset their password, let them */
|
||||
if( Member::loggedIn()->members_bitoptions['password_reset_forced'] AND !Member::loggedIn()->members_pass_hash )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Don't get in the way of the ModCP, registering, logging in, etc */
|
||||
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect', 'cookie' ];
|
||||
$ignoreControllers = [ 'modcp', 'register', 'login', 'redirect', 'cookies', 'lostpass' ];
|
||||
if( !Request::i()->isAjax() and !in_array( $this->controller, $ignoreControllers ) AND $alert = Alert::getNextAlertForMember( Member::loggedIn() ) )
|
||||
{
|
||||
$alert->viewed( Member::loggedIn() );
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace IPS;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use BadMethodCallException;
|
||||
use DateInterval;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMText;
|
||||
@@ -127,7 +128,9 @@ abstract class Email
|
||||
{
|
||||
foreach( static::outgoingHandlers() as $key => $class )
|
||||
{
|
||||
if( ( Settings::i()->mail_method == $key AND $class::isUsable( $type ) ) OR ( $key == 'debug' and $class::isUsable( Email::TYPE_TRANSACTIONAL ) ) )
|
||||
if( ( Settings::i()->mail_method == $key AND $class::isUsable( $type ) )
|
||||
OR ( $key == 'sendgrid' and $class::isUsable( $type ) ) // SendGrid is a special use case since it can send bulk mail only.
|
||||
OR ( $key == 'debug' and $class::isUsable( \IPS\Email::TYPE_TRANSACTIONAL ) ) )
|
||||
{
|
||||
return $class;
|
||||
}
|
||||
@@ -466,7 +469,7 @@ abstract class Email
|
||||
$ourPicks = NULL;
|
||||
if( Settings::i()->our_picks_in_email and $this->type !== 'transactional' )
|
||||
{
|
||||
$ourPicks = Feature::internalStream( 4 );
|
||||
$ourPicks = Feature::internalStream( 4, 'promote_added', 'desc', DateTime::create()->sub( new DateInterval( 'P1Y' ) ) );
|
||||
}
|
||||
|
||||
/* Wrap */
|
||||
|
||||
@@ -18,10 +18,12 @@ use IPS\Application;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Events\ListenerType\ClubListenerType;
|
||||
use IPS\Events\ListenerType\ContentListenerType;
|
||||
use IPS\Events\ListenerType\FileListenerType;
|
||||
use IPS\Events\ListenerType\InvoiceListenerType;
|
||||
use IPS\Events\ListenerType\NodeListenerType;
|
||||
use IPS\Events\ListenerType\PackageListenerType;
|
||||
use IPS\Events\ListenerType\MemberListenerType;
|
||||
use IPS\Events\ListenerType\PollListenerType;
|
||||
use OutOfRangeException;
|
||||
|
||||
if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -140,7 +142,9 @@ class ListenerType
|
||||
'InvoiceListenerType' => InvoiceListenerType::class,
|
||||
'MemberListenerType' => MemberListenerType::class,
|
||||
'NodeListenerType' => NodeListenerType::class,
|
||||
'PollListenerType' => PollListenerType::class,
|
||||
'PackageListenerType' => PackageListenerType::class,
|
||||
'FileListenerType' => FileListenerType::class
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief FileListenerType
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @subpackage
|
||||
* @since 4/3/2025
|
||||
*/
|
||||
|
||||
namespace IPS\Events\ListenerType;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Events\ListenerType;
|
||||
use IPS\File as SystemFile;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method onDownload( SystemFile $file, object $content, array $extra ) : void
|
||||
*/
|
||||
class FileListenerType extends ListenerType
|
||||
{
|
||||
/**
|
||||
* Defines the classes that are supported by each Listener Type
|
||||
* When a new Listener Type is created, we must specify which
|
||||
* classes are valid (e.g. \IPS\Content, \IPS\Member).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $supportedBaseClasses = array(
|
||||
SystemFile::class
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,10 @@ namespace IPS\Events\ListenerType;
|
||||
use IPS\calendar\Event;
|
||||
use IPS\Content as ContentClass;
|
||||
use IPS\Content\Reaction;
|
||||
use IPS\courses\Course;
|
||||
use IPS\courses\Lesson;
|
||||
use IPS\courses\Module;
|
||||
use IPS\courses\Quiz;
|
||||
use IPS\Events\ListenerType;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member as MemberClass;
|
||||
@@ -48,6 +52,10 @@ if( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
* @method onUnreact( MemberClass $member, ContentClass $content ) : void
|
||||
* @method onFollow( MemberClass $member, object $object, bool $isAnonymous ): void
|
||||
* @method onUnfollow( MemberClass $member, object $object ) : void
|
||||
* @method onCourseComplete( MemberClass $member, Course $course ) : void
|
||||
* @method onModuleComplete( MemberClass $member, Module $module ) : void
|
||||
* @method onLessonComplete( MemberClass $member, Lesson $lesson ) : void
|
||||
* @method onQuizComplete( MemberClass $member, Quiz $quiz ) : void
|
||||
*/
|
||||
class MemberListenerType extends ListenerType
|
||||
{
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief PollListenerType
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @subpackage
|
||||
* @since 4/3/2025
|
||||
*/
|
||||
|
||||
namespace IPS\Events\ListenerType;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Events\ListenerType;
|
||||
use IPS\Poll as PollClass;
|
||||
use IPS\Poll\Vote as VoteClass;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method onCreateOrEdit( PollClass $poll ) : void
|
||||
* @method onDelete( PollClass $poll ) : void
|
||||
* @method onVote( PollClass $poll, VoteClass $vote ) : void
|
||||
* @method onVoteRecount( PollClass $poll ) : void
|
||||
* @method onStateChange( PollClass $poll, string $state ) : void
|
||||
*/
|
||||
class PollListenerType extends ListenerType
|
||||
{
|
||||
/**
|
||||
* Defines the classes that are supported by each Listener Type
|
||||
* When a new Listener Type is created, we must specify which
|
||||
* classes are valid (e.g. \IPS\Content, \IPS\Member).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $supportedBaseClasses = array(
|
||||
PollClass::class
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace IPS\Extensions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Application\BuilderFilter;
|
||||
use RuntimeException;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -32,10 +33,20 @@ abstract class BuildAbstract
|
||||
*/
|
||||
abstract public function build() : void;
|
||||
|
||||
/**
|
||||
* Should the current file/directory be included in the tar
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function accept( BuilderFilter $current ): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish Build
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function finish() : void;
|
||||
abstract public function finish() : void;
|
||||
}
|
||||
@@ -13,7 +13,11 @@
|
||||
namespace IPS\Extensions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Helpers\Menu\Buttons;
|
||||
use IPS\Helpers\Menu\MenuItem;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Member\Club\Page as ClubPage;
|
||||
use IPS\Node\Model;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -31,5 +35,34 @@ abstract class ClubAbstract
|
||||
* @param Model|null $container Container
|
||||
* @return array
|
||||
*/
|
||||
abstract public function tabs( Club $club, ?Model $container = NULL ): array;
|
||||
public function tabs( Club $club, ?Model $container = NULL ): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of menu elements to add to the club menu
|
||||
*
|
||||
* @param Club $club
|
||||
* @param Model|ClubPage|null $container
|
||||
* @return array<string,MenuItem>
|
||||
*/
|
||||
public function menu( Club $club, Model|ClubPage|null $container = null ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of menu elements to add to the buttons list
|
||||
* in the club header. The menu will be rendered as individual buttons and not
|
||||
* as a dropdown menu.
|
||||
*
|
||||
* @see Buttons::button()
|
||||
* @param Club $club
|
||||
* @return array
|
||||
*/
|
||||
public function buttons( Club $club ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@
|
||||
namespace IPS\Extensions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Member;
|
||||
use IPS\Theme;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
@@ -45,4 +49,17 @@ abstract class MemberHistoryAbstract
|
||||
* @return string
|
||||
*/
|
||||
abstract public function parseLogData( string $value, array $row ): string;
|
||||
|
||||
/**
|
||||
* Parse LogMember column
|
||||
*
|
||||
* @param string $value column value
|
||||
* @param array $row entire log row
|
||||
* @return string
|
||||
*/
|
||||
public function parseLogMember( string $value, array $row ): string
|
||||
{
|
||||
$member = Member::load( $value );
|
||||
return Theme::i()->getTemplate( 'global', 'core' )->userPhoto( $member, 'tiny' ) . ' ' . $member->link();
|
||||
}
|
||||
}
|
||||
@@ -42,4 +42,18 @@ abstract class ModCpAbstract
|
||||
* @return void
|
||||
*/
|
||||
abstract public function manage() : void;
|
||||
|
||||
/**
|
||||
* Any counters that will be displayed in the ModCP Header.
|
||||
* This should return an array of counters, where each item contains
|
||||
* title (a language string)
|
||||
* total
|
||||
* id (optional element ID)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCounters() : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ namespace IPS\Extensions;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -40,6 +41,20 @@ abstract class ProfileAbstract
|
||||
$this->member = $member;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Url
|
||||
*/
|
||||
public function url() : Url
|
||||
{
|
||||
$className = explode( "\\", get_class( $this ) );
|
||||
return Url::internal(
|
||||
"app=core&module=members&controller=profile&id=" . $this->member->member_id . "&tab=" . mb_strtolower( array_pop( $className ) ),
|
||||
"front",
|
||||
"profile_tab",
|
||||
$this->member->members_seo_name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there content to display?
|
||||
*
|
||||
|
||||
+23
-11
@@ -14,6 +14,7 @@ namespace IPS\File;
|
||||
|
||||
use DomainException;
|
||||
use Exception;
|
||||
use IPS\Application;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Db;
|
||||
use IPS\File;
|
||||
@@ -24,6 +25,7 @@ use IPS\Platform\Bridge;
|
||||
use IPS\Request;
|
||||
use IPS\Xml\SimpleXML;
|
||||
use LogicException;
|
||||
use OutOfRangeException;
|
||||
use RuntimeException;
|
||||
use UnderflowException;
|
||||
use function count;
|
||||
@@ -241,12 +243,15 @@ class Amazon extends File
|
||||
if( mb_strpos( $this->storageExtension, '_' ) !== FALSE )
|
||||
{
|
||||
$bits = explode( '_', $this->storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$storeGzipExtensions ) and is_array( $class::$storeGzipExtensions ) and count( $class::$storeGzipExtensions ) )
|
||||
try
|
||||
{
|
||||
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
|
||||
$class = Application::getExtensionClass( $bits[0], 'FileStorage', $bits[1] );
|
||||
if ( isset( $class::$storeGzipExtensions ) and is_array( $class::$storeGzipExtensions ) and count( $class::$storeGzipExtensions ) )
|
||||
{
|
||||
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,12 +271,15 @@ class Amazon extends File
|
||||
if ( mb_strpos( $this->storageExtension, '_' ) !== FALSE )
|
||||
{
|
||||
$bits = explode( '_', $this->storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$isPrivate ) )
|
||||
try
|
||||
{
|
||||
return $class::$isPrivate;
|
||||
$class = Application::getExtensionClass( $bits[0], 'FileStorage', $bits[1] );
|
||||
if ( isset( $class::$isPrivate ) )
|
||||
{
|
||||
return $class::$isPrivate;
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,12 +784,16 @@ class Amazon extends File
|
||||
if( $storageExtension !== NULL AND mb_strpos( $storageExtension, '_' ) !== FALSE )
|
||||
{
|
||||
$bits = explode( '_', $storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
|
||||
try
|
||||
{
|
||||
$cacheSeconds = $class::$cacheControlTtl;
|
||||
$class = Application::getExtensionClass( $bits[0], 'FileStorage', $bits[1] );
|
||||
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
|
||||
{
|
||||
$cacheSeconds = $class::$cacheControlTtl;
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
$headers['Cache-Control'] = 'public, max-age=' . $cacheSeconds;
|
||||
|
||||
+13
-19
@@ -11,38 +11,29 @@
|
||||
namespace IPS\File;
|
||||
|
||||
/* Set use statements to make Uncle Matt happy */
|
||||
|
||||
use IPS\Application;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\DateTime AS DT;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Db;
|
||||
use IPS\File;
|
||||
use IPS\File\Amazon;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Http\Response;
|
||||
use IPS\Request;
|
||||
use IPS\Http\Request\Exception as RequestException;
|
||||
use IPS\Member;
|
||||
use DateInterval;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function debug_backtrace;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
use function header;
|
||||
use function in_array;
|
||||
use function mb_strstr;
|
||||
use function trim;
|
||||
use function strlen;
|
||||
|
||||
use const ARRAY_FILTER_USE_KEY;
|
||||
use const DEBUG_BACKTRACE_IGNORE_ARGS;
|
||||
|
||||
use const IPS\VERY_LONG_REQUEST_TIMEOUT;
|
||||
|
||||
/* 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' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -85,13 +76,13 @@ class Backblaze extends Amazon
|
||||
* @param string $uri The URI (relative to the bucket)
|
||||
* @param string $verb The HTTP verb to use
|
||||
* @param array $configuration The configuration for this instance
|
||||
* @param int $configurationId The configuration ID
|
||||
* @param int|null $configurationId The configuration ID
|
||||
* @param string|null $content The content to send
|
||||
* @param string|null $storageExtension Storage extension
|
||||
* @param bool $skipExtraChecks Skips the endpoint check (to prevent infinite looping)
|
||||
* @param bool $isPrivate This can be set to true to access/store private files (i.e. that are not publicly readable)
|
||||
* @param array $queryString Array of key => value pairs
|
||||
* @return \IPS\Http\Response
|
||||
* @return Response
|
||||
* @throws \IPS\Http\Request\Exception
|
||||
*/
|
||||
protected static function makeRequest( string $uri, string $verb, array $configuration, ?int $configurationId, ?string $content=NULL, ?string $storageExtension=NULL, bool $skipExtraChecks=FALSE, ?bool $isPrivate=false, array $queryString=array() ): Response
|
||||
@@ -132,12 +123,15 @@ class Backblaze extends Amazon
|
||||
if( $storageExtension !== NULL AND mb_strpos( $storageExtension, '_' ) !== FALSE )
|
||||
{
|
||||
$bits = explode( '_', $storageExtension );
|
||||
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
|
||||
|
||||
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
|
||||
try
|
||||
{
|
||||
$cacheSeconds = $class::$cacheControlTtl;
|
||||
$class = Application::getExtensionClass( $bits[0], 'FileStorage', $bits[1] );
|
||||
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
|
||||
{
|
||||
$cacheSeconds = $class::$cacheControlTtl;
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
$headers['Cache-Control'] = 'public, max-age=' . $cacheSeconds;
|
||||
|
||||
+11
-5
@@ -19,6 +19,7 @@ use IPS\Data\Store;
|
||||
use IPS\File\FileSystem;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Output\Plugin\Filesize;
|
||||
use IPS\Platform\Bridge;
|
||||
use LogicException;
|
||||
use OutOfRangeException;
|
||||
use RuntimeException;
|
||||
@@ -85,6 +86,8 @@ abstract class File
|
||||
$return['Ftp'] = 'IPS\\File\\Ftp';
|
||||
}
|
||||
|
||||
$return = Bridge::i()->fileHandlers( $return );
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
@@ -1173,13 +1176,16 @@ abstract class File
|
||||
{
|
||||
if ( $id == $this->configurationId )
|
||||
{
|
||||
$exploded = explode( '_', str_replace( 'filestorage__', '', $method ) );
|
||||
$classname = "IPS\\{$exploded[0]}\\extensions\\core\\FileStorage\\{$exploded[1]}";
|
||||
|
||||
if ( method_exists( $classname, 'settingsUpdated' ) )
|
||||
$exploded = explode( '_', str_replace( 'filestorage__', '', $method ) );
|
||||
try
|
||||
{
|
||||
$classname::settingsUpdated( $this->configuration );
|
||||
$classname = Application::getExtensionClass( $exploded[0], 'FileStorage', $exploded[1] );
|
||||
if ( method_exists( $classname, 'settingsUpdated' ) )
|
||||
{
|
||||
$classname::settingsUpdated( $this->configuration );
|
||||
}
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,20 @@ class Codemirror extends TextArea
|
||||
{
|
||||
if( $this->options['simple'] )
|
||||
{
|
||||
if ( isset( $this->options['mode'] ) )
|
||||
{
|
||||
if ( empty( $this->options['codeModeAllowedLanguages'] ) )
|
||||
{
|
||||
$this->options['codeModeAllowedLanguages'] = [];
|
||||
}
|
||||
$this->options['codeModeAllowedLanguages'][] = $this->options['mode'];
|
||||
}
|
||||
|
||||
if ( is_array( $this->options['codeModeAllowedLanguages'] ) )
|
||||
{
|
||||
// HTMLMixed is not a valid option for Monaco
|
||||
$this->options['codeModeAllowedLanguages'] = array_unique( array_map( function ($lang) { return $lang === 'htmlmixed' ? 'html' : $lang; }, $this->options['codeModeAllowedLanguages'] ) );
|
||||
}
|
||||
return parent::html();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use IPS\Theme;
|
||||
use IPS\Xml\SimpleXML;
|
||||
use OutOfBoundsException;
|
||||
use UnderflowException;
|
||||
use UnexpectedValueException;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function file_get_contents;
|
||||
@@ -45,6 +46,8 @@ use function in_array;
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
use function is_string;
|
||||
use function md5;
|
||||
use function session_id;
|
||||
use function strlen;
|
||||
use const IPS\ROOT_PATH;
|
||||
|
||||
@@ -140,6 +143,9 @@ class Editor extends FormAbstract
|
||||
"external_video",
|
||||
'og_embed',
|
||||
"raw_embed",
|
||||
'native_emoji',
|
||||
'custom_emoji',
|
||||
'fa_icons'
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -296,9 +302,13 @@ class Editor extends FormAbstract
|
||||
which creates a discrepency between wheat displays in a post and what displays in the editor */
|
||||
// $value = preg_replace( '/<p>\s*<\/p>/', '', $value );
|
||||
|
||||
/* It is possible that the autosave key was updated, so we make doubly sure the uploader's post key matches our own */
|
||||
$finalPostKey = md5( $this->options['autoSaveKey'] . ':' . session_id() );
|
||||
|
||||
/* Show full uploader */
|
||||
if ( $this->uploader )
|
||||
{
|
||||
$this->uploader->options['postKey'] = $finalPostKey;
|
||||
$attachmentArea = $this->uploader->html();
|
||||
}
|
||||
/* Or show a loading icon where the uploader will go if the editor is minimized */
|
||||
@@ -320,7 +330,7 @@ class Editor extends FormAbstract
|
||||
/* Or if the user can't attach, just show a bar */
|
||||
else
|
||||
{
|
||||
$attachmentArea = Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $this->postKey, $this->noUploaderError, $this->canUseMediaExtension() );
|
||||
$attachmentArea = Theme::i()->getTemplate( 'forms', 'core', 'global' )->editorAttachmentsPlaceholder( $this->name, $finalPostKey, $this->noUploaderError, $this->canUseMediaExtension() );
|
||||
}
|
||||
|
||||
if ( Member::loggedIn()->group['g_bypass_badwords'] )
|
||||
@@ -1000,7 +1010,18 @@ class Editor extends FormAbstract
|
||||
$editorFile = Application::getRootPath( $app->directory ) . "/applications/" . $app->directory . "/data/editor.xml";
|
||||
if( file_exists( $editorFile ) )
|
||||
{
|
||||
$xml = SimpleXML::loadFile( $editorFile );
|
||||
try
|
||||
{
|
||||
$xml = SimpleXML::loadFile( $editorFile );
|
||||
if ( !isset( $xml->plugin ) or !is_iterable( $xml->plugin ) )
|
||||
{
|
||||
throw new UnexpectedValueException;
|
||||
}
|
||||
}
|
||||
catch ( Exception $e )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach( $xml->plugin as $plugin )
|
||||
{
|
||||
$contents = (string) $plugin;
|
||||
|
||||
@@ -112,7 +112,7 @@ class Radio extends Select
|
||||
|
||||
if ( $this->options['parse'] === 'image' )
|
||||
{
|
||||
return Theme::i()->getTemplate( 'forms', 'core', 'global' )->radioImages( $this->name, $value, $this->required, $this->options['options'], $this->options['disabled'], $this->options['toggles'], $descriptions, $warnings, $this->options['userSuppliedInput'], $this->options['unlimited'], $this->options['unlimitedLang'], $this->htmlId, $this->options['gridspan'] );
|
||||
return Theme::i()->getTemplate( 'forms', 'core', 'global' )->radioImages( $this->name, $value, $this->required, $this->options['options'], $this->options['disabled'], $this->options['toggles'], $descriptions, $warnings, $this->options['userSuppliedInput'], $this->options['unlimited'], $this->options['unlimitedLang'], $this->htmlId );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -42,7 +42,6 @@ class Select extends FormAbstract
|
||||
'class' => '', // CSS class
|
||||
'disabled' => FALSE, // Disables input. Default is FALSE.
|
||||
'parse' => 'lang', // Sets how the values for options should be parsed. Acceptable values are "lang" (language keys), "normal" (htmlentities), "raw" (no parsing) or "image" (image URLs, only for Radio fields). Default is "lang".
|
||||
'gridspan' => 3, // If 'parse' is set to 'image', this controls the gridspan to use when creating the option layout
|
||||
'unlimited' => -1, // If any value other than NULL is provided, an "Unlimited" checkbox will be displayed. If checked, the value specified will be sent.
|
||||
'unlimitedLang' => 'unlimited', // Language string to use for unlimited checkbox label
|
||||
'unlimitedToggles' => array(...), // Names of other input fields that should show/hide when the "Unlimited" checkbox is toggled.
|
||||
@@ -62,7 +61,6 @@ class Select extends FormAbstract
|
||||
'class' => '',
|
||||
'disabled' => FALSE,
|
||||
'parse' => 'lang',
|
||||
'gridspan' => 3,
|
||||
'unlimited' => NULL,
|
||||
'unlimitedLang' => 'all',
|
||||
'unlimitedToggles' => array(),
|
||||
@@ -230,7 +228,7 @@ class Select extends FormAbstract
|
||||
if( isset( $this->options['userSuppliedInput'] ) )
|
||||
{
|
||||
$name = $this->options['userSuppliedInput'] . '_' . $this->name;
|
||||
$userInputValue = Request::i()->$name;
|
||||
$userInputValue = mb_strpos( $name, '[' ) ? Request::i()->valueFromArray( $name ) : Request::i()->$name;
|
||||
if( is_array( $userInputValue ) )
|
||||
{
|
||||
$userInputValue = $userInputValue[0];
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Buttons
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @subpackage
|
||||
* @since 3/28/2025
|
||||
*/
|
||||
|
||||
namespace IPS\Helpers\Menu;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Helpers\Menu;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Theme;
|
||||
use function is_string;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
class Buttons extends Menu
|
||||
{
|
||||
/**
|
||||
* @param string $css
|
||||
*/
|
||||
public function __construct( string $css = 'ipsButtons' )
|
||||
{
|
||||
parent::__construct( '', null, $css );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a seperator element to the stack
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addSeparator(): self
|
||||
{
|
||||
/* Separators are not supported in buttons list */
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a title field to the stack
|
||||
*
|
||||
* @param string $title
|
||||
* @return $this
|
||||
*/
|
||||
public function addTitleField( string $title ): self
|
||||
{
|
||||
/* Title fields are not supported in buttons list */
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to build a button.
|
||||
* Primarily used for 3rd party extensions.
|
||||
*
|
||||
* @param Url|string|null $url
|
||||
* @param string $languageString
|
||||
* @param string|null $css
|
||||
* @param array $dataAttributes
|
||||
* @param bool $opensDialog
|
||||
* @param string|null $icon
|
||||
* @param bool|string $tooltip
|
||||
* @return string|Link
|
||||
*/
|
||||
public static function button( Url|string|null $url, string $languageString, ?string $css = null, array $dataAttributes = [], bool $opensDialog = FALSE, ?string $icon = NULL, bool|string $tooltip=false ) : string|Link
|
||||
{
|
||||
/* Handle the tooltip */
|
||||
if( $tooltip !== false )
|
||||
{
|
||||
$dataAttributes['title'] = is_string( $tooltip ) ? $tooltip : $languageString;
|
||||
$dataAttributes['data-ipsTooltip'] = '';
|
||||
}
|
||||
|
||||
/* If the URL is null, then we're just showing a <span> instead of an actual link */
|
||||
if( $url === null )
|
||||
{
|
||||
return Theme::i()->getTemplate( 'menu', 'core', 'front' )->buttonsItem( $css ?? 'ipsButton ipsButton--text', $languageString, $icon ?? '', $dataAttributes );
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Link( $url, $languageString, $css ?? 'ipsButton ipsButton--inherit', $dataAttributes, $opensDialog, $icon );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a button
|
||||
*
|
||||
* @param Url|string|null $url
|
||||
* @param string $languageString
|
||||
* @param string|null $css
|
||||
* @param array $dataAttributes
|
||||
* @param bool $opensDialog
|
||||
* @param string|null $icon
|
||||
* @param bool|string $tooltip
|
||||
* @return $this
|
||||
*/
|
||||
public function addButton( Url|string|null $url, string $languageString, ?string $css = null, array $dataAttributes = [], bool $opensDialog = FALSE, ?string $icon = NULL, bool|string $tooltip=false ) : self
|
||||
{
|
||||
$this->elements[] = static::button( $url, $languageString, $css, $dataAttributes, $opensDialog, $icon, $tooltip );
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a row wrapper which can contain custom HTML code
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function rowWrapper( string $content ): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function linkHtml(): string
|
||||
{
|
||||
return $this->contentHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed menu content
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentHtml(): string
|
||||
{
|
||||
if( $this->hasContent() )
|
||||
{
|
||||
return Theme::i()->getTemplate( 'menu', 'core', 'front' )->buttonsContent( $this );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parsed menu
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if( !$this->hasContent() )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
return Theme::i()->getTemplate( 'menu', 'core', 'front' )->buttonsList( $this );
|
||||
}
|
||||
}
|
||||
@@ -409,10 +409,7 @@ class Content extends Table
|
||||
}
|
||||
|
||||
/* Check extensions for custom where clause */
|
||||
foreach( UiExtension::i()->run( $class, 'contentTableWhere', array( $this ) ) as $customWhere )
|
||||
{
|
||||
$where[] = $customWhere;
|
||||
}
|
||||
UiExtension::i()->run( $class, 'contentTableWhere', array( $this, &$where ) );
|
||||
|
||||
/* Get results */
|
||||
$this->count = $class::getItemsWithPermission( $where, $sortBy, NULL, $this->permCheck, $this->includeHiddenContent, 0, NULL, $this->joinContainer, $this->joinComments, $this->joinReviews, TRUE, $this->joins, ( $this->container and $this->limitByContainer ) ? $this->container : FALSE, TRUE, TRUE, TRUE, $this->showMovedLinks );
|
||||
@@ -619,6 +616,9 @@ class Content extends Table
|
||||
{
|
||||
$class::tableGetRows( $rows );
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
UiExtension::i()->run( $class, 'contentTableGetRows', [ $this, &$rows ] );
|
||||
|
||||
if ( $this->callback != NULL and is_callable( $this->callback ) )
|
||||
{
|
||||
@@ -990,11 +990,32 @@ class Content extends Table
|
||||
$currentTags['prefix'] = $currentPrefix;
|
||||
}
|
||||
|
||||
foreach( $params[ $class::$formLangPrefix . 'tags' ] as $tag )
|
||||
foreach( $params[ $class::$formLangPrefix . 'tags' ] as $k => $tag )
|
||||
{
|
||||
$currentTagIndex = array_search( $tag, $currentTags );
|
||||
if( Request::i()->modaction == 'tag' )
|
||||
{
|
||||
if( !in_array( $tag, $currentTags ) )
|
||||
/* Prefix should be handled slightly differently */
|
||||
if( $k == 'prefix' )
|
||||
{
|
||||
if( !isset( $currentTags['prefix'] ) or $currentTags['prefix'] != $tag )
|
||||
{
|
||||
/* Move the current prefix to the regular tag list */
|
||||
if( isset( $currentTags['prefix'] ) )
|
||||
{
|
||||
$currentTags[] = $currentTags['prefix'];
|
||||
}
|
||||
|
||||
/* Set the tag as the prefix and remove it from the tag list */
|
||||
$currentTags['prefix'] = $tag;
|
||||
if( $currentTagIndex !== false )
|
||||
{
|
||||
unset( $currentTags[ $currentTagIndex ] );
|
||||
}
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
elseif( $currentTagIndex === false )
|
||||
{
|
||||
$currentTags[] = $tag;
|
||||
$modified = true;
|
||||
@@ -1002,10 +1023,10 @@ class Content extends Table
|
||||
}
|
||||
else
|
||||
{
|
||||
$index = array_search( $tag, $currentTags );
|
||||
if( $index !== false )
|
||||
$currentTagIndex = array_search( $tag, $currentTags );
|
||||
if( $currentTagIndex !== false )
|
||||
{
|
||||
unset( $currentTags[ $index ] );
|
||||
unset( $currentTags[ $currentTagIndex ] );
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ class Friendly extends Internal
|
||||
$furlDefinition = static::furlDefinition();
|
||||
if ( !isset( $furlDefinition[ $seoTemplate ] ) )
|
||||
{
|
||||
throw new Exception( 'INVALID_SEO_TEMPLATE ' . $seoTemplate );
|
||||
throw new Exception( 'INVALID_SEO_TEMPLATE' );
|
||||
}
|
||||
$component = $furlDefinition[ $seoTemplate ]['friendly'];
|
||||
|
||||
@@ -716,6 +716,9 @@ class Friendly extends Internal
|
||||
|
||||
/* Disallowed characters which browsers may try to automatically percent-encode */
|
||||
$value = str_replace( array( '!', '*', '\'', '(', ')', ';', ':', '@', '&', '=', '+', '$', ',', '/', '?', '#', '[', ']', '%', '\\', '"', '<', '>', '^', '{', '}', '|', '.', '`' ), '', $value );
|
||||
|
||||
/* Curly quotes */
|
||||
$value = str_replace( array( "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c", "\xe2\x80\x9d", "\xe2\x80\x93", "\xe2\x80\x94", "\xe2\x80\xa6" ), '', $value );
|
||||
|
||||
/* Trim */
|
||||
$value = preg_replace( '/\-+/', '-', $value );
|
||||
|
||||
@@ -385,6 +385,10 @@ class Gd extends Image
|
||||
public function flip(): void
|
||||
{
|
||||
imageflip( $this->image, IMG_FLIP_HORIZONTAL );
|
||||
|
||||
/* Set width/height */
|
||||
$this->width = imagesx( $this->image );
|
||||
$this->height = imagesy( $this->image );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+61
-21
@@ -148,35 +148,75 @@ abstract class Image
|
||||
if( static::exifSupported() AND $checkRotation )
|
||||
{
|
||||
$obj->setExifData( $contents );
|
||||
|
||||
|
||||
/* If the image is misoriented, attempt to automatically reorient */
|
||||
if ( $orientation = $obj->getImageOrientation() )
|
||||
$orientation = $obj->getImageOrientation();
|
||||
|
||||
/* Valid orientation values:
|
||||
1 = 0 degrees: the correct orientation, no adjustment is required.
|
||||
2 = 0 degrees, mirrored: image has been flipped back-to-front.
|
||||
3 = 180 degrees: image is upside down.
|
||||
4 = 180 degrees, mirrored: image has been flipped back-to-front and is upside down.
|
||||
5 = 90 degrees: image has been flipped back-to-front and is on its side.
|
||||
6 = 90 degrees, mirrored: image is on its side.
|
||||
7 = 270 degrees: image has been flipped back-to-front and is on its far side.
|
||||
8 = 270 degrees, mirrored: image is on its far side.*/
|
||||
|
||||
/* Differences in orientation between GD and ImageMagick can cause auto-reorient to not work properly
|
||||
GD rotates counter-clockwise; ImageMagick rotates clockwise */
|
||||
|
||||
if ( !( $obj instanceof Image\Imagemagick ) )
|
||||
{
|
||||
$mirrored = in_array( $orientation, [2,4,5,7] );
|
||||
$degrees = match ($orientation) {
|
||||
3,4 => 180,
|
||||
5,6 => 90,
|
||||
7,8 => -90,
|
||||
default => 0
|
||||
};
|
||||
|
||||
if ( $degrees !== 0 OR $mirrored )
|
||||
switch ( $orientation )
|
||||
{
|
||||
$obj->rotate( $degrees );
|
||||
if ( $mirrored )
|
||||
{
|
||||
$obj->flip();
|
||||
}
|
||||
case 3:
|
||||
case 4:
|
||||
$obj->rotate( 180 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
$obj->hasBeenRotated = true;
|
||||
case 5:
|
||||
case 6:
|
||||
$obj->rotate( -90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
case 8:
|
||||
$obj->rotate( 90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
/* ImageMagick requires orientation be reset when rotated */
|
||||
if ( $degrees !== 0 OR $mirrored OR $obj instanceof Imagemagick )
|
||||
}
|
||||
else
|
||||
{
|
||||
switch( $orientation )
|
||||
{
|
||||
$obj->setImageOrientation( 1 );
|
||||
case 3:
|
||||
case 4:
|
||||
$obj->rotate( 180 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
case 6:
|
||||
$obj->rotate( 90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
|
||||
case 7:
|
||||
case 8:
|
||||
$obj->rotate( -90 );
|
||||
$obj->hasBeenRotated = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Has the image been flipped? */
|
||||
if( in_array( $orientation, array( 2, 4, 5, 7 ) ) )
|
||||
{
|
||||
$obj->flip();
|
||||
}
|
||||
}
|
||||
|
||||
/* Return */
|
||||
|
||||
@@ -260,6 +260,9 @@ class Imagemagick extends Image
|
||||
public function flip(): void
|
||||
{
|
||||
$this->imagick->flopImage();
|
||||
|
||||
/* Set width/height */
|
||||
$this->setDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1676,11 +1676,15 @@ class Lang extends Model
|
||||
|
||||
/* @var cms\Fields\ $fieldsClass */
|
||||
$fieldsClass = '\IPS\cms\Fields' . $database['database_id'];
|
||||
$customFields = $fieldsClass::databaseFieldIds();
|
||||
|
||||
foreach ( $customFields AS $id )
|
||||
|
||||
if ( class_exists( $fieldsClass ) )
|
||||
{
|
||||
$this->words['sort_field_' . $id] = $this->addToStack( 'content_field_' . $id );
|
||||
$customFields = $fieldsClass::databaseFieldIds();
|
||||
|
||||
foreach ( $customFields AS $id )
|
||||
{
|
||||
$this->words['sort_field_' . $id] = $this->addToStack( 'content_field_' . $id );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-5
@@ -157,8 +157,8 @@ class Log extends ActiveRecord
|
||||
|
||||
if ( $message instanceof Exception )
|
||||
{
|
||||
$log->exceptionClass = get_class( $message );
|
||||
$log->exceptionCode = $message->getCode();
|
||||
$log->exception_class = get_class( $message );
|
||||
$log->exception_code = $message->getCode();
|
||||
|
||||
if ( method_exists( $message, 'extraLogData' ) AND $extraData = $message->extraLogData() )
|
||||
{
|
||||
@@ -205,13 +205,22 @@ class Log extends ActiveRecord
|
||||
/* If that fails, log to disk */
|
||||
catch ( Exception $e )
|
||||
{
|
||||
if ( !NO_WRITES and !CIC)
|
||||
if ( !NO_WRITES and !CIC )
|
||||
{
|
||||
/* What are we writing? */
|
||||
try
|
||||
{
|
||||
$url = "URL: " . ( new Log() )->url . "\n";
|
||||
}
|
||||
catch( \Exception | \Error )
|
||||
{
|
||||
$url = '';
|
||||
}
|
||||
|
||||
$date = date('r');
|
||||
if ( $message instanceof Exception )
|
||||
{
|
||||
$messageToLog = $date . "\n" . get_class( $message ) . '::' . $message->getCode() . "\n" . $message->getMessage() . "\n" . $message->getTraceAsString();
|
||||
$messageToLog = $date . "\n" . $url . get_class( $message ) . '::' . $message->getCode() . "\n" . $message->getMessage() . "\n" . $message->getTraceAsString();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -219,7 +228,7 @@ class Log extends ActiveRecord
|
||||
{
|
||||
$message = var_export( $message, TRUE );
|
||||
}
|
||||
$messageToLog = $date . "\n" . $message . "\n" . ( new Exception )->getTraceAsString();
|
||||
$messageToLog = $date . "\n" . $url . $message . "\n" . ( new Exception )->getTraceAsString();
|
||||
}
|
||||
|
||||
/* Where are we writing it? */
|
||||
|
||||
@@ -679,13 +679,13 @@ abstract class Handler extends Model
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @param string|null $username The username from remote service
|
||||
* @return Url|NULL
|
||||
* @throws Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws DomainException General error where it is safe to show a message to the user
|
||||
* @throws RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( string $identifier, string $username ): ?Url
|
||||
public function userLink( string $identifier, ?string $username ): ?Url
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -304,13 +304,13 @@ class Twitter extends OAuth1
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @param string|null $username The username from remote service
|
||||
* @return Url|NULL
|
||||
* @throws LoginException The token is invalid and the user needs to reauthenticate
|
||||
* @throws DomainException General error where it is safe to show a message to the user
|
||||
* @throws RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( string $identifier, string $username ): ?Url
|
||||
public function userLink( string $identifier, ?string $username ): ?Url
|
||||
{
|
||||
return Url::external( "https://twitter.com/" )->setPath( $username );
|
||||
}
|
||||
|
||||
@@ -318,13 +318,13 @@ class Google extends OAuth2
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @param string|null $username The username from remote service
|
||||
* @return Url|NULL
|
||||
* @throws Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws DomainException General error where it is safe to show a message to the user
|
||||
* @throws RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( string $identifier, string $username ): ?Url
|
||||
public function userLink( string $identifier, ?string $username ): ?Url
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -410,13 +410,13 @@ class Invision extends OAuth2
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Nnumber/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @param string|null $username The username from remote service
|
||||
* @return Url|NULL
|
||||
* @throws LoginException The token is invalid and the user needs to reauthenticate
|
||||
* @throws DomainException General error where it is safe to show a message to the user
|
||||
* @throws RuntimeException Unexpected error from service
|
||||
*/
|
||||
public function userLink( string $identifier, string $username ): ?Url
|
||||
public function userLink( string $identifier, ?string $username ): ?Url
|
||||
{
|
||||
return Url::external( $this->settings['url'] )->setQueryString( 'showuser', $identifier );
|
||||
}
|
||||
|
||||
@@ -338,14 +338,14 @@ class LinkedIn extends OAuth2
|
||||
* May return NULL if server doesn't support this
|
||||
*
|
||||
* @param string $identifier The ID Number/string from remote service
|
||||
* @param string $username The username from remote service
|
||||
* @param string|null $username The username from remote service
|
||||
* @return Url|NULL
|
||||
* @throws Exception The token is invalid and the user needs to reauthenticate
|
||||
* @throws DomainException General error where it is safe to show a message to the user
|
||||
* @throws RuntimeException Unexpected error from service
|
||||
* @note You have to apply to LinkedIn's Partnership program manually to get the profile data necessary to generate the public profile URL (specifically, the vanityName field). As such, we can't generate the URL to your profile beginning with the v2 API.
|
||||
*/
|
||||
public function userLink( string $identifier, string $username ): ?Url
|
||||
public function userLink( string $identifier, ?string $username ): ?Url
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -95,8 +95,6 @@ class Standard extends Handler
|
||||
/* If we didn't match any, throw an exception */
|
||||
if ( !count( $members ) )
|
||||
{
|
||||
$member = NULL;
|
||||
|
||||
$member = new Member;
|
||||
$member->email = $usernameOrEmail;
|
||||
|
||||
|
||||
@@ -66,12 +66,19 @@ abstract class MFAHandler
|
||||
*/
|
||||
public static function handlers(): array
|
||||
{
|
||||
return array(
|
||||
$return = array(
|
||||
'authy' => new Authy\Handler(),
|
||||
'google' => new GoogleAuthenticator\Handler(),
|
||||
'questions' => new SecurityQuestions\Handler(),
|
||||
'verify' => new Handler()
|
||||
);
|
||||
|
||||
foreach( Application::allExtensions( 'core', 'MFAHandler', null, 'core' ) as $ext )
|
||||
{
|
||||
$return[ $ext::$key ] = $ext;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+256
-4
@@ -44,6 +44,9 @@ use IPS\Helpers\Form\Text;
|
||||
use IPS\Helpers\Form\TextArea;
|
||||
use IPS\Helpers\Form\Upload;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Helpers\Menu;
|
||||
use IPS\Helpers\Menu\Buttons;
|
||||
use IPS\Helpers\Menu\Link;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Http\Url\Friendly;
|
||||
use IPS\IPS;
|
||||
@@ -52,6 +55,7 @@ use IPS\Log;
|
||||
use IPS\Math\Number;
|
||||
use IPS\Member;
|
||||
use IPS\Member\Club\CustomField;
|
||||
use IPS\Member\Club\Page as ClubPage;
|
||||
use IPS\nexus\Customer;
|
||||
use IPS\nexus\Invoice;
|
||||
use IPS\nexus\Money;
|
||||
@@ -930,6 +934,7 @@ class Club extends ActiveRecord implements Embeddable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
$values = array_filter( $values, function ( $val) { return ( $val !== '' AND $val !== NULL ); } );
|
||||
|
||||
return $values;
|
||||
}
|
||||
@@ -944,9 +949,10 @@ class Club extends ActiveRecord implements Embeddable
|
||||
*
|
||||
* @param bool $getOverlay If FALSE, will not set the overlay, which saves queries if it will not be used (such as in clubCard)
|
||||
* @param string $position Position of cover photo
|
||||
* @param Model|ClubPage|null $container Node or Page currently viewing
|
||||
* @return CoverPhoto
|
||||
*/
|
||||
public function coverPhoto( bool $getOverlay=TRUE, string $position='full' ): CoverPhoto
|
||||
public function coverPhoto( bool $getOverlay=TRUE, string $position='full', Model|ClubPage|null $container=null ): CoverPhoto
|
||||
{
|
||||
$photo = new CoverPhoto;
|
||||
|
||||
@@ -958,7 +964,7 @@ class Club extends ActiveRecord implements Embeddable
|
||||
}
|
||||
if ( $getOverlay )
|
||||
{
|
||||
$photo->overlay = Theme::i()->getTemplate( 'clubs', 'core', 'front' )->coverPhotoOverlay( $this, $position );
|
||||
$photo->overlay = Theme::i()->getTemplate( 'clubs', 'core', 'front' )->coverPhotoOverlay( $this, $position, $container );
|
||||
}
|
||||
$photo->editable = $this->isLeader();
|
||||
$photo->object = $this;
|
||||
@@ -1491,12 +1497,19 @@ class Club extends ActiveRecord implements Embeddable
|
||||
{
|
||||
$class = $row['node_class'];
|
||||
$classBits = explode( '\\', $class );
|
||||
|
||||
if( !Application::load( $classBits[1] )->_enabled )
|
||||
try
|
||||
{
|
||||
if( !Application::load( $classBits[1] )->_enabled )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $e)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
/* Make sure the class exists. This can happen in the case of Pages databases/categories */
|
||||
if( !class_exists( $row['node_class'] ) )
|
||||
{
|
||||
@@ -2097,6 +2110,207 @@ class Club extends ActiveRecord implements Embeddable
|
||||
return static::$tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the club menu
|
||||
*
|
||||
* @param Model|ClubPage|null $container
|
||||
* @return Menu
|
||||
*/
|
||||
public function menu( Model|ClubPage|null $container = null ) : Menu
|
||||
{
|
||||
$menu = new Menu( 'club_manage', css: 'ipsButton ipsButton--inherit' );
|
||||
if( Settings::i()->clubs_header == 'sidebar' )
|
||||
{
|
||||
$menu->css .= ' i-width_100p i-margin-top_2';
|
||||
}
|
||||
|
||||
if( !$this->isLeader() and $this->owner->member_id != Member::loggedIn()->member_id and !Member::loggedIn()->modPermission( 'can_access_all_clubs' ) )
|
||||
{
|
||||
return $menu;
|
||||
}
|
||||
|
||||
if( !Settings::i()->clubs_require_approval or $this->approved )
|
||||
{
|
||||
if ( $this->isLeader() and $nodeTypes = static::availableNodeTypes() )
|
||||
{
|
||||
foreach( $nodeTypes as $nodeType )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( [ 'do' => 'nodeForm', 'type' => $nodeType ] ), $nodeType::clubFrontTitle(), dataAttributes: [ 'data-ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'club_create_node' ) ], opensDialog: true ) );
|
||||
}
|
||||
}
|
||||
|
||||
if( ( $this->owner and $this->owner->member_id == Member::loggedIn()->member_id ) or Member::loggedIn()->modPermission( 'can_access_all_clubs' ) or Member::loggedIn()->modPermission( 'can_manage_featured_clubs' ) )
|
||||
{
|
||||
$menu->addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->isLeader() )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( 'do', 'editPhoto' ), 'club_profile_photo', dataAttributes: [
|
||||
'data-ipsDialog' => true,
|
||||
'data-ipsDialog-modal' => true,
|
||||
'data-ipsDialog-forceReload' => true,
|
||||
'data-ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'club_profile_photo' ),
|
||||
'data-action' => 'editPhoto'
|
||||
] ) );
|
||||
}
|
||||
|
||||
if( ( $this->owner and $this->owner->member_id == Member::loggedIn()->member_id ) or Member::loggedIn()->modPermission( 'can_access_all_clubs' ) )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( 'do', 'edit' ), 'club_edit_settings', opensDialog: true ) );
|
||||
}
|
||||
|
||||
if( Member::loggedIn()->modPermission( 'can_manage_featured_clubs' ) )
|
||||
{
|
||||
if( $this->featured )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->csrf()->setQueryString( 'do', 'unfeature' ), 'club_unfeature', dataAttributes: [ 'data-confirm' => '' ] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$menu->add( new Link( $this->url()->csrf()->setQueryString( 'do', 'feature' ), 'club_feature', dataAttributes: [ 'data-confirm' => '' ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->canInvite() )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( 'do', 'invite' ), 'club_invite_members', dataAttributes: [ 'data-ipsDialog-size' => 'narrow' ], opensDialog: true ) );
|
||||
}
|
||||
|
||||
if( $this->canManageNavigation() and $container instanceof Model and count( $this->tabs( $container ) ) > 1 )
|
||||
{
|
||||
$menu->add( new Link( '#', 'reorder_club_menu', dataAttributes: [ 'data-action' => 'reorderClubmenu' ] ) );
|
||||
}
|
||||
|
||||
if( ClubPage::canAdd( $this ) )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( 'do', 'addPage' ), 'club_add_page', dataAttributes: [
|
||||
'data-ipsDialog-title' => Member::loggedIn()->language()->addToStack( 'add_page_to_club', true, [ 'sprintf' => $this->name ] )
|
||||
], opensDialog: true ) );
|
||||
}
|
||||
|
||||
if( $container instanceof ClubPage)
|
||||
{
|
||||
if( $container->canEdit() )
|
||||
{
|
||||
$menu->add( new Link( $container->url( 'edit' ), 'edit_page', opensDialog: true ) );
|
||||
}
|
||||
if( $container->canDelete() )
|
||||
{
|
||||
$menu->add( new Link( $container->url( 'delete' )->csrf(), 'delete_page', dataAttributes: [ 'data-confirm' => '' ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
if( $container instanceof Model and $this->isLeader() )
|
||||
{
|
||||
$menu->addSeparator();
|
||||
$menu->add( new Link( $this->url()->setQueryString( [ 'do' => 'nodeForm', 'type' => get_class( $container ), 'node' => $container->_id ] ), Member::loggedIn()->language()->addToStack( 'clubs_edit_this_container', true, [ 'sprintf' => Member::loggedIn()->language()->addToStack( $container::$nodeTitle . '_sg' ) ] ), dataAttributes: [ 'data-ipsDialog-title' => $container->_title ], opensDialog: true ) );
|
||||
|
||||
if( isset( $container::$contentItemClass ) and $itemClass = $container::$contentItemClass and ( $container->modPermission( 'delete', Member::loggedIn(), $itemClass ) or !!$itemClass::contentCount( $container, true, true, true, 1 ) ) )
|
||||
{
|
||||
$menu->add( new Link( $this->url()->setQueryString( [ 'do' => 'nodeDelete', 'type' => get_class( $container ), 'node' => $container->_id] )->csrf(), Member::loggedIn()->language()->addToStack( 'clubs_delete_this_container', true, [ 'sprintf' => Member::loggedIn()->language()->addToStack( $container::$nodeTitle . '_sg' ) ] ), dataAttributes: [ 'data-confirm' => '', 'data-confirmSubMessage' => Member::loggedIn()->language()->addToStack( 'clubs_delete_container_confirm' ) ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
foreach( Application::allExtensions( 'core', 'Club' ) AS $ext )
|
||||
{
|
||||
foreach( $ext->menu( $this, $container ) as $link )
|
||||
{
|
||||
$menu->add( $link );
|
||||
}
|
||||
}
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a buttons list for the club header
|
||||
*
|
||||
* @return Buttons
|
||||
*/
|
||||
public function buttons() : Buttons
|
||||
{
|
||||
$buttons = new Buttons();
|
||||
|
||||
$memberStatus = $this->memberStatus( Member::loggedIn() );
|
||||
if( $this->type !== static::TYPE_PUBLIC )
|
||||
{
|
||||
if( $memberStatus === static::STATUS_BANNED )
|
||||
{
|
||||
$buttons->addButton( null, 'club_banned_title', icon: 'fa-solid fa-xmark', tooltip: 'club_banned_desc_short' );
|
||||
}
|
||||
elseif( in_array( $memberStatus, [ static::STATUS_MEMBER, static::STATUS_MODERATOR, static::STATUS_LEADER, static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR, static::STATUS_WAITING_PAYMENT ] ) )
|
||||
{
|
||||
if( $memberStatus === static::STATUS_WAITING_PAYMENT )
|
||||
{
|
||||
$buttons->addButton( $this->url()->setQueryString( 'do', 'join' )->csrf(), 'club_pay_membership_fee' );
|
||||
}
|
||||
if( in_array( $memberStatus, [ static::STATUS_EXPIRED, static::STATUS_EXPIRED_MODERATOR ] ) )
|
||||
{
|
||||
$buttons->addButton( $this->url()->setQueryString( 'do', 'renew' )->csrf(), 'club_renew_membership' );
|
||||
}
|
||||
if( !$this->owner or $this->owner->member_id != Member::loggedIn()->member_id )
|
||||
{
|
||||
$attributes = [ 'data-confirm' => '' ];
|
||||
if( $this->isPaid() )
|
||||
{
|
||||
$attributes['data-confirmSubMessage'] = Member::loggedIn()->language()->addToStack( 'club_leave_paid_warning' );
|
||||
}
|
||||
$buttons->addButton( $this->url()->setQueryString( 'do', 'leave' )->csrf(), 'club_leave', dataAttributes: $attributes );
|
||||
}
|
||||
}
|
||||
elseif( $this->canJoin() )
|
||||
{
|
||||
/* We skip the join button for the sidebar view; it's shown somewhere else */
|
||||
if( Settings::i()->clubs_header != 'sidebar' )
|
||||
{
|
||||
$attributes = [];
|
||||
if( $this->isPaid() and $memberStatus !== static::STATUS_INVITED_BYPASSING_PAYMENT )
|
||||
{
|
||||
$attributes = [
|
||||
'data-confirm' => '',
|
||||
'data-confirmIcon' => 'info',
|
||||
'data-confirmMessage' => Member::loggedIn()->language()->addToStack( 'club_membership_item' ),
|
||||
'data-confirmSubmessage' => Member::loggedIn()->language()->addToStack( $this->memberFeeMessage() )
|
||||
];
|
||||
}
|
||||
$buttons->addButton( $this->url()->csrf()->setQueryString( 'do', 'join' )->addRef( Request::i()->url() ), 'club_join', dataAttributes: $attributes );
|
||||
}
|
||||
}
|
||||
elseif( !$this->canRead() )
|
||||
{
|
||||
if( $memberStatus === static::STATUS_REQUESTED )
|
||||
{
|
||||
$buttons->addButton( $this->url()->csrf()->setQueryString( 'do', 'cancelJoin' ), 'club_cancel_request', 'ipsButton ipsButton--text', icon: 'fa-solid fa-xmark' );
|
||||
}
|
||||
elseif( $memberStatus === static::STATUS_DECLINED )
|
||||
{
|
||||
$buttons->addButton( null, 'club_denied_title', dataAttributes: [ 'aria-disabled' => 'true' ], icon: 'fa-solid fa-xmark', tooltip: Member::loggedIn()->language()->addToStack( 'club_denied_desc_short' ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Extensions */
|
||||
foreach( Application::allExtensions( 'core', 'Club' ) AS $ext )
|
||||
{
|
||||
foreach( $ext->buttons( $this ) as $button )
|
||||
{
|
||||
if( is_string( $button ) )
|
||||
{
|
||||
$buttons->addHtml( $button );
|
||||
}
|
||||
else
|
||||
{
|
||||
$buttons->add( $button );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can a member view the members page
|
||||
*
|
||||
@@ -2383,4 +2597,42 @@ class Club extends ActiveRecord implements Embeddable
|
||||
|
||||
$purchase->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the club nodes and set the last_activity
|
||||
*
|
||||
* @param Member|null $member
|
||||
* @return void
|
||||
*/
|
||||
public function updateLastActivityAndItemCount( Member $member = null ) : void
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
$this->content = 0;
|
||||
$this->last_activity = 0;
|
||||
|
||||
foreach ( $this->nodes() as $node )
|
||||
{
|
||||
try
|
||||
{
|
||||
$nodeClass = $node['node_class'];
|
||||
if ( ! class_exists( $nodeClass ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$node = $nodeClass::load( $node['node_id'] );
|
||||
|
||||
if ( $lastCommentTime = $node->getLastCommentTime( $member ) and $lastCommentTime->getTimestamp() > $this->last_activity )
|
||||
{
|
||||
$this->last_activity = $lastCommentTime->getTimestamp();
|
||||
}
|
||||
|
||||
$this->content += (int) $node->getContentItemCount();
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
}
|
||||
|
||||
$this->rebuilt = time();
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@ class Group extends ActiveRecord
|
||||
'gbw_club_manage_indexing' => 1024, // Users in this group can manage indexing of clubs
|
||||
'gbw_view_helpful' => 2048, // Users in this group can view who found content helpful
|
||||
'gbw_change_layouts' => 4096, // Users in this group can change the layouts of key areas set by themes
|
||||
'gbw_can_mark_helpful' => 8192, // Users in this group can mark content as helpful
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -18,9 +18,7 @@ use IPS\DateTime;
|
||||
use IPS\Db;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Log;
|
||||
use IPS\Member;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use IPS\Theme;
|
||||
use Throwable;
|
||||
use function defined;
|
||||
use const IPS\Helpers\Table\SEARCH_DATE_RANGE;
|
||||
@@ -148,17 +146,7 @@ class History extends \IPS\Helpers\Table\Db
|
||||
},
|
||||
'log_member'=> function( $val, $row ) use ( $extensions )
|
||||
{
|
||||
try
|
||||
{
|
||||
return $extensions[ $row['log_app'] ]->parseLogMember( $val, $row );
|
||||
}
|
||||
catch( Throwable $e )
|
||||
{
|
||||
Log::log( $e, 'member_history' );
|
||||
}
|
||||
|
||||
$member = Member::load( $val );
|
||||
return Theme::i()->getTemplate( 'global', 'core' )->userPhoto( $member, 'tiny' ) . ' ' . $member->link();
|
||||
return $extensions[ $row['log_app'] ]->parseLogMember( $val, $row );
|
||||
},
|
||||
'log_data' => function( $val, $row ) use ( $extensions )
|
||||
{
|
||||
@@ -171,8 +159,6 @@ class History extends \IPS\Helpers\Table\Db
|
||||
Log::log( $e, 'member_history' );
|
||||
return $val; # Return the value so the admin may have some clue as to what the log entry was for
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1121,7 +1121,7 @@ class Member extends ActiveRecord
|
||||
|
||||
if( $this->_data['mgroup_others'] )
|
||||
{
|
||||
foreach( explode( ',', $this->_data['mgroup_others'] ) as $id )
|
||||
foreach( array_filter( explode( ',', $this->_data['mgroup_others'] ) ) as $id )
|
||||
{
|
||||
$this->_groups[] = intval( $id );
|
||||
}
|
||||
@@ -3514,14 +3514,14 @@ class Member extends ActiveRecord
|
||||
"(assign_type=? and assign_to=?)"
|
||||
];
|
||||
$binds = [
|
||||
Assignment::ASSIGNMENT_TEAM,
|
||||
Assignment::ASSIGNMENT_MEMBER,
|
||||
$this->member_id
|
||||
];
|
||||
|
||||
if( $teams = $this->teams() )
|
||||
{
|
||||
$clause[] = "(assign_type=? and " . Db::i()->in( 'assign_to', array_keys( $teams ) ) . ")";
|
||||
$binds[] = Assignment::ASSIGNMENT_MEMBER;
|
||||
$binds[] = Assignment::ASSIGNMENT_TEAM;
|
||||
}
|
||||
|
||||
$where = array_merge( array( implode( " OR ", $clause ) ), $binds );
|
||||
@@ -4123,7 +4123,7 @@ class Member extends ActiveRecord
|
||||
}
|
||||
|
||||
/* Otherwise return it */
|
||||
return $permissions[$key] ?? NULL;
|
||||
return $permissions[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5077,9 +5077,9 @@ class Member extends ActiveRecord
|
||||
|
||||
/* Send referral notification */
|
||||
$this->_sendReferralNotification();
|
||||
|
||||
|
||||
/* Send an admin notification */
|
||||
AdminNotification::send( 'core', 'NewRegComplete', NULL, TRUE, $this, !$initiatedByAdmin );
|
||||
AdminNotification::send( 'core', 'NewRegComplete', NULL, TRUE, $this, $initiatedByAdmin );
|
||||
|
||||
/* Delete rows */
|
||||
Db::i()->delete( 'core_validating', array( 'member_id=?', $this->member_id ) );
|
||||
|
||||
@@ -28,6 +28,7 @@ use IPS\Node\Model;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use IPS\Settings;
|
||||
use OutOfBoundsException;
|
||||
use OutOfRangeException;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function is_array;
|
||||
@@ -462,8 +463,15 @@ class ProfileStep extends Model
|
||||
}
|
||||
|
||||
list( $app, $extension ) = explode( '_', $this->_data['extension'] );
|
||||
$class = "\\IPS\\{$app}\\extensions\\core\\ProfileSteps\\{$extension}";
|
||||
return new $class;
|
||||
try
|
||||
{
|
||||
$class = Application::getExtensionClass( $app, 'ProfileSteps', $extension );
|
||||
return new $class;
|
||||
}
|
||||
catch( OutOfRangeException )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,18 +650,16 @@ class ProfileStep extends Model
|
||||
$return[ str_replace( 'profile_', '', $key ) ] = $value;
|
||||
}
|
||||
|
||||
try
|
||||
/*try
|
||||
{
|
||||
$ext = static::findExtensionFromAction( $return['step_completion_act'] );
|
||||
$this->extension = $ext;
|
||||
$ext = explode( '_', $ext );
|
||||
$class = "\\IPS\\{$ext[0]}\\extensions\\core\\ProfileSteps\\{$ext[1]}";
|
||||
$extension = new $class;
|
||||
$ext = explode( '_', static::findExtensionFromAction( $return['step_completion_act'] ) );
|
||||
$class = Application::getExtensionClass( $ext[0], 'ProfileSteps', $ext[1] );
|
||||
$return['step_extension'] = implode( "_", $ext );
|
||||
}
|
||||
catch( OutOfBoundsException $e )
|
||||
catch( OutOfBoundsException | OutOfRangeException $e )
|
||||
{
|
||||
throw new InvalidArgumentException;
|
||||
}
|
||||
}*/
|
||||
|
||||
return $return;
|
||||
}
|
||||
@@ -746,8 +752,12 @@ class ProfileStep extends Model
|
||||
if( $extensionKey = static::findExtensionFromAction( $key ) )
|
||||
{
|
||||
list( $app, $extension ) = explode( '_', $extensionKey );
|
||||
$class = "\\IPS\\{$app}\\extensions\\core\\ProfileSteps\\{$extension}";
|
||||
return new $class;
|
||||
try
|
||||
{
|
||||
$class = Application::getExtensionClass( $app, 'ProfileSteps', $extension );
|
||||
return new $class;
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -17,11 +17,13 @@ namespace IPS\Member;
|
||||
use IPS\Application;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Dispatcher;
|
||||
use IPS\Extensions\MemberFilterAbstract;
|
||||
use IPS\Helpers\CoverPhoto;
|
||||
use IPS\Helpers\Menu;
|
||||
use IPS\Helpers\Menu\Link;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Member;
|
||||
use IPS\Output\UI\MenuExtension;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use IPS\Theme;
|
||||
@@ -256,6 +258,29 @@ class UserMenu
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification count displayed only on the mobile menu.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function mobileNotificationCount() : int
|
||||
{
|
||||
$notificationCount = 0;
|
||||
|
||||
if( Member::loggedIn()->canAccessModule( Module::get( 'core', 'modcp' ) ) and Member::loggedIn()->canAccessReportCenter() )
|
||||
{
|
||||
$notificationCount += Member::loggedIn()->reportCount();
|
||||
}
|
||||
|
||||
foreach( Application::allExtensions( 'core', 'UserMenu', Member::loggedIn() ) as $ext )
|
||||
{
|
||||
/** @var MenuExtension $ext */
|
||||
$notificationCount += $ext->mobileNotificationCount();
|
||||
}
|
||||
|
||||
return $notificationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Member $member
|
||||
* @return Menu|null
|
||||
@@ -268,11 +293,11 @@ class UserMenu
|
||||
return null;
|
||||
}
|
||||
|
||||
$menu = new Menu( 'profile_edit','fa-regular fa-pen-to-square');
|
||||
$menu = new Menu( 'profile_edit','fa-solid fa-pen-to-square');
|
||||
$menu->css = 'ipsButton ipsButton--overlay';
|
||||
|
||||
/* Only the logged in member should be able to modify the photos */
|
||||
if( Member::loggedIn()->member_id == $member->member_id AND explode( ':', $member->group['g_photo_max_vars'] )[0] )
|
||||
if( explode( ':', $member->group['g_photo_max_vars'] )[0] )
|
||||
{
|
||||
$link = new Link( Url::internal( "app=core&module=members&controller=profile&do=editPhoto&id={$member->member_id}", 'front','edit_profile_photo', $member->members_seo_name ), 'profile_edit_photo_tab',icon: 'fa-regular fa-circle-user' );
|
||||
$link->dataAttributes['data-action'] = 'editPhoto';
|
||||
@@ -303,7 +328,7 @@ class UserMenu
|
||||
}
|
||||
}
|
||||
|
||||
$link = new Link( $member->url()->setQueryString('do', 'edit'), 'profile_edit', icon: 'fa-regular fa-pen-to-square');
|
||||
$link = new Link( $member->url()->setQueryString('do', 'edit'), 'profile_edit', icon: 'fa-solid fa-pen-to-square');
|
||||
$link->opensDialog('profile_edit');
|
||||
$menu->add( $link);
|
||||
if( $member->member_id === Member::loggedIn()->member_id )
|
||||
|
||||
@@ -297,7 +297,7 @@ class Controller extends DispatcherController
|
||||
$this->_titleHtml,
|
||||
$this->_descriptionHtml,
|
||||
$node->canAdd(),
|
||||
( $node instanceof $nodeClass ),
|
||||
$this->_canBeRoot( $node ),
|
||||
$this->_getRowHtml( $node ) . $node->ui( 'rowHtml' ),
|
||||
isset( $node->_lockedLang ) ? $node->_lockedLang : null,
|
||||
isset( $class::$featureColumnName ),
|
||||
@@ -354,6 +354,17 @@ class Controller extends DispatcherController
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the node can be a root-level
|
||||
*
|
||||
* @param Model $node
|
||||
* @return bool
|
||||
*/
|
||||
public function _canBeRoot( Model $node ) : bool
|
||||
{
|
||||
return $node instanceof $this->nodeClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/Edit Form
|
||||
*
|
||||
@@ -1198,7 +1209,7 @@ class Controller extends DispatcherController
|
||||
}
|
||||
catch ( OutOfRangeException $e ) { }
|
||||
|
||||
return Theme::i()->getTemplate( 'global', 'core', 'global' )->titleWithLink( $value, $groupLink, 'all_permissions', Member::loggedIn()->language()->addToStack( "all_permissions_group_hovertitle", FALSE, array( 'sprintf' => [ $value ] ) ) );
|
||||
return Theme::i()->getTemplate( 'global', 'core', 'global' )->titleWithLink( $value, $groupLink, 'all_permissions', Member::loggedIn()->language()->addToStack( "all_permissions_group_hovertitle", FALSE, array( 'htmlsprintf' => [ $value ] ) ) );
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief CustomBadge
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @subpackage
|
||||
* @since 3/6/2025
|
||||
*/
|
||||
|
||||
namespace IPS\Node;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\core\CustomBadge as CustomBadgeClass;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Color;
|
||||
use IPS\Helpers\Form\Icon;
|
||||
use IPS\Helpers\Form\Number;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\YesNo;
|
||||
use IPS\Output;
|
||||
use IPS\Theme;
|
||||
use UnderflowException;
|
||||
use function get_called_class;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
trait CustomBadge
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected static array $customBadgeStore = array();
|
||||
|
||||
/**
|
||||
* Get the custom badge linked to this class
|
||||
*
|
||||
* @return CustomBadgeClass|null
|
||||
*/
|
||||
public function getRecordBadge() : CustomBadgeClass|null
|
||||
{
|
||||
$class = get_called_class();
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
if( !isset( static::$customBadgeStore[ $class ][ $this->$idColumn ] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$row = Db::i()->select( '*', 'core_custom_badges', [ 'class=? and active_record_id=?', $class, $this->$idColumn ] )->first();
|
||||
static::$customBadgeStore[ $class ][ $this->$idColumn ] = CustomBadgeClass::constructFromData( $row );
|
||||
}
|
||||
catch( UnderflowException )
|
||||
{
|
||||
static::$customBadgeStore[ $class ][ $this->$idColumn ] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return static::$customBadgeStore[ $class ][ $this->$idColumn ] ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add elements to the form which are responsible for creating custom badges, and loads the JS and CSS that forms the interactive and grid-style UI
|
||||
*
|
||||
* @param Form $form
|
||||
* @param CustomBadgeClass|null $badge
|
||||
* @return void
|
||||
*/
|
||||
public function addBadgeFieldsToForm( Form $form, ?CustomBadgeClass $badge=null ) : void
|
||||
{
|
||||
$form->class .= " ipsBadgePicker--form";
|
||||
|
||||
$shapeToggles = ["custombadge_background", "custombadge_border"];
|
||||
$dynamicShapeToggles = [ "custombadge_sides", "custombadge_rotation" ];
|
||||
$form->add( new YesNo(
|
||||
"custombadge_use_custom",
|
||||
(bool) $badge,
|
||||
true,
|
||||
[
|
||||
'togglesOff' => static::$customBadgeToggles ?? [],
|
||||
'togglesOn' => [
|
||||
"{$form->id}_header_badge_preview",
|
||||
"custombadge_border",
|
||||
"custombadge_foreground",
|
||||
"custombadge_background",
|
||||
"custombadge_shape",
|
||||
"custombadge_icon",
|
||||
"custombadge_sides",
|
||||
"custombadge_rotation",
|
||||
"custombadge_number_overlay",
|
||||
"custombadge_icon_size",
|
||||
'preview'
|
||||
]
|
||||
]
|
||||
));
|
||||
|
||||
$form->addHeader( 'custombadge_preview' );
|
||||
|
||||
Output::i()->jsFiles = array_merge( Output::i()->jsFiles, Output::i()->js( 'admin_badgepreview.js', 'core', 'admin' ) );
|
||||
$form->addHtml( Theme::i()->getTemplate( 'achievements', 'core', 'admin' )->badgePreview( $badge ? $badge->raw : '' ) );
|
||||
|
||||
$form->add( new Radio( "custombadge_shape", $badge ? $badge->shape : 'circle', null, ['options' => [
|
||||
'square' => 'custombadge_square',
|
||||
'circle' => 'custombadge_circle',
|
||||
'ngon' => 'custombadge_n_gon',
|
||||
'star' => 'custombadge_star',
|
||||
'flower' => 'custombadge_flower',
|
||||
"no_shape" => "custombadge_no_shape",
|
||||
], 'toggles' => [
|
||||
'ngon' => [...$dynamicShapeToggles, ...$shapeToggles],
|
||||
'star' => [...$dynamicShapeToggles, ...$shapeToggles],
|
||||
'flower' => [...$dynamicShapeToggles, ...$shapeToggles],
|
||||
"square" => $shapeToggles,
|
||||
"circle" => $shapeToggles
|
||||
], 'rowClasses' => ['ipsFieldRow--badge-creator ipsFieldRow--badge-creator-shape']], null, null, null, "custombadge_shape") );
|
||||
|
||||
$form->add( new Icon( "custombadge_icon", ( $badge and $badge->icon ) ? [ $badge->icon ] : null, false, ['useSvgIcon' => true, 'rowClasses' => ['ipsFieldRow--badge-creator ipsFieldRow--badge-creator-icon']], null, null, null, "custombadge_icon" ) );
|
||||
|
||||
/* The overlay is optional */
|
||||
if ( !isset( static::$customBadgeNumberOverlay ) or static::$customBadgeNumberOverlay )
|
||||
{
|
||||
$form->add( new Number( "custombadge_number_overlay", ( $badge and $badge->number_overlay ) ? $badge->number_overlay : 0, false, [ 'step' => 1, 'min' => 1, 'max' => 999, 'unlimited' => 0, 'unlimitedLang' => 'none', 'rowClasses' => ['ipsFieldRow--badge-creator'] ], null, null, null, "custombadge_number_overlay" ) );
|
||||
}
|
||||
|
||||
$form->add( new Color( "custombadge_background", $badge ? $badge->background : '#eeb95f', null, ['rowClasses' => ['ipsFieldRow--badge-creator ipsFieldRow--badge-creator-background']], null, null, null, "custombadge_background" ) );
|
||||
$form->add( new Color( "custombadge_border", $badge ? $badge->border : '#f7d36f', null, ['rowClasses' => ['ipsFieldRow--badge-creator ipsFieldRow--badge-creator-border']], null, null, null, "custombadge_border" ) );
|
||||
$form->add( new Color( "custombadge_foreground", $badge ? $badge->foreground : '#ffffff', null, ['rgba' => true, 'swatches' => true, 'rowClasses' => ['ipsFieldRow--badge-creator ipsFieldRow--badge-creator-foreground']], null, null, null, "custombadge_foreground" ) );
|
||||
|
||||
$form->add( new Form\Number( "custombadge_icon_size", $badge ? $badge->icon_size : 3, null, ["min" => 1, "max" => 5, "step" => 1, "range" => true, "rowClasses" => ['ipsFieldRow--badge-creator', 'ipsFieldRow--badge-creator-icon-size']], id: "custombadge_icon_size" ) );
|
||||
|
||||
$form->add( new Number( "custombadge_sides", $badge ? $badge->sides : 5, null, ['step' => 1, 'min' => 4, 'max' => 12, 'range' => true, 'rowClasses' => ['ipsFieldRow--badge-creator']], null, null, null, "custombadge_sides" ) );
|
||||
$form->add( new Number( "custombadge_rotation", $badge?->rotation, null, ['step' => 1, 'min' => 0, 'max' => 90, 'range' => true, 'rowClasses' => ['ipsFieldRow--badge-creator']], null, null, null, "custombadge_rotation" ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $values
|
||||
* @return void
|
||||
*/
|
||||
public function saveRecordBadge( array &$values ) : void
|
||||
{
|
||||
$badge = $this->getRecordBadge();
|
||||
|
||||
/* Are we using a badge? */
|
||||
if( $values['custombadge_use_custom'] )
|
||||
{
|
||||
if( $badge === null )
|
||||
{
|
||||
$badge = new CustomBadgeClass;
|
||||
$badge->class = get_called_class();
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$badge->active_record_id = $this->$idColumn;
|
||||
}
|
||||
|
||||
$badge->shape = $values['custombadge_shape'];
|
||||
$badge->foreground = $values['custombadge_foreground'];
|
||||
$badge->background = $values['custombadge_background'];
|
||||
$badge->border = $values['custombadge_border'];
|
||||
$badge->icon = $values['custombadge_icon'][0] ?? null;
|
||||
$badge->rotation = $values['custombadge_rotation'];
|
||||
$badge->sides = $values['custombadge_sides'];
|
||||
$badge->number_overlay = $values['custombadge_number_overlay'] ?? 0;
|
||||
$badge->icon_size = $values['custombadge_icon_size'] ?? 3;
|
||||
$badge->save();
|
||||
}
|
||||
elseif( $badge !== null )
|
||||
{
|
||||
/* If we are not using a custom badge, and we already have one, delete it */
|
||||
$badge->delete();
|
||||
}
|
||||
|
||||
/* Clear the form fields */
|
||||
foreach ( ['shape', 'foreground', 'background', 'border', 'icon', 'rotation', 'sides', 'number_overlay', 'use_custom', 'icon_size'] as $key )
|
||||
{
|
||||
unset( $values["custombadge_{$key}"] );
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
-8
@@ -14,7 +14,6 @@ namespace IPS\Node;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Application\Module;
|
||||
use IPS\Content\ClubContainer;
|
||||
use IPS\Content\Comment;
|
||||
@@ -79,6 +78,7 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
* @mixin Colorize
|
||||
* @mixin Permissions
|
||||
* @mixin Icon
|
||||
* @mixin CustomBadge
|
||||
*/
|
||||
abstract class Model extends ActiveRecord
|
||||
{
|
||||
@@ -2747,7 +2747,7 @@ abstract class Model extends ActiveRecord
|
||||
{
|
||||
$canView = true;
|
||||
}
|
||||
elseif( $container !== null AND in_array( $containerId, $perms[ static::$modPerm ] ) )
|
||||
elseif( $container !== null and !empty( $perms[ static::$modPerm ] ) and in_array( $containerId, $perms[ static::$modPerm ] ) )
|
||||
{
|
||||
$canView = true;
|
||||
}
|
||||
@@ -2817,6 +2817,15 @@ abstract class Model extends ActiveRecord
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
/* Validate the counts, sometimes we inadvertently end up with a count less than 0 */
|
||||
foreach( [ '_items', '_comments', '_reviews', '_unapprovedItems', '_unapprovedComments', '_unapprovedReviews', '_futureItems' ] as $column )
|
||||
{
|
||||
if( $this->$column !== null and $this->$column < 0 )
|
||||
{
|
||||
$this->$column = 0;
|
||||
}
|
||||
}
|
||||
|
||||
parent::save();
|
||||
|
||||
if ( in_array( Permissions::class, class_implements( $this ) ) and $this->_permissions !== NULL and $this->_permissions != $this->_originalPermissions )
|
||||
@@ -3529,6 +3538,15 @@ abstract class Model extends ActiveRecord
|
||||
}
|
||||
catch( OutOfRangeException ){}
|
||||
|
||||
/* Delete custom badges */
|
||||
if( IPS::classUsesTrait( $this, CustomBadge::class ) )
|
||||
{
|
||||
if( $badge = $this->getRecordBadge() )
|
||||
{
|
||||
$badge->delete();
|
||||
}
|
||||
}
|
||||
|
||||
parent::delete();
|
||||
|
||||
Event::fire( 'onDelete', $this );
|
||||
@@ -3644,10 +3662,25 @@ abstract class Model extends ActiveRecord
|
||||
public function form( Form &$form ) : void
|
||||
{
|
||||
/* Now loop through and add all the elements to the form */
|
||||
foreach( UiExtension::i()->run( $this, 'formElements' ) as $element )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
if( static::$canBeExtended )
|
||||
{
|
||||
foreach( UiExtension::i()->run( $this, 'formElements' ) as $element )
|
||||
{
|
||||
if( is_object( $element ) )
|
||||
{
|
||||
$form->add( $element );
|
||||
}
|
||||
else
|
||||
{
|
||||
$form->addHtml( $element );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, CustomBadge::class ) )
|
||||
{
|
||||
$this->addBadgeFieldsToForm( $form, $this->getRecordBadge() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3670,9 +3703,17 @@ abstract class Model extends ActiveRecord
|
||||
public function saveForm( array $values ): mixed
|
||||
{
|
||||
/* Loop through each extension and update the values */
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
if( static::$canBeExtended )
|
||||
{
|
||||
$values = $extension->processForm( $this, $values );
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
{
|
||||
$values = $extension->processForm( $this, $values );
|
||||
}
|
||||
}
|
||||
|
||||
if( IPS::classUsesTrait( $this, CustomBadge::class ) )
|
||||
{
|
||||
$this->saveRecordBadge( $values );
|
||||
}
|
||||
|
||||
foreach ( $values as $k => $v )
|
||||
|
||||
@@ -31,6 +31,7 @@ use IPS\Http\Url\Internal;
|
||||
use IPS\Output\Javascript;
|
||||
use IPS\Output\System as SystemOutput;
|
||||
use IPS\Session\Front;
|
||||
use IPS\Widget\Area;
|
||||
use OutOfRangeException;
|
||||
use RuntimeException;
|
||||
use UnderflowException;
|
||||
@@ -43,12 +44,18 @@ use function get_called_class;
|
||||
use function in_array;
|
||||
use function intval;
|
||||
use function is_array;
|
||||
use function json_encode;
|
||||
use function ob_start;
|
||||
use function stristr;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strstr;
|
||||
use function substr;
|
||||
use const JSON_HEX_AMP;
|
||||
use const JSON_HEX_APOS;
|
||||
use const JSON_HEX_TAG;
|
||||
use const JSON_PRETTY_PRINT;
|
||||
use const JSON_UNESCAPED_SLASHES;
|
||||
use const LIVE_TOPICS_DEV;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -1165,7 +1172,17 @@ class Output
|
||||
/* VLE language bits now parseOutputForDisplay has run */
|
||||
if ( Lang::vleActive() )
|
||||
{
|
||||
$output = str_replace( '<!--ipsVleWords-->', 'var ipsVle = ' . json_encode( Member::loggedIn()->language()->vleForJson(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS ) . ';', $output );
|
||||
if ( str_contains( $output, "<!--ipsVleWords-->" ) )
|
||||
{
|
||||
$vleObj = json_encode( Member::loggedIn()->language()->vleForJson(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS );
|
||||
$vleScriptContent = <<<JS
|
||||
|
||||
window.ipsVle = window.ipsVle || {};
|
||||
Object.assign(window.ipsVle, $vleObj);
|
||||
JS;
|
||||
|
||||
$output = str_replace( '<!--ipsVleWords-->', $vleScriptContent, $output );
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for any autosave cookies */
|
||||
@@ -1615,6 +1632,37 @@ class Output
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load any areas that should be displayed on all pages
|
||||
* For example, the global footer is always loaded
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function loadGlobalAreas() : void
|
||||
{
|
||||
foreach( Db::i()->select( '*', 'core_widget_areas', [ 'app=? and module=? and controller=?', 'global', 'global', 'global'] ) as $row )
|
||||
{
|
||||
if( in_array( $row['area'], Area::$reservedAreas ) )
|
||||
{
|
||||
if( $row['tree'] )
|
||||
{
|
||||
$area = new Area( json_decode( $row['tree'], true ), $row['area'] );
|
||||
}
|
||||
elseif( $row['widgets'] )
|
||||
{
|
||||
$area = Area::create( $row['area'], json_decode( $row['widgets'], true ) );
|
||||
}
|
||||
|
||||
if( isset( $area ) )
|
||||
{
|
||||
$this->sidebar['widgetareas'] = $this->sidebar['widgetareas'] ?? [];
|
||||
$this->sidebar['widgetareas'][$area->id] = $area;
|
||||
$this->sidebar['widgets'][$area->id] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and rebuilds JS map if it is broken
|
||||
*
|
||||
|
||||
@@ -41,6 +41,7 @@ class Datetime
|
||||
{
|
||||
$return = array();
|
||||
$return['pre'] = '$val = ( ' . $data . ' instanceof \IPS\DateTime ) ? ' . $data . ' : \IPS\DateTime::ts( ' . $data . ' );';
|
||||
$useTitle = isset( $options['notitle'] ) ? "useTitle: false" : "useTitle: true";
|
||||
|
||||
if( isset( $options['dateonly'] ) )
|
||||
{
|
||||
@@ -52,15 +53,15 @@ class Datetime
|
||||
}
|
||||
elseif ( isset( $options['lowercase'] ) )
|
||||
{
|
||||
$return['return'] = '$val->html(FALSE)';
|
||||
$return['return'] = '$val->html(FALSE, ' . $useTitle . ')';
|
||||
}
|
||||
elseif ( isset( $options['short'] ) )
|
||||
{
|
||||
$return['return'] = '$val->html(TRUE, TRUE)';
|
||||
$return['return'] = '$val->html(TRUE, TRUE, ' . $useTitle . ')';
|
||||
}
|
||||
else
|
||||
{
|
||||
$return['return'] = '$val->html()';
|
||||
$return['return'] = '$val->html(' . $useTitle . ')';
|
||||
}
|
||||
|
||||
return $return;
|
||||
|
||||
@@ -64,6 +64,9 @@ class Theme
|
||||
case 'custom_css_for_output':
|
||||
return "\IPS\Theme::i()->getCustomCssForOutput()";
|
||||
|
||||
case 'custom_css_for_theme_editor_codebox':
|
||||
return "\IPS\Theme::i()->getCustomCssForThemeEditorCodebox()";
|
||||
|
||||
case 'custom_css':
|
||||
return "\IPS\Theme::i()->custom_css";
|
||||
|
||||
@@ -92,7 +95,14 @@ class Theme
|
||||
return "\IPS\Theme::i()->getLayoutValue('{$variable}')";
|
||||
}
|
||||
|
||||
return ( array_key_exists( $data, SystemTheme::i()->settings ) ) ? "\IPS\Theme::i()->settings['{$data}']" : '';
|
||||
/* If we are still here, this is legacy syntax; treat it as a variable */
|
||||
$cssVariables = SystemTheme::i()->getCssVariables();
|
||||
if( array_key_exists( $data, $cssVariables ) )
|
||||
{
|
||||
return "\IPS\Theme::i()->getParsedCssVariableFromKey( '{$data}' )";
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
catch( InvalidArgumentException $e )
|
||||
{
|
||||
|
||||
@@ -99,11 +99,11 @@ abstract class Comment
|
||||
* Add custom sort options to the content table for this class
|
||||
* @note $comment will ALWAYS be null.
|
||||
*
|
||||
* @param BaseItem|null $comment
|
||||
* @param BaseComment|null $comment
|
||||
* @param Content $table
|
||||
* @return array
|
||||
*/
|
||||
public function contentTableSortOptions( ?BaseItem $comment, Content $table ) : array
|
||||
public function contentTableSortOptions( ?BaseComment $comment, Content $table ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -114,12 +114,12 @@ abstract class Comment
|
||||
*
|
||||
* @note $comment will ALWAYS be null.
|
||||
*
|
||||
* @param BaseItem|null $comment
|
||||
* @param BaseComment|null $comment
|
||||
* @param Content $table
|
||||
* @param string $sortBy
|
||||
* @return string|null
|
||||
*/
|
||||
public function contentTableSortDirection( ?BaseItem $comment, Content $table, string $sortBy ) : string|null
|
||||
public function contentTableSortDirection( ?BaseComment $comment, Content $table, string $sortBy ) : string|null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -128,11 +128,11 @@ abstract class Comment
|
||||
* Add custom filters to the content table for this class
|
||||
* @note $comment will ALWAYS be null.
|
||||
*
|
||||
* @param BaseItem|null $comment
|
||||
* @param BaseComment|null $comment
|
||||
* @param Content $table
|
||||
* @return array
|
||||
*/
|
||||
public function contentTableFilters( ?BaseItem $comment, Content $table ) : array
|
||||
public function contentTableFilters( ?BaseComment $comment, Content $table ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
@@ -141,12 +141,26 @@ abstract class Comment
|
||||
* Add custom conditions to the where clause for a content table.
|
||||
* @note $comment will ALWAYS be null.
|
||||
*
|
||||
* @param BaseItem|null $comment
|
||||
* @param BaseComment|null $comment
|
||||
* @param Content $table
|
||||
* @return array
|
||||
*/
|
||||
public function contentTableWhere( ?BaseItem $comment, Content $table ) : array
|
||||
public function contentTableWhere( ?BaseComment $comment, Content $table ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the table rows before output
|
||||
*
|
||||
* @note $comment will always be null
|
||||
* @param BaseComment|null $comment
|
||||
* @param Content $table
|
||||
* @param array $rows
|
||||
* @return void
|
||||
*/
|
||||
public function contentTableGetRows( ?BaseComment $comment, Content $table, array &$rows ) : void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -152,15 +152,32 @@ abstract class Item
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom conditions to the where clause for a content table.
|
||||
* Modify the where clause for a content table.
|
||||
* You can change existing where conditions, or add new ones
|
||||
*
|
||||
* @note $item will ALWAYS be null.
|
||||
*
|
||||
* @param BaseItem|null $item
|
||||
* @param Content $table
|
||||
* @return array
|
||||
* @param array $where
|
||||
* @return void
|
||||
*/
|
||||
public function contentTableWhere( ?BaseItem $item, Content $table ) : array
|
||||
public function contentTableWhere( ?BaseItem $item, Content $table, array &$where ) : void
|
||||
{
|
||||
return [];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the table rows before output
|
||||
*
|
||||
* @note $item will always be null
|
||||
* @param BaseItem|null $item
|
||||
* @param Content $table
|
||||
* @param array $rows
|
||||
* @return void
|
||||
*/
|
||||
public function contentTableGetRows( ?BaseItem $item, Content $table, array &$rows ) : void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,18 @@ abstract class MenuExtension
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a total notification count that will be displayed
|
||||
* on the mobile menu. Previously was just a report count,
|
||||
* now this allows for other extensions.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function mobileNotificationCount() : int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to add additional content to userbar
|
||||
*
|
||||
@@ -71,4 +83,27 @@ abstract class MenuExtension
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add items to the Account Actions dropdown in the ACP member view
|
||||
*
|
||||
* @param MemberClass $member
|
||||
* @return array<MenuItem>
|
||||
*/
|
||||
public function acpAccountActionsMenu( MemberClass $member ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return additional buttons that will be shown on the member list
|
||||
* Buttons should be constructed as an array typically used in ACP tables
|
||||
*
|
||||
* @param MemberClass $member
|
||||
* @return array<string,array>
|
||||
*/
|
||||
public function acpRowButtons( MemberClass $member ) : array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace IPS\Output\UI;
|
||||
|
||||
|
||||
use IPS\Helpers\Form\FormAbstract;
|
||||
use IPS\Member\Club;
|
||||
use IPS\Node\Model;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
@@ -117,4 +118,45 @@ abstract class Node
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Add elements to the Club Node form
|
||||
* This method returns all elements that will be added.
|
||||
* By default, all elements will be added in order at the end of the form.
|
||||
* To specify placement within the form, @see FormAbstract::setPosition().
|
||||
*
|
||||
* @param Model $node
|
||||
* @param Club $club
|
||||
* @return array<string,FormAbstract>
|
||||
*/
|
||||
public function clubFormElements( Model $node, Club $club ): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Any formatting necessary for custom fields added in clubFormElements
|
||||
*
|
||||
* @param Model $node
|
||||
* @param Club $club
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function processClubForm( Model $node, Club $club, array $values ) : array
|
||||
{
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the club Node form is saved
|
||||
*
|
||||
* @param Model $node
|
||||
* @param Club $club
|
||||
* @param array $values
|
||||
* @return void
|
||||
*/
|
||||
public function clubFormPostSave( Model $node, Club $club, array $values ): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ class Bridge
|
||||
'coreAdminWebhooks' => true,
|
||||
'checkItemForSpam' => false,
|
||||
'checkCommentForSpam' => false,
|
||||
'featureIsEnabled' => false
|
||||
'featureIsEnabled' => false,
|
||||
'sendNotifications' => true,
|
||||
'sendUnapprovedNotification' => true
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -172,4 +174,15 @@ class Bridge
|
||||
{
|
||||
return $moderated;
|
||||
}
|
||||
|
||||
/**
|
||||
* File Handlers
|
||||
*
|
||||
* @param array $handlers Handlers
|
||||
* @return array
|
||||
*/
|
||||
public function fileHandlers( array $handlers ): array
|
||||
{
|
||||
return $handlers;
|
||||
}
|
||||
}
|
||||
+28
-15
@@ -15,6 +15,7 @@ namespace IPS;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\Events\Event;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Date;
|
||||
use IPS\Patterns\ActiveRecord;
|
||||
@@ -192,7 +193,12 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
*/
|
||||
public function get_choices(): array
|
||||
{
|
||||
return json_decode( $this->_data['choices'], TRUE );
|
||||
if( isset( $this->_data['choices'] ) and $choices = json_decode( $this->_data['choices'], true ) )
|
||||
{
|
||||
return $choices;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,6 +312,9 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
$this->poll_close_date = -1;
|
||||
}
|
||||
|
||||
/* Event */
|
||||
Event::fire( 'onCreateOrEdit', $this );
|
||||
|
||||
/* Set the number of voters */
|
||||
$this->recountVotes();
|
||||
}
|
||||
@@ -441,6 +450,9 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
$this->recountVotes();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/* Fire an event */
|
||||
Event::fire( 'onVote', $this, [ $vote ] );
|
||||
|
||||
/* Achievements */
|
||||
$member->achievementAction( 'core', 'VotePoll', $this );
|
||||
@@ -458,6 +470,9 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
{
|
||||
/* Reset the cached count */
|
||||
$this->votes = Db::i()->select( 'COUNT(*)', 'core_voters', array( 'poll=?', $this->pid ) )->first();
|
||||
|
||||
/* Event */
|
||||
Event::fire( 'onVoteRecount', $this );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -586,25 +601,20 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
}
|
||||
elseif( Member::loggedIn()->group['g_poll_results'] == 1 )
|
||||
{
|
||||
if ( isset( Request::i()->nullVote ) and Member::loggedIn()->member_id )
|
||||
{
|
||||
if ( ! $this->getVote() )
|
||||
{
|
||||
Session::i()->csrfCheck();
|
||||
$this->addVote( Vote::fromForm( NULL ) );
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
elseif( Settings::i()->allow_result_view )
|
||||
/* If we already voted, let it through */
|
||||
if( $this->getVote() )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
/* If we are submitting a null vote, add it and then let it through */
|
||||
if ( isset( Request::i()->nullVote ) and Member::loggedIn()->member_id )
|
||||
{
|
||||
/* If the setting is disabled, check if we have voted */
|
||||
return (bool) $this->getVote();
|
||||
Session::i()->csrfCheck();
|
||||
$this->addVote( Vote::fromForm( NULL ) );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
@@ -762,5 +772,8 @@ class Poll extends ActiveRecord implements SplSubject
|
||||
|
||||
/* Delete records from voters table */
|
||||
Db::i()->delete( 'core_voters', array( 'poll=?', $this->pid ) );
|
||||
|
||||
/* Event */
|
||||
Event::fire( 'onDelete', $this );
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class ConverterParser extends Parser
|
||||
/**
|
||||
* @brief If parsing BBCode, the supported BBCode tags
|
||||
*/
|
||||
protected ?array $bbcode = NULL;
|
||||
public ?array $bbcodes = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -89,7 +89,7 @@ class ConverterParser extends Parser
|
||||
/* Get available BBCodes */
|
||||
if ( $bbcode )
|
||||
{
|
||||
$this->bbcode = static::bbcodeTags( $this->member, $this->area );
|
||||
$this->bbcodes = static::bbcodeTags( $this->member, $this->area );
|
||||
}
|
||||
|
||||
/* Get attachments */
|
||||
@@ -173,11 +173,11 @@ class ConverterParser extends Parser
|
||||
|
||||
if( $enabled )
|
||||
{
|
||||
$this->bbcode = static::bbcodeTags( $this->member, $this->area );
|
||||
$this->bbcodes = static::bbcodeTags( $this->member, $this->area );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->bbcode = NULL;
|
||||
$this->bbcodes = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,10 +234,10 @@ class ConverterParser extends Parser
|
||||
/* This fix resolves an issue using <br> mode where BBCode tags are wrapped in P tags like so: <p>[tag]</p><p>Content</p><p>[/tag]</p> tags.
|
||||
The fix just removes the </p><p> tags inside block BBCode tags, so our example ends up parsing like so: <p>[tag]<br><br>Content<br><br>[/tag]</p>
|
||||
We will want to find a more elegant fix for this at some point */
|
||||
if ( $this->bbcode !== NULL and ! Settings::i()->editor_paragraph_padding )
|
||||
if ( $this->bbcodes !== NULL and ! Settings::i()->editor_paragraph_padding )
|
||||
{
|
||||
$blockTags = array();
|
||||
foreach( $this->bbcode as $tag => $data )
|
||||
foreach( $this->bbcodes as $tag => $data )
|
||||
{
|
||||
if ( ! empty( $data['block'] ) )
|
||||
{
|
||||
@@ -288,7 +288,7 @@ class ConverterParser extends Parser
|
||||
$document = $parser->parseValueIntoDocument( $value );
|
||||
|
||||
/* [page] tags need to be handled specially */
|
||||
if ( $this->bbcode !== NULL and $this->containsPageTags )
|
||||
if ( $this->bbcodes !== NULL and $this->containsPageTags )
|
||||
{
|
||||
$body = DOMParser::getDocumentBody( $document );
|
||||
|
||||
@@ -443,10 +443,10 @@ class ConverterParser extends Parser
|
||||
}
|
||||
|
||||
/* If we are parsing BBCode, we will look for opening (e.g. "[foo=bar]") and closing (e.g. "[/foo]") tags */
|
||||
if ( $this->bbcode !== NULL and count( $this->bbcode ) )
|
||||
if ( $this->bbcodes !== NULL and count( $this->bbcodes ) )
|
||||
{
|
||||
/* First, if we have any single-tag BBCodes (e.g. "[img=URL]") expressed as normal BBCodes (e.g. "[img]URL[/img]") - fix that */
|
||||
foreach ( $this->bbcode as $tag => $bbcode )
|
||||
foreach ( $this->bbcodes as $tag => $bbcode )
|
||||
{
|
||||
if ( isset( $bbcode['single'] ) and $bbcode['single'] )
|
||||
{
|
||||
@@ -462,7 +462,7 @@ class ConverterParser extends Parser
|
||||
}
|
||||
|
||||
/* And add our regex to the breakpoints */
|
||||
$breakPoints[] = '(\[\/?(?:' . implode( '|', array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->bbcode ) ) ) . ')(?:[=\s].+?)?\])';
|
||||
$breakPoints[] = '(\[\/?(?:' . implode( '|', array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->bbcodes ) ) ) . ')(?:[=\s].+?)?\])';
|
||||
}
|
||||
|
||||
/* If we have any acronyms, they also need to be breakpoints */
|
||||
@@ -522,10 +522,10 @@ class ConverterParser extends Parser
|
||||
|
||||
/* Start of BBCode tag? */
|
||||
if (
|
||||
$this->bbcode !== NULL and $this->bbcodeParse and // BBCode is enabled
|
||||
$this->bbcodes !== NULL and $this->bbcodeParse and // BBCode is enabled
|
||||
preg_match( '/^\[([a-z\*]+?)(?:([=\s])(.+?))?\]$/i', $section, $matches ) and // It looks like a BBCode tag
|
||||
array_key_exists( mb_strtolower( $matches[1] ), $this->bbcode ) and // The tag is in the list
|
||||
( !isset( $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] ) or $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] === TRUE or !isset( $matches[3] ) or !$matches[3] ) // If options aren't allowed for this tag, there isn't one
|
||||
array_key_exists( mb_strtolower( $matches[1] ), $this->bbcodes ) and // The tag is in the list
|
||||
( !isset( $this->bbcodes[ mb_strtolower( $matches[1] ) ]['allowOption'] ) or $this->bbcodes[ mb_strtolower( $matches[1] ) ]['allowOption'] === TRUE or !isset( $matches[3] ) or !$matches[3] ) // If options aren't allowed for this tag, there isn't one
|
||||
)
|
||||
{
|
||||
/* What was the option? */
|
||||
@@ -689,7 +689,7 @@ class ConverterParser extends Parser
|
||||
protected function _openBBCode( string $tag, ?string $option, DOMNode &$parent, int $sectionId, int $sectionCount ) : void
|
||||
{
|
||||
/* Get definition */
|
||||
$bbcode = $this->bbcode[ $tag ];
|
||||
$bbcode = $this->bbcodes[ $tag ];
|
||||
|
||||
/* Get the document */
|
||||
$document = $parent->ownerDocument ?: $parent;
|
||||
@@ -744,7 +744,7 @@ class ConverterParser extends Parser
|
||||
|
||||
if ( $bbcodeElement instanceof DOMElement )
|
||||
{
|
||||
if ( $lastOpenedBlockId and list( $id, $tagName ) = explode( '-', $lastOpenedBlockId ) and isset( $this->bbcode[ $tagName ]['noChildren'] ) and $this->bbcode[ $tagName ]['noChildren'] )
|
||||
if ( $lastOpenedBlockId and list( $id, $tagName ) = explode( '-', $lastOpenedBlockId ) and isset( $this->bbcodes[ $tagName ]['noChildren'] ) and $this->bbcodes[ $tagName ]['noChildren'] )
|
||||
{
|
||||
$parent->appendChild( $bbcodeElement );
|
||||
}
|
||||
@@ -815,7 +815,7 @@ class ConverterParser extends Parser
|
||||
protected function _closeBBCode( string $tag, DOMNode &$parent, int $sectionId, int $sectionCount ) : void
|
||||
{
|
||||
/* Get definition */
|
||||
$bbcode = $this->bbcode[ $tag ];
|
||||
$bbcode = $this->bbcodes[ $tag ];
|
||||
|
||||
/* Block level? */
|
||||
if ( isset( $bbcode['block'] ) and $bbcode['block'] )
|
||||
|
||||
@@ -75,7 +75,7 @@ class LegacyParser
|
||||
/**
|
||||
* @brief BBcodes
|
||||
*/
|
||||
public static ?array $bbcodes = NULL;
|
||||
public ?array $bbcodes = NULL;
|
||||
|
||||
/**
|
||||
* @brief Emoticons
|
||||
@@ -187,7 +187,7 @@ class LegacyParser
|
||||
/* Grab legacy custom bbcodes */
|
||||
try
|
||||
{
|
||||
$this->bbcodes = iterator_to_array( Db::i()->select( '*', 'custom_bbcode' )->setKeyField( 'bbcode_tag' ) );
|
||||
$this->bbcodes = iterator_to_array( Db::i()->select( '*', 'convert_custom_bbcode' )->setKeyField( 'bbcode_tag' ) );
|
||||
}
|
||||
catch(Db\Exception $e )
|
||||
{
|
||||
@@ -484,7 +484,7 @@ class LegacyParser
|
||||
foreach( $this->parser->bbcodes as $code )
|
||||
{
|
||||
/* If we don't have a regex replacement, probably because we used to use a plugin, we can't do this automatically */
|
||||
if( !$code['bbcode_replace'] )
|
||||
if( !isset( $code['bbcode_replace'] ) or !$code['bbcode_replace'] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
+42
-3
@@ -56,6 +56,7 @@ use IPS\Xml\DOMDocument;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use UnexpectedValueException;
|
||||
use function array_key_exists;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function defined;
|
||||
@@ -641,6 +642,7 @@ class Parser
|
||||
$def->addAttribute( 'iframe', 'data-ipsembed-timestamp', 'Text' ); // used in embeddableMedia
|
||||
$def->addAttribute( 'iframe', 'data-internalembed', 'Text' ); // used in embeddableMedia
|
||||
$def->addAttribute( 'iframe', 'allowfullscreen', 'Text' ); // Some services will specify this property
|
||||
$def->addAttribute( 'iframe', 'allow', 'Text' ); // Some services will specify this property. We replace it with individual allow* attributes later on in the parsing process
|
||||
|
||||
/* Custom Iframes need these */
|
||||
$def->addAttribute( 'iframe', 'width', 'Number' ); // Some services will specify this property
|
||||
@@ -681,14 +683,14 @@ class Parser
|
||||
$def->addAttribute( 'iframe', 'data-munge-src', 'Text' );
|
||||
|
||||
/* Videos */
|
||||
$def->addElement( 'video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
|
||||
$def->addElement( 'video', 'Inline', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
|
||||
'controls' => 'Bool',
|
||||
'data-controller' => new HTMLPurifier_AttrDef_Enum( array( 'core.global.core.embeddedvideo' ) ),
|
||||
'data-video-embed' => 'Text',
|
||||
'preload' => new HTMLPurifier_AttrDef_Enum( [ 'auto', 'metadata', 'none' ] ),
|
||||
'style' => 'Text'
|
||||
) );
|
||||
$def->addElement( 'source', 'Block', 'Flow', 'Common', array(
|
||||
$def->addElement( 'source', 'Inline', 'Flow', 'Common', array(
|
||||
'src' => new HTMLPurifier_AttrDef_URI( TRUE ),
|
||||
'srcset' => new HtmlPurifierSrcsetDef( TRUE ),
|
||||
'media' => 'Text',
|
||||
@@ -697,7 +699,7 @@ class Parser
|
||||
) );
|
||||
|
||||
/* Audio */
|
||||
$def->addElement('audio', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
|
||||
$def->addElement('audio', 'Inline', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array(
|
||||
'controls' => 'Bool',
|
||||
'data-controller' => new HTMLPurifier_AttrDef_Enum( array( 'core.global.core.embeddedaudio' ) ),
|
||||
'src' => new HTMLPurifier_AttrDef_URI( TRUE ),
|
||||
@@ -1777,6 +1779,43 @@ class Parser
|
||||
}
|
||||
}
|
||||
catch ( Url\Exception ) {}
|
||||
|
||||
}
|
||||
|
||||
/* It is possible the iframe has an `allow` property. Let's make sure to split that up into individual attributes */
|
||||
if ( $element->hasAttribute( "allow" ) )
|
||||
{
|
||||
// todo support a system setting for this in v5.x
|
||||
$allowedAttrs = [
|
||||
"fullscreen" => "",
|
||||
"picture-in-picture" => "",
|
||||
'encrypted-media' => '',
|
||||
];
|
||||
|
||||
$foundAttrs = [];
|
||||
foreach ( explode( ";", $element->getAttribute( 'allow' ) ) as $permission )
|
||||
{
|
||||
$fixedPermission = mb_strtolower( trim( $permission ) );
|
||||
if ( array_key_exists( $fixedPermission, $allowedAttrs ) )
|
||||
{
|
||||
$foundAttrs[] = $fixedPermission;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $element->hasAttribute( 'allowfullscreen' ) )
|
||||
{
|
||||
$foundAttrs[] = "fullscreen";
|
||||
$element->removeAttribute( "allowfullscreen" );
|
||||
}
|
||||
|
||||
if ( count( $foundAttrs ) )
|
||||
{
|
||||
$element->setAttribute( "allow", implode( "; ", array_unique( $foundAttrs ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$element->removeAttribute( "allow" );
|
||||
}
|
||||
}
|
||||
|
||||
/* Native Lazyload all iFrames */
|
||||
|
||||
+333
-283
File diff suppressed because it is too large.
Load diff
@@ -362,19 +362,13 @@ abstract class Widget
|
||||
|
||||
/**
|
||||
* Return any extra classes that should be added to the widget wrapper
|
||||
* Placeholder method in case an override is necessary for individual widgets.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getWrapperClasses() : array
|
||||
{
|
||||
$classes = [];
|
||||
|
||||
if( $this->isBuilderWidget() )
|
||||
{
|
||||
$classes[] = 'ipsWidget--transparent';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,11 +636,7 @@ abstract class Widget
|
||||
$css['padding'] = $config['padding_custom'][0] . 'px ' . $config['padding_custom'][1] . 'px ' . $config['padding_custom'][2] . 'px ' . $config['padding_custom'][3] . 'px';
|
||||
}
|
||||
|
||||
if ( isset( $config['background_custom'] ) and $config['background_custom'] == 'transparent' )
|
||||
{
|
||||
$css['background-color'] = 'transparent';
|
||||
}
|
||||
elseif ( isset( $config['background_custom'] ) and $config['background_custom'] == 'image' and ! empty( $config['background_custom_image'] ) )
|
||||
if ( isset( $config['background_custom'] ) and $config['background_custom'] == 'image' and ! empty( $config['background_custom_image'] ) )
|
||||
{
|
||||
$css['background-image'] = 'url("' . File::get( 'core_Attachment', $config['background_custom_image'] )->url . '")';
|
||||
$css['background-size'] = 'cover';
|
||||
@@ -1645,7 +1635,6 @@ abstract class Widget
|
||||
}
|
||||
|
||||
/* Box model */
|
||||
$form->add( new YesNo( 'widget_adv__border', $this->configuration['widget_adv__border'] ?? TRUE, FALSE ) );
|
||||
$form->add( new Select( 'widget_adv__padding', $this->configuration['widget_adv__padding'] ?? 'full', FALSE,
|
||||
array(
|
||||
'options' => array(
|
||||
@@ -1719,7 +1708,7 @@ abstract class Widget
|
||||
array(
|
||||
'options' => array(
|
||||
'inherit' => 'widget_adv__inherit',
|
||||
'transparent' => 'widget_adv__fontcolor_custom_transparent',
|
||||
//'transparent' => 'widget_adv__fontcolor_custom_transparent',
|
||||
'custom' => 'widget_adv__background_custom_color',
|
||||
'image' => 'widget_adv__background_custom_image',
|
||||
),
|
||||
@@ -1730,7 +1719,7 @@ abstract class Widget
|
||||
), NULL, NULL, NULL, 'background_selector' ) );
|
||||
|
||||
$form->add( new Upload( 'widget_adv__background_custom_image', isset( $this->configuration['widget_adv__background_custom_image'] ) ? File::get( 'core_Attachment', $this->configuration['widget_adv__background_custom_image'] ) : '', FALSE, array( 'storageExtension' => 'core_Attachment', 'allowStockPhotos' => TRUE, 'image' => true ), NULL, NULL, NULL, 'background_image' ) );
|
||||
$form->add( new Color( 'widget_adv__background_custom_image_overlay', $this->configuration['widget_adv__background_custom_image_overlay'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE ), NULL, NULL, NULL, 'background_overlay' ) );
|
||||
$form->add( new Color( 'widget_adv__background_custom_image_overlay', $this->configuration['widget_adv__background_custom_image_overlay'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE, 'allowNone' => true ), NULL, NULL, NULL, 'background_overlay' ) );
|
||||
|
||||
$form->add( new Color( 'widget_adv__background', $this->configuration['widget_adv__background'] ?? '', FALSE, array( 'swatches' => TRUE, 'rgba' => TRUE ), NULL, NULL, NULL, 'background_custom' ) );
|
||||
|
||||
|
||||
@@ -149,14 +149,14 @@ class SimpleXML extends SimpleXMLElement
|
||||
* Add Child
|
||||
* Modified to support passing other data types (including multi-dimensional arrays and other \IPS\XML\SimpleXML objects) as values
|
||||
*
|
||||
* @param string $qualifiedName Element Name
|
||||
* @param mixed $qualifiedName Element Name
|
||||
* @param mixed $value Value
|
||||
* @param string|null $namespace Namespace
|
||||
* @return SimpleXMLElement|null
|
||||
*@see <a href='http://www.php.net/manual/en/simplexmlelement.addchild.php'>SimpleXMLElement::addChild</a>
|
||||
*/
|
||||
public function addChild( string $qualifiedName, mixed $value=null, ?string $namespace=null ): null|static
|
||||
{
|
||||
public function addChild( mixed $qualifiedName, mixed $value=null, ?string $namespace=null ): null|static
|
||||
{
|
||||
/* Arrays are possibly numerically indexed, which XML won't have */
|
||||
if( gettype( $qualifiedName ) === 'integer' )
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user