Version 5.0.0 beta 4
This commit is contained in:
1 parent
e86153356a
commit
2f24f9de48
276 files changed
+8614
-6044
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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') )
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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 );
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 )
|
||||
|
||||
@@ -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
@@ -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 ) )
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' ) )
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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 );
|
||||
|
||||
|
||||
@@ -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'] ) )
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user