Version 5.0.6

This commit is contained in:
Neo committed 2025-12-19 18:55:49 -08:00
1 parent a3a4a9ccbd
commit b89858b2ff
1214 files changed
+92828 -14859

No files matched your search

@@ -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
@@ -1 +1,4 @@
patreon: FlorentMorselli
# 315662YNVPFR37N8S30
@@ -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
File diff suppressed because it is too large. Load diff
+8
View File
@@ -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() ) );
}
+105 -17
View File
@@ -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;
}
}
}
+20 -9
View File
@@ -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 )
+9 -1
View File
@@ -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();
+10 -5
View File
@@ -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 ) { }
-8
View File
@@ -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 );
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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'] );
}
/**
+3 -3
View File
@@ -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;
}
+91 -72
View File
@@ -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;
}
}
+10 -6
View File
@@ -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 )
+3 -2
View File
@@ -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
+10 -4
View File
@@ -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 )
{
+7 -4
View File
@@ -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');
}
/**
+11 -1
View File
@@ -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;
}
/**
+1 -1
View File
@@ -657,7 +657,7 @@ trait Statistics
$return[ $k ]['commentUrl'] = (string) $url->url();
}
catch ( LogicException|BadMethodCallException $e ) { }
catch ( LogicException | OutOfRangeException $e ) { }
}
}
+66 -2
View File
@@ -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;
}
+5 -2
View File
@@ -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
{
+6 -2
View File
@@ -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,
+5 -1
View File
@@ -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
);
+5 -2
View File
@@ -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>";
}
/**
+1 -1
View File
@@ -689,7 +689,7 @@ class Admin extends Standard
*/
final public static function showSwitchLink(): bool
{
if ( CIC )
if ( ! CIC )
{
return false;
}
+4 -4
View File
@@ -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
View File
@@ -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() );
+5 -2
View File
@@ -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 */
+4
View File
@@ -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
);
}
+12 -1
View File
@@ -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;
}
+34 -1
View File
@@ -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();
}
}
+14
View File
@@ -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 [];
}
}
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 ){}
}
}
+14
View File
@@ -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();
}
+23 -2
View File
@@ -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;
+1 -1
View File
@@ -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
{
+1 -3
View File
@@ -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];
+160
View File
@@ -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 );
}
}
+30 -9
View File
@@ -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;
}
}
+4 -1
View File
@@ -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 );
+4
View File
@@ -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
View File
@@ -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 */
+3
View File
@@ -260,6 +260,9 @@ class Imagemagick extends Image
public function flip(): void
{
$this->imagick->flopImage();
/* Set width/height */
$this->setDimensions();
}
/**
+8 -4
View File
@@ -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
View File
@@ -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? */
+2 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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 );
}
+2 -2
View File
@@ -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;
}
+2 -2
View File
@@ -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 );
}
+2 -2
View File
@@ -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;
+8 -1
View File
@@ -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
View File
@@ -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();
}
}
+1
View File
@@ -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
)
)
);
+1 -15
View File
@@ -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 '';
}
);
}
+6 -6
View File
@@ -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 ) );
+22 -12
View File
@@ -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;
+28 -3
View File
@@ -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 )
+13 -2
View File
@@ -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 ] ) ) );
},
);
+185
View File
@@ -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
View File
@@ -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 )
+49 -1
View File
@@ -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
*
+4 -3
View File
@@ -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;
+11 -1
View File
@@ -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 )
{
+22 -8
View File
@@ -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
{
}
}
+21 -4
View File
@@ -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
{
}
}
+35
View File
@@ -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 [];
}
}
+42
View File
@@ -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
{
}
}
+14 -1
View File
@@ -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
View File
@@ -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 );
}
}
+16 -16
View File
@@ -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'] )
+3 -3
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+5 -16
View File
@@ -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' ) );
+3 -3
View File
@@ -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' )
{