Version 5.0.0 beta 4

This commit is contained in:
Neo committed 2025-12-19 16:57:00 -08:00
1 parent e86153356a
commit 2f24f9de48
276 files changed
+8614 -6044

No files matched your search

+1 -1
View File
@@ -281,7 +281,7 @@ class Application extends Model
*/
public static function appIsEnabled( string $key ): bool
{
if ( Dispatcher::hasInstance() AND Dispatcher::i()->controllerLocation === 'setup' )
if ( Dispatcher::hasInstance() AND Dispatcher::i()->controllerLocation === 'setup' and Dispatcher::i()->setupLocation == 'install' )
{
return FALSE;
}
+1 -1
View File
@@ -165,7 +165,7 @@ class ItemController extends Controller
/* Sort */
$supportedSortBy = array( 'date', 'title' );
if( $class::$databaseColumnMap['updated'] )
if( isset($class::$databaseColumnMap['updated'] ) )
{
$supportedSortBy[] = 'updated';
}
+84 -27
View File
@@ -29,6 +29,7 @@ use IPS\DateTime;
use IPS\Db;
use IPS\Db\Exception;
use IPS\Db\Select;
use IPS\Dispatcher\Front;
use IPS\Events\Event;
use IPS\File;
use IPS\Helpers\Form\Captcha;
@@ -39,6 +40,7 @@ use IPS\IPS;
use IPS\Member;
use IPS\Node\Model;
use IPS\Notification;
use IPS\Output;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Platform\Bridge;
use IPS\Redis;
@@ -57,6 +59,7 @@ use function in_array;
use function intval;
use function is_array;
use function substr;
use function ucfirst;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -151,27 +154,31 @@ abstract class Comment extends Content
break;
case 'approved':
if ( $first ) // If this is the first post within an item, don't mark it hidden, otherwise the count of unapproved comments/items will include both the comment and the item when really only the item is hidden
if( IPS::classUsesTrait( $obj, Hideable::class ) )
{
$val = TRUE;
}
elseif ( $hiddenStatus === NULL )
{
$permissionCheckFunction = in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) ? 'canReview' : 'canComment';
if ( !$member->member_id and !$item->$permissionCheckFunction( $member, FALSE ) )
if ( $first ) // If this is the first post within an item, don't mark it hidden, otherwise the count of unapproved comments/items will include both the comment and the item when really only the item is hidden
{
$val = -3;
$val = TRUE;
}
elseif ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
elseif ( $hiddenStatus === NULL )
{
$val = $item->moderateNewReviews( $member ) ? 0 : 1;
}
else
{
$val = $item->moderateNewComments( $member ) ? 0 : 1;
$permissionCheckFunction = in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) ? 'canReview' : 'canComment';
if ( !$member->member_id and !$item->$permissionCheckFunction( $member, FALSE ) )
{
$val = -3;
}
elseif ( in_array( 'IPS\Content\Review', class_parents( get_called_class() ) ) )
{
$val = $item->moderateNewReviews( $member ) ? 0 : 1;
}
else
{
$val = $item->moderateNewComments( $member ) ? 0 : 1;
}
}
}
else
if( $val === null or !IPS::classUsesTrait( $obj, Hideable::class ) )
{
switch ( $hiddenStatus )
{
@@ -189,11 +196,15 @@ abstract class Comment extends Content
break;
case 'hidden':
if( !IPS::classUsesTrait( $obj, Hideable::class ) )
{
$val = $hiddenStatus;
}
if ( $first )
{
$val = FALSE; // If this is the first post within an item, don't mark it hidden, otherwise the count of unapproved comments/items will include both the comment and the item when really only the item is hidden
}
elseif ( $item->approvedButHidden() )
elseif ( IPS::classUsesTrait( $item, Hideable::class ) and $item->approvedButHidden() )
{
$val = 2;
}
@@ -602,6 +613,49 @@ abstract class Comment extends Content
) );
}
/**
* @var array Shareable URLs
*/
protected static $shareableUrls = array();
/**
* Get a shareable URL
*
* @return Url
*/
public function shareableUrl( string $type='comment' ): Url
{
$currentPage = Front::i()->currentPage ?? 1;
$idColumn = static::$databaseColumnId;
$item = $this->item();
$itemIdColumn = $item::$databaseColumnId;
/* Avoid building a single item's URL multiple times, such as when viewing a topic */
if ( isset( static::$shareableUrls[ $type ][ $item->$itemIdColumn ] ) )
{
$url = static::$shareableUrls[ $type ][ $item->$itemIdColumn ];
}
else
{
$url = $item->url()->setPage( 'page', $currentPage );
/* if we can do both comments and reviews, just return the findCommment url as we have tabs to navigate anyway */
if ( isset( $item::$reviewClass ) and isset( $item::$commentClass ) )
{
if ( method_exists( $item, 'commentReviewTabs' ) )
{
if ( count( $item->commentReviewTabs() ) > 1 )
{
$url = $url->setQueryString( ['tab' => $type . 's'] );
}
}
}
static::$shareableUrls[ $type ][ $item->$itemIdColumn ] = $url;
}
return $url->setFragment( 'find' . ucfirst( $type ) . '-' . $this->$idColumn );
}
/**
* Get containing item
*
@@ -1868,28 +1922,31 @@ abstract class Comment extends Content
}
}
if( $this->mapped('first') )
if ( ! Output::i()->reduceLinks() )
{
$share = new ContentMenuLink( url: $this->item()->url('share' ), languageString: Member::loggedIn()->language()->addToStack( $contentType === 'Comment' ? 'share_this_post' : 'share_this_review' ) );
}
else
{
if ( $contentType === 'Comment' )
if ( $this->mapped( 'first' ) )
{
$share = new ContentMenuLink( url: $this->item()->url( )->setQueryString( array( 'do' => 'findComment', 'comment' => $this->$idField )), languageString: Member::loggedIn()->language()->addToStack( 'share_this_post' ) );
$share = new ContentMenuLink( url: $this->item()->url( 'share' ), languageString: Member::loggedIn()->language()->addToStack( $contentType === 'Comment' ? 'share_this_post' : 'share_this_review' ) );
}
else
{
$share = new ContentMenuLink( url: $this->item()->url( )->setQueryString( array( 'do' => 'findReview', 'review' => $this->$idField )), languageString: Member::loggedIn()->language()->addToStack( 'share_this_review' ) );
if ( $contentType === 'Comment' )
{
$share = new ContentMenuLink( url: $this->shareableUrl(), languageString: Member::loggedIn()->language()->addToStack( 'share_this_post' ) );
}
else
{
$share = new ContentMenuLink( url: $this->shareableUrl('review'), languageString: Member::loggedIn()->language()->addToStack( 'share_this_review' ) );
}
}
}
$share->opensDialog( title: Member::loggedIn()->language()->addToStack('share'), size: 'narrow', contentSelector: '#elShare' . ucfirst( static::$moderationMenuIdPrefix ) . $this->$idField . '_menu' )
$share->opensDialog( title: Member::loggedIn()->language()->addToStack('share'), size: 'narrow', contentSelector: '#elShare' . ucfirst( static::$moderationMenuIdPrefix ) . $this->$idField . '_menu' )
->addAttribute( 'data-role', 'share' . $contentType )
->addAttribute( 'id', 'elShare' . ucfirst( static::$moderationMenuIdPrefix ) . $this->$idField )
->addAttribute( 'rel', 'nofollow' );
$menu->add( $share );
$menu->add( $share );
}
if( method_exists( $this, 'canRecognize') )
{
+12 -4
View File
@@ -879,11 +879,12 @@ abstract class Content extends ActiveRecord
*
* @param string $reportContent Report content message from member
* @param int $reportType Report type (see constants in \IPS\core\Reports\Report)
* @param Member|null $member Member making the report (or NULL for loggedIn())
* @param Member|null $member Member making the report (or NULL for loggedIn())
* @param array $guestDetails Details for a guest report
* @return Report
* @throws UnexpectedValueException If there is a permission error - you should only call this method after checking canReport
*/
public function report( string $reportContent, int $reportType=1, Member|null $member=null ): Report
public function report( string $reportContent, int $reportType=1, Member|null $member=null, array $guestDetails=array() ): Report
{
$member = ( $member ) ?: Member::loggedIn();
@@ -936,9 +937,16 @@ abstract class Content extends ActiveRecord
'report_by' => (int) $member->member_id,
'date_reported' => time(),
'ip_address' => Request::i()->ipAddress(),
'report_type' => $member->member_id ? $reportType : 0
'report_type' => ( ! $member->member_id and Settings::i()->automoderation_enabled ) ? 0 : $reportType
);
if ( count( $guestDetails ) and isset( $guestDetails['email'] ) )
{
/* Guest name can be optional */
$reportInsert['guest_name'] = isset( $guestDetails['name'] ) ? $guestDetails['name'] : '';
$reportInsert['guest_email'] = $guestDetails['email'];
}
$insertID = Db::i()->insert( 'core_rc_reports', $reportInsert );
$reportInsert['id'] = $insertID;
+71 -14
View File
@@ -19,8 +19,11 @@ use ErrorException;
use Exception;
use InvalidArgumentException;
use IPS\Api\Webhook;
use IPS\cloud\Application as CloudApplication;
use IPS\Content\Search\SearchContent;
use IPS\core\Feature;
use IPS\Helpers\Form\Email;
use IPS\Helpers\Form\Select;
use IPS\Helpers\Form\TextArea;
use IPS\Helpers\Form\Upload;
use IPS\Helpers\Form\Url as FormUrl;
@@ -398,7 +401,12 @@ class Controller extends CoverPhotoController
}
Output::i()->sidebar['contextual'] .= $item->ui( 'sidebar' );
if( Application::appIsEnabled( 'cloud' ) )
{
Output::setCacheTime( CloudApplication::getCacheDateForPage( $item->mapped('updated'), ( ( $item::$application === 'forums' and $item->isArchived() ) ? 'archived' : 'content' ) ) );
}
/* Return */
return $item;
}
@@ -1470,7 +1478,26 @@ class Controller extends CoverPhotoController
}
if ( $values = $form->values() )
{
$report = $item->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0 );
$guestDetails = [];
if ( ! Member::loggedIn()->member_id and Settings::i()->report_capture_guest_details )
{
$guestDetails = [ 'name' => $values['report_guest_name'], 'email' => $values['report_guest_email'] ];
/* Has this guest already reported this? */
try
{
$index = Db::i()->select( '*', 'core_rc_index', [ 'class=? and content_id=?', \get_class( $item ), $item->$idColumn ] )->first();
$report = Db::i()->select( '*', 'core_rc_reports', [ 'rid=? and guest_email=?', $index['id'], $values['report_guest_email'] ] )->first();
Output::i()->error( 'report_err_already_reported', '2S136/11', 403, '' );
}
catch( \UnderflowException )
{
/* The query failed, so this is a new report */
}
}
$report = $item->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0, null, $guestDetails );
File::claimAttachments( "report-{$class::$application}-{$class::$module}-{$item->$idColumn}", $report->id );
if ( Request::i()->isAjax() )
{
@@ -3337,7 +3364,7 @@ class Controller extends CoverPhotoController
}
/* Show form */
$form = new Form( NULL, 'report_submit' );
$form = new Form( 'report_submit', 'report_submit' );
$form->class = 'ipsForm--vertical ipsForm--report';
$itemIdColumn = $class::$databaseColumnId;
$idColumn = $comment::$databaseColumnId;
@@ -3355,17 +3382,28 @@ class Controller extends CoverPhotoController
return;
}
catch( Exception $e ) { }
$options = array( Report::TYPE_MESSAGE => Member::loggedIn()->language()->addToStack('report_message_comment') );
foreach( Types::roots() as $type )
{
$options[ $type->id ] = $type->_title;
}
$form->add( new Radio( 'report_type', NULL, FALSE, array( 'options' => $options ) ) );
}
$form->add( new Editor( 'report_message', NULL, FALSE, array( 'app' => 'core', 'key' => 'Reports', 'autoSaveKey' => "report-{$class::$application}-{$class::$module}-{$item->$itemIdColumn}-{$comment->$idColumn}", 'minimize' => 'report_message_placeholder' ) ) );
$options = array( Report::TYPE_MESSAGE => Member::loggedIn()->language()->addToStack('report_message_comment') );
foreach( Types::roots() as $type )
{
$options[ $type->id ] = $type->_title;
}
if ( count( $options ) > 1 )
{
$form->add( new Select( 'report_type', null, false, ['options' => $options] ) );
}
if ( ! Member::loggedIn()->member_id and Settings::i()->report_capture_guest_details )
{
/* Guest details */
$form->add( new Text( 'report_guest_name', null, Settings::i()->report_guest_details_name_mandatory ) );
$form->add( new Email( 'report_guest_email', null, true ) );
}
$form->add( new Editor( 'report_message', null, (bool) Settings::i()->report_content_mandatory, array( 'app' => 'core', 'key' => 'Reports', 'autoSaveKey' => "report-{$class::$application}-{$class::$module}-{$item->$itemIdColumn}-{$comment->$idColumn}", 'minimize' => Settings::i()->report_content_mandatory ? 'report_message_placeholder_mandatory' : 'report_message_placeholder' ) ) );
if ( !Request::i()->isAjax() )
{
Member::loggedIn()->language()->words['report_message'] = Member::loggedIn()->language()->addToStack('report_message_fallback');
@@ -3378,7 +3416,26 @@ class Controller extends CoverPhotoController
if ( $values = $form->values() )
{
$report = $comment->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0 );
$guestDetails = [];
if ( ! Member::loggedIn()->member_id and Settings::i()->report_capture_guest_details )
{
$guestDetails = [ 'name' => $values['report_guest_name'], 'email' => $values['report_guest_email'] ];
/* Has this guest already reported this? */
try
{
$index = Db::i()->select( '*', 'core_rc_index', [ 'class=? and content_id=?', \get_class( $comment ), $comment->$idColumn ] )->first();
$report = Db::i()->select( '*', 'core_rc_reports', [ 'rid=? and guest_email=?', $index['id'], $values['report_guest_email'] ] )->first();
Output::i()->error( 'report_err_already_reported', '2S136/11', 403, '' );
}
catch( \UnderflowException )
{
/* The query failed, so this is a new report */
}
}
$report = $comment->report( $values['report_message'], ( isset( $values['report_type'] ) ) ? $values['report_type'] : 0, null, $guestDetails );
File::claimAttachments( "report-{$class::$application}-{$class::$module}-{$item->$itemIdColumn}-{$comment->$idColumn}", $report->id );
if ( Request::i()->isAjax() )
{
+21 -2
View File
@@ -1401,7 +1401,26 @@ abstract class Item extends Content
}
throw new BadMethodCallException;
}
/**
* Get a shareable URL
*
* @param int|null $commentId
* @return Url
*/
public function shareableUrl( int $commentId = NULL ): Url
{
$idColumn = static::$databaseColumnId;
$url = $this->url();
if ( $commentId )
{
$url = $url->setFragment( 'findComment-' . $commentId );
}
return $url;
}
/**
* Get mapped value
*
@@ -7165,7 +7184,7 @@ abstract class Item extends Content
{
$lock->addAttribute( 'data-ipsDialog')
->addAttribute( 'data-ipsDialog-size', 'medium')
->addAttribute( 'data-ipsDialog-title', 'lock')
->addAttribute( 'data-ipsDialog-title', Member::loggedIn()->language()->addToStack( 'lock'))
->addAttribute( 'data-ipsDialog-destructOnClose', 'true');
}
$menu->add( $lock );
+1
View File
@@ -619,6 +619,7 @@ class Tag extends Model
$this->text = sprintf( Member::loggedIn()->language()->get( 'copy_tag_text' ), $this->text );
$this->text_seo = Friendly::seoTitle( $this->text );
$this->totals = null;
parent::__clone();
+1 -1
View File
@@ -707,7 +707,7 @@ abstract class CustomField extends Model
Lang::deleteCustom( 'core', static::$langKey . '_' . $this->id );
Lang::deleteCustom( 'core', static::$langKey . '_' . $this->id . '_desc' );
if ( isset( static::$contentDatabaseTable ) )
if ( isset( static::$contentDatabaseTable ) and static::$contentDatabaseTable )
{
if( $this->type == 'Upload' )
{
+11 -1
View File
@@ -2920,13 +2920,23 @@ class Db extends mysqli
{
$this->cachedTableData[ $tableName ] = array( 'rows' => 0, 'size' => 0 );
$this->cachedTableData[ $tableName ]['rows'] = $this->select( 'count(*)', $tableName )->first();
if ( ! \IPS\CIC )
{
/* Outside of Cloud we don't know if it's InnoDB, etc */
$this->cachedTableData[ $tableName ]['rows'] = $this->select( 'count(*)', $tableName )->first();
}
$result = $this->forceQuery("SHOW TABLE STATUS WHERE name LIKE '" . $this->prefix . $tableName . "'");
while( $data = $result->fetch_assoc() )
{
$this->cachedTableData[ $tableName ]['size'] = $data['Data_length'];
if ( \IPS\CIC )
{
/* Less accurate but much faster */
$this->cachedTableData[ $tableName ]['rows'] = $data['Rows'];
}
}
}
+29 -1
View File
@@ -86,6 +86,11 @@ class Front extends Standard
*/
protected ?string $_controller = NULL;
/**
* @var int|null $currentPage
*/
public int|null $currentPage = null;
/**
* Init
*
@@ -109,7 +114,7 @@ class Front extends Standard
header( "{$headerKey}: {$headerValue}" );
}
if ( CIC and ! Session::loggedIn() and ! Session::i()->userAgent->spider )
if ( CIC and ! Session::loggedIn() and ! Session::i()->userAgent->bot )
{
/* The software is unavailable, but the site is up so we do not want to affect our cloud downtime statistics and trigger monitoring alarms
if we are not a search engine */
@@ -140,6 +145,16 @@ class Front extends Standard
/* Base JS */
static::baseJs();
/* Get the current page, if any */
if ( isset( Request::i()->page ) )
{
$this->currentPage = Request::i()->page;
}
else if ( isset( Request::i()->url()->hiddenQueryString['page'] ) and $page = Request::i()->url()->hiddenQueryString['page'] )
{
$this->currentPage = $page;
}
/* Check friendly URL and whether it is correct */
try
{
@@ -159,6 +174,19 @@ class Front extends Standard
Output::i()->error( 'requested_route_404', '1S160/2', 404, '' );
}
/* Check if we're a low quality (no search engine bot) user */
if ( ! Member::loggedIn()->member_id and \IPS\Session\Front::i()->userAgent->isLowValue() )
{
/* We want to restrict the number of 301s low quality bots get */
foreach( [ 'findComment', 'findReview', 'getNewComment', 'getLastComment'] as $do )
{
if ( isset( Request::i()->do ) and Request::i()->do === $do ) // baby shark
{
unset( Request::i()->do );
}
}
}
/* Run global init */
try
{
+2 -1
View File
@@ -90,7 +90,7 @@ abstract class File
/**
* @brief File extensions considered safe. Not an exhaustive list but these are the files we're most interested in being recognised.
*/
public static array $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v', 'webp', 'm4a', 'wav' );
public static array $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v', 'webp', 'm4a', 'wav', 'avif' );
/**
* @brief File extensions for HTML5 compatible videos
@@ -1926,6 +1926,7 @@ abstract class File
'atx' => 'application/vnd.antix.game-component',
'au' => 'audio/basic',
'avi' => 'video/x-msvideo',
'avif' => 'image/avif',
'aw' => 'application/applixware',
'axd' => 'text/plain',
'azf' => 'application/vnd.airzip.filesecure.azf',
+3 -3
View File
@@ -342,14 +342,14 @@ class Form
* Add Message Row
*
* @param string $lang Language key or formatted string to display
* @param string $css Custom CSS class(es) to apply
* @param string|null $css Custom CSS class(es) to apply
* @param bool $parse Set this to false if the language string passed is already formatted for display
* @param string|null $_id HTML ID
* @param string|null $after The key of element to insert after
* @param string|null $tab The tab to insert onto
* @return void
*/
public function addMessage( string $lang, string $css='', bool $parse=TRUE, string $_id=NULL, string $after=NULL, string $tab=NULL ) : void
public function addMessage( string $lang, ?string $css='', bool $parse=TRUE, string $_id=NULL, string $after=NULL, string $tab=NULL ) : void
{
if ( !$_id )
{
@@ -361,7 +361,7 @@ class Form
}
/* Place the input into the correct position */
$this->_insert( Theme::i()->getTemplate( 'forms', 'core', 'global' )->message( $lang, $_id, $css, $parse ), NULL, $tab, $after );
$this->_insert( Theme::i()->getTemplate( 'forms', 'core', 'global' )->message( $lang, $_id, ( $css ?? '' ), $parse ), NULL, $tab, $after );
}
/**
@@ -160,7 +160,7 @@ class MultipleRedirect
public function __toString()
{
/* Don't cache for a short while to ensure sessions work */
Output::i()->pageCaching = FALSE;
Output::setCacheTime( false );
return $this->output ?: Theme::i()->getTemplate( 'global', 'core', 'global' )->multipleRedirect( $this->url, ( ( isset( Request::i()->mr_continue ) and isset( Request::i()->mr ) ) ? Request::i()->mr : 0 ), isset( Request::i()->_wizardHeight ) ? intval( Request::i()->_wizardHeight ) : NULL );
}
+1 -1
View File
@@ -133,7 +133,7 @@ class Wizard
public function __toString()
{
/* Don't cache for a short while to ensure sessions work */
Output::i()->pageCaching = FALSE;
Output::setCacheTime( false );
try
{
+12 -15
View File
@@ -159,21 +159,12 @@ class Url
/* If we don't have a base, assume the template location */
if ( !$base )
{
if ( Dispatcher::hasInstance() )
{
if ( Dispatcher::i()->controllerLocation === 'setup' )
{
return new static(( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . ( $_SERVER['QUERY_STRING'] ? rtrim( mb_substr( $_SERVER['REQUEST_URI'], 0, -mb_strlen( $_SERVER['QUERY_STRING'] ) ), '?' ) : $_SERVER['REQUEST_URI'] ) . '?' . $queryString);
}
else
{
$base = Dispatcher::i()->controllerLocation;
}
}
else
{
$base = 'front';
}
$base = Dispatcher::hasInstance() ? Dispatcher::i()->controllerLocation : 'front';
}
if( $base === 'setup' )
{
return new static(( Request::i()->isSecure() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . ( $_SERVER['QUERY_STRING'] ? rtrim( mb_substr( $_SERVER['REQUEST_URI'], 0, -mb_strlen( $_SERVER['QUERY_STRING'] ) ), '?' ) : $_SERVER['REQUEST_URI'] ) . '?' . $queryString);
}
/* Front-End Friendly */
@@ -322,6 +313,12 @@ class Url
$return = Url::createFromComponents( $components['host'], $components['scheme'], $components['path'], $components['query'], $components['port'], $components['user'], $components['pass'], $components['fragment'] );
}
/* If we are in setup, stop here */
if( Dispatcher::hasInstance() and Dispatcher::i()->controllerLocation == 'setup' )
{
return $return;
}
/* If the normal handling doesn't recognise it as an internal URL and we
have a gateway file, check that */
if ( Application::appIsEnabled( 'cms' ) and !( $return instanceof Internal ) and Settings::i()->cms_root_page_url )
+3
View File
@@ -391,6 +391,9 @@ class Friendly extends Internal
/* Remove pagination attributes */
$return = $return->setPage();
/* now remove any trailing / from the path */
$return = $return->setPath( rtrim( $return->data['path'], '/' ) );
/* Return */
return $return;
}
+75 -41
View File
@@ -29,11 +29,25 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
class Useragent
{
CONST TYPE_SEARCH = 1;
CONST TYPE_AI = 2;
CONST TYPE_CRAWLER = 3;
CONST TYPE_OTHER = 4;
/**
* @brief Search engine spider?
* @brief [Legacy] Search engine spider?
*/
public bool $spider = FALSE;
/**
* @brief Bot?
*/
public bool $bot = FALSE;
/**
* @brief Bot type
*/
public int $type = 0;
/**
* @brief Browser name
*/
@@ -106,24 +120,28 @@ class Useragent
$this->browserVersion = $data['version'];
/* Is this a spider? */
foreach( $this->searchEngineUseragents as $key => $regex )
foreach( $this->searchEngineUseragents as $key => $data )
{
if( is_array( $regex ) )
if( is_array( $data[1] ) )
{
foreach( $regex as $_expression )
foreach( $data[1] as $_expression )
{
if( preg_match( "#" . $_expression . "#im", $this->useragent, $matches ) )
{
$this->spider = $key;
$this->spider = $key; // keep this for older apps
$this->bot = $key;
$this->type = $data[0];
break 2;
}
}
}
else
{
if( preg_match( "#" . $regex . "#im", $this->useragent, $matches ) )
if( preg_match( "#" . $data[1] . "#im", $this->useragent, $matches ) )
{
$this->spider = $key;
$this->spider = $key; // Keep this for older apps
$this->bot = $key;
$this->type = $data[0];
break;
}
}
@@ -138,37 +156,43 @@ class Useragent
* @note If the UA matches an entry in this array, $this->spider will be set to TRUE
*/
protected array $searchEngineUseragents = array(
'about' => "Libby[_/ ]([0-9.]{1,10})",
'adsense' => array( "Mediapartners-Google/([0-9.]{1,10})", "Mediapartners-Google" ),
'ahrefs' => "AhrefsBot",
'alexa' => "^ia_archive",
'altavista' => "Scooter[ /\-]*[a-z]*([0-9.]{1,10})",
'ask' => "Ask[ \-]?Jeeves",
'baidu' => array( "^baiduspider\-", "baiduspider[ /]([0-9.]{1,10})" ),
'bing' => array( "bingbot[ /]([0-9.]{1,10})", "msnbot(?:-media)?[ /]([0-9.]{1,10})" ),
'brandwatch' => "magpie-crawler",
'excite' => "Architext[ \-]?Spider",
'google' => array( "Googl(?:e|ebot)(?:-Image|-Video|-News)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Image|-Video|-News)?/?" ),
'googlemobile' => array( "Googl(?:e|ebot)(?:-Mobile)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Mobile)?/" ),
'facebook' => "facebookexternalhit/([0-9.]{1,10})",
'infoseek' => array( "SideWinder[ /]?([0-9a-z.]{1,10})", "Infoseek" ),
'inktomi' => "slurp@inktomi\.com",
'internetseer' => "^InternetSeer\.com",
'look' => "www\.look\.com",
'looksmart' => "looksmart-sv-fw",
'lycos' => "Lycos_Spider_",
'majestic' => "MJ12bot\/v([0-9.]{1,10})",
'msproxy' => "MSProxy[ /]([0-9.]{1,10})",
'webcrawl' => "webcrawl\.net",
'websense' => "(?:Sqworm|websense|Konqueror/3\.(?:0|1)(?:\-rc[1-6])?; i686 Linux; 2002[0-9]{4})",
'yahoo' => "Yahoo(?:.*?)(?:Slurp|FeedSeeker)",
'yandex' => "Yandex(?:[^\/]+?)\/([0-9.]{1,10})",
'seznam' => array( "SeznamBot[ /]([0-9.]{1,10})", "Seznam screenshot-generator ([0-9.]{1,10})" ),
'dotbot' => "DotBot[ /]([0-9.]{1,10})",
'sogou' => "Sogou web spider[ /]([0-9.]{1,10})",
'isetallabot' => "istellabot[ /][a-z]([0-9.]{1,10})",
'blexbot' => "BLEXBot[ /]([0-9.]{1,10})",
'semrush' => "SemrushBot/([0-9.]{1,10})"
'about' => [ self::TYPE_OTHER, "Libby[_/ ]([0-9.]{1,10})" ],
'adsense' => [ self::TYPE_OTHER, array( "Mediapartners-Google/([0-9.]{1,10})", "Mediapartners-Google" ) ],
'ahrefs' => [ self::TYPE_CRAWLER, "AhrefsBot" ],
'alexa' => [ self::TYPE_SEARCH, "^ia_archive" ],
'altavista' => [ self::TYPE_OTHER, "Scooter[ /\-]*[a-z]*([0-9.]{1,10})" ],
'amazon' => [ self::TYPE_AI, [ "developer\.amazon\.com", "amazonbot" ] ],
'ask' => [ self::TYPE_OTHER, "Ask[ \-]?Jeeves" ],
'baidu' => [ self::TYPE_OTHER, array( "^baiduspider\-", "baiduspider[ /]([0-9.]{1,10})" ) ],
'bing' => [ self::TYPE_SEARCH, array( "bingbot[ /]([0-9.]{1,10})", "msnbot(?:-media)?[ /]([0-9.]{1,10})" ) ],
'brandwatch' => [ self::TYPE_OTHER, "magpie-crawler" ],
'chatgpt' => [ self::TYPE_AI, [ "OAI-SearchBot/([0-9.]{1,10})", "ChatGPT-User/([0-9.]{1,10})", "GPTBot/([0-9.]{1,10})", "openai.com/"] ],
'commoncrawl' => [ self::TYPE_OTHER, "CCBot" ],
'excite' => [ self::TYPE_OTHER, "Architext[ \-]?Spider" ],
'google' => [ self::TYPE_SEARCH, array( "Googl(?:e|ebot)(?:-Image|-Video|-News)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Image|-Video|-News)?/?" ) ],
'googlemobile' => [ self::TYPE_SEARCH, array( "Googl(?:e|ebot)(?:-Mobile)?/([0-9.]{1,10})", "Googl(?:e|ebot)(?:-Mobile)?/" ) ],
'facebook' => [ self::TYPE_OTHER, "facebookexternalhit/([0-9.]{1,10})" ],
'infoseek' => [ self::TYPE_OTHER, array( "SideWinder[ /]?([0-9a-z.]{1,10})", "Infoseek" ) ],
'inktomi' => [ self::TYPE_OTHER, "slurp@inktomi\.com" ],
'internetseer' => [ self::TYPE_SEARCH, "^InternetSeer\.com" ],
'look' => [ self::TYPE_OTHER, "www\.look\.com" ],
'looksmart' => [ self::TYPE_OTHER, "looksmart-sv-fw" ],
'lycos' => [ self::TYPE_OTHER, "Lycos_Spider_" ],
'majestic' => [ self::TYPE_OTHER, "MJ12bot\/v([0-9.]{1,10})" ],
'meta' => [ self::TYPE_OTHER, [ "meta-externalagent/", "developers\.facebook\.com" ] ],
'msproxy' => [ self::TYPE_OTHER, "MSProxy[ /]([0-9.]{1,10})" ],
'perplexity' => [ self::TYPE_OTHER, "PerplexityBot" ],
'pinterest' => [ self::TYPE_OTHER, "Pinterestbot/([0-9.]{1,10})" ],
'webcrawl' => [ self::TYPE_CRAWLER, "webcrawl\.net" ],
'websense' => [ self::TYPE_OTHER, "(?:Sqworm|websense|Konqueror/3\.(?:0|1)(?:\-rc[1-6])?; i686 Linux; 2002[0-9]{4})" ],
'yahoo' => [ self::TYPE_SEARCH, "Yahoo(?:.*?)(?:Slurp|FeedSeeker)" ],
'yandex' => [ self::TYPE_OTHER, "Yandex(?:[^\/]+?)\/([0-9.]{1,10})" ],
'seznam' => [ self::TYPE_OTHER, array( "SeznamBot[ /]([0-9.]{1,10})", "Seznam screenshot-generator ([0-9.]{1,10})" ) ],
'dotbot' => [ self::TYPE_OTHER, "DotBot[ /]([0-9.]{1,10})" ],
'sogou' => [ self::TYPE_OTHER, "Sogou web spider[ /]([0-9.]{1,10})" ],
'isetallabot' => [ self::TYPE_OTHER, "istellabot[ /][a-z]([0-9.]{1,10})" ],
'blexbot' => [ self::TYPE_OTHER, "BLEXBot[ /]([0-9.]{1,10})" ],
'semrush' => [ self::TYPE_CRAWLER, "SemrushBot/([0-9.]{1,10})" ]
);
/**
@@ -180,7 +204,17 @@ class Useragent
{
return Member::loggedIn()->language()->addToStack( 'user_agent_parsed', FALSE, array( 'sprintf' => array( $this->browser, $this->browserVersion, $this->platform ) ) );
}
/**
* Is this a low value bot?
*
* @return bool
*/
public function isLowValue(): bool
{
return $this->bot === true and $this->type !== self::TYPE_SEARCH;
}
/**
* @brief List of Facebook IP addresses
* @see <a href='https://developers.facebook.com/docs/ApplicationSecurity/#facebook_scraper'>Facebook application security</a>
@@ -210,7 +244,7 @@ class Useragent
continue;
}
list( $net, $maskBits ) = explode( '/', $range );
[ $net, $maskBits ] = explode( '/', $range );
$net = $this->_convertCompressedIpv6ToBits( inet_pton( $net ) );
@@ -229,7 +263,7 @@ class Useragent
continue;
}
list( $net, $maskBits ) = explode( '/', $range );
[ $net, $maskBits ] = explode( '/', $range );
if( ( ip2long( $ip ) & ~( ( 1 << ( 32 - $maskBits ) ) - 1 ) ) == ip2long( $net ) )
{
+5
View File
@@ -165,6 +165,11 @@ class Gd extends Image
imagewebp( $this->image, NULL, $quality );
break;
case 'avif':
$quality = Settings::i()->image_jpg_quality ?: 85;
imageavif( $this->image, null, $quality );
break;
}
return ob_get_clean();
}
+1 -1
View File
@@ -70,7 +70,7 @@ class Imagemagick extends Image
$this->imagick = new Imagick( $this->tempFile );
/* Set quality (if image format is JPEG) */
if ( in_array( mb_strtolower( $this->imagick->getImageFormat() ), array( 'jpg', 'jpeg', 'webp' ) ) )
if ( in_array( mb_strtolower( $this->imagick->getImageFormat() ), array( 'jpg', 'jpeg', 'webp', 'avif' ) ) )
{
$this->imagick->setImageCompressionQuality( (int) Settings::i()->image_jpg_quality ?: 85 );
}
+7 -1
View File
@@ -60,6 +60,7 @@ use function in_array;
use function is_array;
use function is_int;
use function is_null;
use function is_numeric;
use function strlen;
use function substr;
@@ -3410,7 +3411,12 @@ abstract class Model extends ActiveRecord
{
$orderColumn = static::$databaseColumnOrder;
$order = Db::i()->select( array( "MAX( `" . static::$databasePrefix . static::$databaseColumnOrder . "` )" ), static::$databaseTable, array() )->first();
$this->$orderColumn = $order + 1;
/* Make sure this is numeric; we might be ordering by a string */
if( is_numeric( $order ) )
{
$this->$orderColumn = $order + 1;
}
}
if( IPS::classUsesTrait( $this, Icon::class ) and $oldIcon )
+145 -25
View File
@@ -291,6 +291,11 @@ class Output
* @brief Data which were loaded via the GraphQL framework, but which have to be immediately available, rather then via later AJAX requests
*/
public array $graphData = [];
/**
* @brief A custom cache DateTime object set to UTC
*/
protected static bool|null|DateTime $cacheDate = NULL;
/**
* Constructor
@@ -523,6 +528,17 @@ class Output
return $title;
}
/**
* Store any custom DateTime for cache headers
*
* @param DateTime|bool $date
* @return void
*/
public static function setCacheTime( DateTime|bool $date=false ): void
{
static::$cacheDate = $date;
}
/**
* Retrieve cache headers
*
@@ -532,10 +548,43 @@ class Output
*/
public static function getCacheHeaders( int $lastModified, int $cacheSeconds ): array
{
/* The default is null, so if it is to false from setCacheTime, we should not cache */
if ( static::$cacheDate === false )
{
return static::getNoCacheHeaders();
}
$expires = DateTime::ts( ( time() + $cacheSeconds ), TRUE );
if ( static::$cacheDate instanceof \DateTime )
{
$expires = static::$cacheDate;
$cacheSeconds = $expires->getTimestamp() - time();
}
else
{
/* Set the cache date to the expiration date in case anything intercepts the output process and checks the cacheDate */
static::$cacheDate = $expires;
}
if ( \IPS\CIC or \IPS\IN_DEV )
{
return array(
'Date' => DateTime::ts( time(), TRUE )->rfc1123(),
'Last-Modified' => DateTime::ts( $lastModified, TRUE )->rfc1123(),
'Expires' => $expires->rfc1123(),
'Cache-Control' => implode( ', ', [
'max-age=0', // No cache for the browser [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives]
'public', // Public cache
's-maxage=' . $cacheSeconds, // Cache for the CDN [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives]
'stale-if-error' // Allow the CDN to serve stale content if there is an error
] )
);
}
return array(
'Date' => DateTime::ts( time(), TRUE )->rfc1123(),
'Last-Modified' => DateTime::ts( $lastModified, TRUE )->rfc1123(),
'Expires' => DateTime::ts( ( time() + $cacheSeconds ), TRUE )->rfc1123(),
'Expires' => $expires->rfc1123(),
'Cache-Control' => 'no-cache="Set-Cookie", max-age=' . $cacheSeconds . ", public, s-maxage=" . $cacheSeconds . ", stale-while-revalidate, stale-if-error",
);
}
@@ -799,7 +848,7 @@ class Output
/* Log */
$level = intval( substr( $code, 0, 1 ) );
if( !Session::i()->userAgent->spider )
if( !Session::i()->userAgent->bot )
{
if( $code and Settings::i()->error_log_level and $level >= Settings::i()->error_log_level )
{
@@ -890,6 +939,79 @@ class Output
}
}
/**
* Can this output be cached by CDN?
*
* @param string $contentType
* @param bool $checkCachePageTimeout
* @return bool
*/
public function isCacheable( string $contentType='text/html', bool $checkCachePageTimeout = true ): bool
{
/* Have we got a value for the default cache timeout? */
if ( $checkCachePageTimeout and ! \IPS\CACHE_PAGE_TIMEOUT )
{
return false;
}
/* Have we set a per-page cache timeout of false, which means we don't want to cache? */
if ( static::$cacheDate === false )
{
return false;
}
else if ( static::$cacheDate instanceof DateTime )
{
/* Or have we set a specific cache date and it's in the past? */
$cacheSeconds = static::$cacheDate->getTimestamp() - time();
if ( $cacheSeconds <= 0 )
{
return false;
}
}
/* Are we logged in? */
if ( Member::loggedIn()->member_id )
{
return false;
}
/* Are we using the legacy pageCaching flag? */
if ( ! $this->pageCaching )
{
return false;
}
/* Do we have a noCache cookie set? */
if ( isset( Request::i()->cookie['noCache'] ) )
{
return false;
}
/* Do we have a CSRF key? */
if ( isset( Request::i()->csrfKey ) )
{
return false;
}
/* Check the request method */
if ( ! in_array( mb_strtolower( $_SERVER['REQUEST_METHOD'] ), [ 'get', 'head' ] ) )
{
return false;
}
if ( ! in_array( $contentType, ['text/html', 'text/xml', 'application/manifest+json' ] ) )
{
return false;
}
if ( Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) and Dispatcher::i()->controllerLocation !== 'front' )
{
return false;
}
return true;
}
/**
* @brief Flag to bypass CSRF IN_DEV check
*/
@@ -945,36 +1067,18 @@ class Output
$this->parseFileObjectUrls( $output );
}
/* Combine some common checks for re-use */
$canCache = function( $contentType, $obj ) {
if( $obj->pageCaching and
( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' OR mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'head' ) and // Is a HTTP GET/HEAD request (don't cache output for POSTs)
( $contentType == 'text/html' or $contentType == 'text/xml' or $contentType == 'application/manifest+json' ) and // Output is HTML, XML or [PWA] application manifest (don't cache JSON output, etc)
Dispatcher::hasInstance() and class_exists( 'IPS\Dispatcher', FALSE ) and Dispatcher::i()->controllerLocation === 'front' // Is a normal, front-end page (necessary to know if user is logged in)
)
{
return TRUE;
}
return FALSE;
};
if (
CACHE_PAGE_TIMEOUT and // Page caching is enabled
$cacheThisPage and // Some pages can specify not to be cached (for example, when displaying a cached page, you don't want it recached)
!isset( Request::i()->cookie['noCache'] ) and // A noCache cookie might get set to not cache a particular guest (for example, if they have added items to their cart in the store)
$canCache( $contentType, $this ) and // Common checks, see above
!isset( Request::i()->csrfKey ) and // CSRF key isn't present (which would be like a POST request)
!Member::loggedIn()->member_id // User is not logged in
) {
/* Can we cache this page to a CDN? */
if( $cacheThisPage and $this->isCacheable( $contentType ) )
{
/* Add caching headers */
if( !isset( $httpHeaders['Cache-Control'] ) )
{
$httpHeaders += Output::getCacheHeaders( time(), CACHE_PAGE_TIMEOUT);
}
}
/* Send no-cache headers if we got to this point without any cache-control headers being set, or page caching is forced off */
elseif( !isset( $httpHeaders['Cache-Control'] ) OR !$this->pageCaching )
elseif( !isset( $httpHeaders['Cache-Control'] ) )
{
/* Send no-cache headers if we got to this point without any cache-control headers being set, or page caching is forced off */
$httpHeaders += Output::getNoCacheHeaders();
}
@@ -1154,6 +1258,22 @@ class Output
exit;
}
/**
* Should we reduce the number of links in the HTML for SEO purposes?
*
* @return bool
*/
public function reduceLinks(): bool
{
if ( ! Member::loggedIn()->member_id and Settings::i()->seo_reduce_links )
{
return true;
}
return false;
}
/**
* Remove any comment tags that were inserted in the middle of an HTML tag
*
+1 -1
View File
@@ -565,7 +565,7 @@ class Request extends Singleton
{
$groupFloodSeconds = Member::loggedIn()->group['g_search_flood'];
if ( Session::i()->userAgent->spider )
if ( Session::i()->userAgent->bot )
{
/* Force a 30 second flood control so if guests have it switched off, or set very low, you do not get flooded by known bots */
$groupFloodSeconds = BOT_SEARCH_FLOOD_SECONDS;
+5 -5
View File
@@ -305,7 +305,7 @@ class Front extends Session
}
else
{
$type = $this->userAgent->spider ? static::LOGIN_TYPE_SPIDER : static::LOGIN_TYPE_GUEST;
$type = $this->userAgent->bot ? static::LOGIN_TYPE_SPIDER : static::LOGIN_TYPE_GUEST;
}
/* Set data */
@@ -327,7 +327,7 @@ class Front extends Session
'current_id' => ( Request::i()->isAjax() ) ? ( $session ? $session['current_id'] : NULL ) : intval( Request::i()->id ),
'uagent_key' => $this->userAgent->browser ?: '',
'uagent_version' => $this->userAgent->browserVersion ?: '',
'uagent_type' => $this->userAgent->spider ? 'search' : 'browser',
'uagent_type' => $this->userAgent->bot ? 'search' : 'browser',
'search_thread_id' => $session ? intval( $session['search_thread_id'] ) : 0,
'search_thread_time' => $session ? $session['search_thread_time'] : 0,
'data' => $session ? $session['data'] : '',
@@ -341,10 +341,10 @@ class Front extends Session
);
/* Is this a spider? */
if( $this->userAgent->spider )
if( $this->userAgent->bot )
{
/* Is this Facebook? Do we need to treat them as a user of a different group? */
if( $this->userAgent->spider == 'facebook' )
if( $this->userAgent->bot == 'facebook' )
{
if( Service::load( 'facebook', 'share_key' )->enabled )
{
@@ -425,7 +425,7 @@ class Front extends Session
$this->data['data'] = $data;
$this->setLocationData();
if ( $this->save === TRUE and ( !empty( Request::i()->cookie ) or $this->userAgent->spider or $this->member->member_id ) ) // If a guest and cookies are disabled we do not write to database to prevent duplicate sessions unless it's a search engine, which we deal with separately
if ( $this->save === TRUE and ( !empty( Request::i()->cookie ) or $this->userAgent->bot or $this->member->member_id ) ) // If a guest and cookies are disabled we do not write to database to prevent duplicate sessions unless it's a search engine, which we deal with separately
{
if( method_exists( '\IPS\Session\Store', 'updateSession' ) )
{
+1 -1
View File
@@ -64,7 +64,7 @@ class Database extends Store
$userAgent = Useragent::parse();
/* Spiders match by IP and useragent */
if ( $userAgent->spider )
if ( $userAgent->bot )
{
$session = Db::i()->select( '*', 'core_sessions', array( 'id=? OR ( ip_address=? AND browser=? )', $sessionId, Request::i()->ipAddress(), $_SERVER['HTTP_USER_AGENT'] ) )->first();
}
+9 -2
View File
@@ -26,6 +26,7 @@ use function is_array;
use function is_null;
use function strlen;
use function substr;
use function time;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -255,12 +256,18 @@ class Task extends ActiveRecord
public static function queued(): ?Task
{
$fifteenMinutesAgo = ( time() - 900 );
foreach (Db::i()->select( '*', 'core_tasks', array( 'next_run<? AND enabled=1', ( time() + 60 ) ), 'next_run ASC', NULL, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER ) as $task )
$enabledApps = [];
foreach( Application::enabledApplications() as $app )
{
$enabledApps[] = $app->directory;
}
foreach (Db::i()->select( '*', 'core_tasks', array( [ 'next_run<?', ( time() + 60 ) ], [ 'enabled=1' ], [ Db::i()->in( 'app', $enabledApps ) ] ), 'next_run ASC', NULL, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER ) as $task )
{
try
{
$task = static::constructFromData( $task );
if ( !$task->running or $task->next_run < $fifteenMinutesAgo )
{
if ( $task->running )
+9 -3
View File
@@ -3005,11 +3005,17 @@ class Parser
protected static function _internalEmbed( Url $url, Member $member=NULL ): ?string
{
/* If this URL has a #comment-123 fragment, change it to the findComment URL so the comment embeds rather than the item */
if ( isset( $url->data['fragment'] ) and mb_strstr( $url->data['fragment'], 'comment-' ) and empty( $url->queryString['do'] ) )
if ( isset( $url->data['fragment'] ) and mb_stristr( $url->data['fragment'], 'comment-' ) and empty( $url->queryString['do'] ) )
{
$url = $url->setQueryString( array( 'do' => 'findComment', 'comment' => str_replace( 'comment-', '', $url->data['fragment'] ) ) );
$url = $url->setQueryString( array( 'do' => 'findComment', 'comment' => preg_replace( '#(find)?comment-#i', '', $url->data['fragment'] ) ) );
}
/* And for reviews */
if ( isset( $url->data['fragment'] ) and mb_stristr( $url->data['fragment'], 'review-' ) and empty( $url->queryString['do'] ) )
{
$url = $url->setQueryString( array( 'do' => 'findReview', 'review' => preg_replace( '#(find)?review-#i', '', $url->data['fragment'] ) ) );
}
/* Get the "real" query string (whatever the query string is, plus what we can get from decoding the FURL) */
$qs = array_merge( $url->queryString, $url->hiddenQueryString );
+7
View File
@@ -673,6 +673,13 @@ class Area
return $this;
}
/* Can this widget be dropped into this area? */
/* @var Widget $widgetClass */
$widgetClass = 'IPS\\' . $block['app'] . '\\widgets\\' . $block['key'];
if( class_exists( $widgetClass ) and in_array( $this->id, $widgetClass::$disallowedAreas ) )
{
return $this;
}
/* Load configuration */
if ( empty( $block['configuration'] ) or !is_array( $block['configuration'] ) )
+10
View File
@@ -138,6 +138,11 @@ abstract class Widget
*/
public bool $soloWidget = false;
/**
* @brief If a widget should not be dropped into a particular area (e.g. a database widget in the header), list those areas here
*/
public static array $disallowedAreas = [];
/**
* These layouts are the default allowed "feed" layouts
*
@@ -411,6 +416,11 @@ abstract class Widget
$return['widget-paddingallowed'] = true;
}
if( count( static::$disallowedAreas ) )
{
$return['widget-disallowed-areas'] = implode( ",", static::$disallowedAreas );
}
return $return;
}