Version 5.0.0 beta 5

This commit is contained in:
Neo committed 2025-12-19 17:04:22 -08:00
1 parent 2f24f9de48
commit 57d9db6ce2
218 files changed
+5331 -7416

No files matched your search

+21 -10
View File
@@ -13,8 +13,10 @@ namespace IPS\Api;
/* To prevent PHP errors (extending class does not exist) revealing path */
use DirectoryIterator;
use ErrorException;
use IPS\Application;
use IPS\Http\Url;
use IPS\Log;
use IPS\Member;
use IPS\Output;
use IPS\Settings;
@@ -281,22 +283,31 @@ abstract class Controller
{
$controllerName = mb_substr( $file, 0, -4 );
$class = 'IPS\\' . $app->directory . '\\api\\' . $controllerName;
if( class_exists( $class ) )
{
$reflection = new ReflectionClass( $class );
foreach ( $reflection->getMethods() as $method )
{
if ( $method->getName() != 'execute' and !$method->isStatic() and $method->isPublic() and mb_substr( $method->getName(), 0, 1 ) != '_' )
{
$details = static::decodeDocblock( $method->getDocComment() );
if ( ( $type !== 'client' or !isset( $details['details']['apimemberonly'] ) ) and ( $type !== 'member' or !isset( $details['details']['apiclientonly'] ) ) )
try
{
if( class_exists( $class ) )
{
$reflection = new ReflectionClass( $class );
foreach ( $reflection->getMethods() as $method )
{
if ( $method->getName() != 'execute' and !$method->isStatic() and $method->isPublic() and mb_substr( $method->getName(), 0, 1 ) != '_' )
{
$return[ $app->directory . '/' . $controllerName . '/' . $method->getName() ] = $details;
$details = static::decodeDocblock( $method->getDocComment() );
if ( ( $type !== 'client' or !isset( $details['details']['apimemberonly'] ) ) and ( $type !== 'member' or !isset( $details['details']['apiclientonly'] ) ) )
{
$return[ $app->directory . '/' . $controllerName . '/' . $method->getName() ] = $details;
}
}
}
}
}
catch( ErrorException $e )
{
/* This can happen if we have legacy files floating around */
Log::debug( "BAD_API_CLASS", 'api' );
}
}
}
}
+3
View File
@@ -347,6 +347,9 @@ trait Assignable
*/
public static function assignmentFormField( Item $item, ?Model $container=null, ?string $fieldName=null ) : FormAbstract
{
/* Preload some language strings */
Member::loggedIn()->language()->get( ['assignment_team', 'assign_to'] );
$prefix = Member::loggedIn()->language()->get( 'assignment_team' );
$currentValue = null;
if( $currentAssignment = $item->assignment )
+10 -4
View File
@@ -2833,12 +2833,15 @@ abstract class Item extends Content
/* Work out the order */
if ( $order === NULL )
{
$dateColumn = static::$databaseColumnMap['date'];
if ( is_array( $dateColumn ) )
if( isset( static::$databaseColumnMap[ 'date' ] ) )
{
$dateColumn = array_pop( $dateColumn );
$dateColumn = static::$databaseColumnMap['date'];
if ( is_array( $dateColumn ) )
{
$dateColumn = array_pop( $dateColumn );
}
$order = static::$databaseTable . '.' . static::$databasePrefix . $dateColumn . ' DESC';
}
$order = static::$databaseTable . '.' . static::$databasePrefix . $dateColumn . ' DESC';
}
$containerWhere = array();
@@ -6340,6 +6343,9 @@ abstract class Item extends Content
{
$blurb = NULL;
/* Preload some language strings */
Member::loggedIn()->language()->get( ['comment_meta_date_weeks_later', 'comment_meta_date_months_later', 'comment_meta_date_years_later'] );
/* Years? */
if ( $nextDate->diff( $date )->y > 0 )
{
+2 -1
View File
@@ -575,8 +575,9 @@ trait Reactable
$this->hasReacted[ $member->member_id ] = Content\Reaction::load( $this->reputation[ $member->member_id ] );
}
}
else
elseif ( ! is_array( $this->reputation ) )
{
/* $this->reputation is not set, so we need to query the database */
$where = $this->getReactionWhereClause( NULL, FALSE );
$where[] = array( 'member_id=?', $member->member_id );
$this->hasReacted[ $member->member_id ] = Content\Reaction::load( Db::i()->select( 'reaction', 'core_reputation_index', $where )->first() );
+29 -16
View File
@@ -397,16 +397,7 @@ trait Statistics
return array();
}
$return = array();
foreach( new ActiveRecordIterator( Db::i()->select( '*', $commentClass::$databaseTable, array( Db::i()->in( $commentIdField, $postIds ) ), "FIND_IN_SET( {$commentIdField}, '" . implode( ",", $postIds ) . "' )", array( 0, $count ) ), $commentClass ) as $comment )
{
if ( $comment->canView() and $comment->mapped('item') == $this->$idField and isset( $counts[ $comment->$commentIdField ] ) )
{
$return[] = array( 'comment' => $comment, 'count' => $counts[ $comment->$commentIdField ] );
}
}
return $return;
return $this->getComments( $commentClass, $commentIdField, $idField, $postIds, $count, $counts );
}
/**
@@ -469,18 +460,40 @@ trait Statistics
return array();
}
$return = array();
foreach( new ActiveRecordIterator( Db::i()->select( '*', $commentClass::$databaseTable, array( Db::i()->in( $commentIdField, $postIds ) ), "FIND_IN_SET( {$commentIdField}, '" . implode( ",", $postIds ) . "' )", array( 0, $count ) ), $commentClass ) as $comment )
return $this->getComments( $commentClass, $commentIdField, $idField, $postIds, $count, $counts );
}
protected $commentCache = array();
/**
* Get the comments, storing them for later use in the same request
*
* @param string $commentClass
* @param string $commentIdField
* @param string $idField
* @param array $postIds
* @param int $count
* @param array $counts
* @return array
*/
protected function getComments( string $commentClass, string $commentIdField, string $idField, array $postIds, int $count, array $counts ): array
{
$key = md5( $commentClass . $commentIdField . $idField . implode( ',', $postIds ) . $count . json_encode( $counts ) );
if ( ! isset( $this->commentCache[ $key ] ) )
{
if ( $comment->canView() and $comment->mapped('item') == $this->$idField and isset( $counts[ $comment->$commentIdField ] ) )
$this->commentCache[ $key ] = [];
foreach ( new ActiveRecordIterator( Db::i()->select( '*', $commentClass::$databaseTable, [Db::i()->in( $commentIdField, $postIds )], "FIND_IN_SET( {$commentIdField}, '" . implode( ",", $postIds ) . "' )", [0, $count] ), $commentClass ) as $comment )
{
$return[] = array( 'comment' => $comment, 'count' => $counts[ $comment->$commentIdField ] );
if ( $comment->canView() and $comment->mapped( 'item' ) == $this->$idField and isset( $counts[$comment->$commentIdField] ) )
{
$this->commentCache[ $key ][] = ['comment' => $comment, 'count' => $counts[$comment->$commentIdField]];
}
}
}
return $return;
return $this->commentCache[ $key ];
}
/**
* Fetch the top 10 popular days for posts
*
+2 -2
View File
@@ -146,11 +146,11 @@ class Front extends Standard
static::baseJs();
/* Get the current page, if any */
if ( isset( Request::i()->page ) )
if ( isset( Request::i()->page ) and is_numeric( Request::i()->page ) )
{
$this->currentPage = Request::i()->page;
}
else if ( isset( Request::i()->url()->hiddenQueryString['page'] ) and $page = Request::i()->url()->hiddenQueryString['page'] )
else if ( isset( Request::i()->url()->hiddenQueryString['page'] ) and is_numeric( Request::i()->url()->hiddenQueryString['page'] ) and $page = Request::i()->url()->hiddenQueryString['page'] )
{
$this->currentPage = $page;
}
+8 -2
View File
@@ -103,7 +103,10 @@ class Event
static::$_listeners = [];
foreach( $allListeners[ $class ] as $listener )
{
static::$_listeners[ $class ][] = new $listener;
if( class_exists( $listener ) )
{
static::$_listeners[ $class ][] = new $listener;
}
}
}
@@ -115,7 +118,10 @@ class Event
{
foreach( $listners as $listener )
{
static::$_listeners[ $class ][] = new $listener;
if( class_exists( $listener ) )
{
static::$_listeners[ $class ][] = new $listener;
}
}
}
}
+3 -2
View File
@@ -14,7 +14,6 @@ namespace IPS\Extensions;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\core\Approval;
use RuntimeException;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -33,9 +32,11 @@ abstract class ApprovalReasonAbstract
/**
* Parse Reason
* Return an array with 2 elements: lang (the language string to use)
* and sprintf (an array of replacements)
*
* @param Approval $approval Approval row
* @return array<lang,sprintf>
* @return array
*/
abstract public function parseReason( Approval $approval ): array;
}
+2 -3
View File
@@ -15,7 +15,6 @@ namespace IPS\Extensions;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Member\Club;
use IPS\Node\Model;
use RuntimeException;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
@@ -28,8 +27,8 @@ abstract class ClubAbstract
/**
* Tabs
*
* @param \IPS\Member\Club $club The club
* @param \IPS\Node\Model $container Container
* @param Club $club The club
* @param Model|null $container Container
* @return array
*/
abstract public function tabs( Club $club, ?Model $container = NULL ): array;
+1 -1
View File
@@ -561,7 +561,7 @@ abstract class Dynamic extends Chart
if( !Request::i()->isAjax() OR ( Request::i()->tab AND !Request::i()->chartId ) )
{
return Theme::i()->getTemplate( 'global', 'core' )->tabs( $this->getChartTabs(), ( isset( Request::i()->chartId ) ) ? Request::i()->chartId : NULL, $chartOutput, $this->url, 'chartId', 'ipsTabs_small ipsTabs_contained' );
return Theme::i()->getTemplate( 'global', 'core' )->tabs( $this->getChartTabs(), ( isset( Request::i()->chartId ) ) ? Request::i()->chartId : NULL, $chartOutput, $this->url, 'chartId', 'ipsTabs--small' );
}
else
{
+3 -2
View File
@@ -457,13 +457,14 @@ class Upload extends FormAbstract
/* Set existing files */
if ( $this->options['temporary'] )
{
$fileSize = mb_strlen( file_get_contents( $file ) );
$existing[] = array(
'id' => $id,
'insertable' => (bool) $this->options['postKey'],
'hasThumb' => false,
'originalFileName' => $file,
'size' => Filesize::humanReadableFilesize( $file, FALSE, TRUE ),
'sizeRaw' => $file,
'size' => Filesize::humanReadableFilesize( $fileSize, FALSE, TRUE ),
'sizeRaw' => $fileSize,
'custom' => NULL
);
}
+1 -1
View File
@@ -862,7 +862,7 @@ class Url
}
/* Set a default user-agent (some services, e.g. spotify, block requests without one but it's good to do so anyway) */
$requestObj->setHeaders( array( 'User-Agent' => 'Invision Community 4' ) );
$requestObj->setHeaders( array( 'User-Agent' => 'Invision Community 5' ) );
/* Return */
return $requestObj;
+2 -2
View File
@@ -145,8 +145,8 @@ class Friendly extends Internal
{
[ $pagePath, $pageNumber ] = Page::getStrippedPagePath( $potentialFurl );
/* Is this from the static folders? */
if ( mb_substr( $pagePath, 0, 7 ) === 'static/' )
/* Is this from the static folders or upload folders? */
if ( mb_substr( $pagePath, 0, 7 ) === 'static/' or mb_substr( $pagePath, 0, 8 ) === 'uploads/' )
{
/* Yes, so just return to avoid looking it up in pages when it won't be there */
return $return;
+7 -1
View File
@@ -2126,7 +2126,7 @@ class Member extends ActiveRecord
*/
public function isFollowing( array $memberIds ): array
{
$memberIds = array_filter( $memberIds );
$memberIds = array_unique( array_filter( $memberIds ) );
if ( !$memberIds )
{
return array();
@@ -3431,6 +3431,12 @@ class Member extends ActiveRecord
}
$perms = $this->modPermissions();
if ( $perms === false )
{
return false;
}
foreach ( Content::routedClasses( $this, FALSE, TRUE ) as $_class )
{
if( isset( $perms[ "can_view_reports_{$_class::$title}" ] ) and $perms[ "can_view_reports_{$_class::$title}" ] )
+2 -2
View File
@@ -67,7 +67,7 @@ class UserMenu
foreach( Application::allExtensions( 'core', 'UserMenu', $member ) as $ext )
{
foreach( $ext->accountMenu( 'content' ) as $element )
foreach( $ext->accountMenu( 'settings' ) as $element )
{
$menu->add( $element );
}
@@ -102,7 +102,7 @@ class UserMenu
foreach( Application::allExtensions( 'core', 'UserMenu', $member ) as $ext )
{
foreach( $ext->accountMenu( 'settings' ) as $element )
foreach( $ext->accountMenu( 'content' ) as $element )
{
$menu->add( $element );
}
+54 -13
View File
@@ -12,6 +12,7 @@ namespace IPS\Output;
/* To prevent PHP errors (extending class does not exist) revealing path */
use IPS\Db;
use IPS\Dispatcher;
use IPS\Http\Url;
use IPS\Output;
@@ -176,6 +177,9 @@ class System extends Output
cursor: pointer;
color: blue;
}
tr.explain-table {
background-color: #f5f5f5;
}
</style>
<script>
function toggleRow(id) {
@@ -214,6 +218,36 @@ HTML;
foreach ($debug as $index => $data)
{
$unique = md5( $data['query'] . uniqid() );
$explain = null;
$explainHtml = '';
/* If this is a select query, we can get the explain */
if ( stripos( $data['query'], 'SELECT' ) !== false )
{
if( $result = Db::i()->query( "EXPLAIN " . $data['query'] ) )
{
$explain = $result->fetch_assoc();
}
}
if ( $explain )
{
$explainHtml = '<table class="explain"><tr>';
foreach( array_keys( $explain ) as $header )
{
$explainHtml .= "<th>{$header}</th>";
}
$explainHtml .= '</tr><tr>';
foreach( $explain as $key => $value )
{
$explainHtml .= "<td>{$value}</td>";
}
$explainHtml .= '</tr></table>';
}
$html .= <<<HTML
<tr>
<td data-query="{$unique}" colspan="3" class="query">{$data['query']}</td>
@@ -227,21 +261,28 @@ HTML;
$trace['line'] = $trace['line'] ?? '';
$html .= <<<HTML
<tr data-row="content-{$unique}" style="display: none" class="collapsible-content">
<td>
{$trace['function']}
</td>
<td>
{$trace['file']}
</td>
<td>
{$trace['line']}
</td>
<td>
</td>
</tr>
<td>
{$trace['function']}
</td>
<td>
{$trace['file']}
</td>
<td>
{$trace['line']}
</td>
<td></td>
</tr>
HTML;
}
$html .= <<<HTML
<tr data-row="content-{$unique}" style="display: none" class="collapsible-content explain-table">
<td colspan="4">
{$explainHtml}
</td>
</tr>
HTML;
}
$html .= <<<HTML
@@ -0,0 +1,45 @@
<?php
/**
* @brief A HTMLPurifier Attribute Definition used for attributes which must be internal URLs or Integers (such as data-fileid for attachments)
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 21 October 2024
*/
namespace IPS\Text;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* A HTMLPurifier Attribute Definition used for attributes which must be internal URLs or Integers (such as data-fileid for attachments)
*/
class HtmlPurifierIntOrInternalLink extends HtmlPurifierInternalLinkDef
{
/**
* Validate
*
* @param string $value
* @param \HTMLPurifier_Config $config
* @param \HTMLPurifier_Context $context
* @return bool|string
*/
public function validate( $value, $config, $context ): bool|string
{
$parentCheck = parent::validate( $value, $config, $context );
if( $parentCheck !== FALSE )
{
return $parentCheck;
}
$integer = new \HTMLPurifier_AttrDef_Integer( false );
return $integer->validate( $value, $config, $context );
}
}
+10 -2
View File
@@ -17,6 +17,7 @@ use HTMLPurifier_Config;
use HTMLPurifier_Context;
use IPS\File;
use IPS\Http\Url;
use IPS\Http\Url\Exception as UrlException;
use IPS\Http\Url\Friendly;
use IPS\Http\Url\Internal;
use IPS\Settings;
@@ -61,8 +62,15 @@ class HtmlPurifierInternalLinkDef extends HTMLPurifier_AttrDef_URI
public function validate($uri, $config, $context): bool|string
{
/* Create the URL */
$url = Url::createFromString( str_replace( array( '%7B___base_url___%7D/', '{___base_url___}/' ), Settings::i()->base_url, $uri ) );
try
{
$url = Url::createFromString( str_replace( array( '%7B___base_url___%7D/', '{___base_url___}/' ), Settings::i()->base_url, $uri ) );
}
catch( UrlException $e )
{
return FALSE;
}
/* If it's not internal, we can stop now */
if ( !( $url instanceof Internal ) )
{
+3 -3
View File
@@ -608,8 +608,8 @@ class Parser
$def->addAttribute( 'img', 'data-emoticon', 'Bool' ); // Identifies emoticons and stops lightbox running on them
/* Attachments (set by _parseAElement, _parseImgElement and "insert existing attachment") - Gallery/Downloads use the full URL rather than an ID, hence Text */
$def->addAttribute( 'a', 'data-fileid', 'Text' );
$def->addAttribute( 'img', 'data-fileid', 'Text' );
$def->addAttribute( 'a', 'data-fileid', new HtmlPurifierIntOrInternalLink() );
$def->addAttribute( 'img', 'data-fileid', new HtmlPurifierIntOrInternalLink() );
$def->addAttribute( 'a', 'data-fileext', 'Text' );
/* Existing media (inserted with data-extension by the JS so that _getFile is able to locate) */
@@ -632,7 +632,7 @@ class Parser
/* iFrames (used by embeddableMedia) */
$def->addAttribute( 'iframe', 'data-controller', new HTMLPurifier_AttrDef_Enum( array( 'core.front.core.autosizeiframe' ) ) ); // used in core/global/embed/iframe.phtml
$def->addAttribute( 'iframe', 'data-embedid', 'Text' ); // used in core/global/embed/iframe.phtml
$def->addAttribute( 'iframe', 'data-embedauthorid', 'Text' ); // used for embed notifications
$def->addAttribute( 'iframe', 'data-embedauthorid', 'Number' ); // used for embed notifications
$def->addAttribute( 'iframe', 'data-embedcontent', 'Text' ); // used in embeddableMedia
$def->addAttribute( 'iframe', 'data-ipsembed-contentapp', 'Text' ); // used in embeddableMedia
$def->addAttribute( 'iframe', 'data-ipsembed-contentid', 'Text' ); // used in embeddableMedia
+6
View File
@@ -138,6 +138,12 @@ abstract class Widget
*/
public bool $soloWidget = false;
/**
* @brief Set to false if this widget should be hidden from the block list
* in the Page Editor
*/
public static bool $showInBlockList = true;
/**
* @brief If a widget should not be dropped into a particular area (e.g. a database widget in the header), list those areas here
*/