Version 5.0.0 beta 1

This commit is contained in:
Neo committed 2025-12-19 16:27:35 -08:00
1 parent 25ddeb65d6
commit 15c7beabc5
6736 files changed
+627902 -497943

No files matched your search

+60 -43
View File
@@ -11,16 +11,33 @@
namespace IPS\Helpers\Form;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
use DateInterval;
use DomainException;
use Exception;
use InvalidArgumentException;
use IPS\Data\Cache;
use IPS\DateTime;
use IPS\File;
use IPS\Http\Url as HttpUrl;
use IPS\Image;
use IPS\Member;
use function defined;
use function in_array;
use function intval;
use function is_array;
use function strlen;
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
}
/**
* URL input class for Form Builder
*/
class _Url extends Text
class Url extends Text
{
/**
* @brief Default Options
@@ -37,7 +54,7 @@ class _Url extends Text
* @endcode
* @note You should NOT use file/image/allowedMimes options for public-facing forms. These options will result in the file being imported, and exposing such a form to end users opens up the community to potential SSRF concerns.
*/
public $childDefaultOptions = array(
public array $childDefaultOptions = array(
'allowedProtocols' => array( 'http', 'https' ),
'allowedMimes' => NULL,
'image' => FALSE,
@@ -50,12 +67,12 @@ class _Url extends Text
/**
* Validate
*
* @throws \InvalidArgumentException
* @throws \DomainException
* @throws InvalidArgumentException
* @throws DomainException
* @return TRUE
* @note Custom validation is performed AFTER all standard validations to ensure consistent error messages in places like importing profile photos (if an attacker was attempting to supply a local address). This means that if the form helper's 'file' childoption is set, the value supplied to the custom helper will be an instance of \IPS\File instead of \IPS\Http\Url. Also, a second param $response is made available if you need to check the value of the URL's response and file, allowedMimes, or image was set (preventing a second unnecessary HTTP request).
*/
public function validate()
public function validate(): bool
{
/* If we have a custom validation function, store it now and we'll run it manually afterwards */
$validationFunction = NULL;
@@ -72,32 +89,32 @@ class _Url extends Text
if ( $this->value )
{
$value = $this->formatValue();
/* Check the URL is valid */
if ( !( $value instanceof \IPS\Http\Url ) )
if ( !( $value instanceof HttpUrl ) )
{
throw new \InvalidArgumentException('form_url_bad');
throw new InvalidArgumentException('form_url_bad');
}
/* And that it's an allowed protocol */
if ( $this->options['allowedProtocols'] and !\in_array( mb_strtolower( $value->data['scheme'] ), $this->options['allowedProtocols'] ) )
if ( $this->options['allowedProtocols'] and !in_array( mb_strtolower( $value->data['scheme'] ), $this->options['allowedProtocols'] ) )
{
throw new \DomainException('form_url_bad_protocol');
throw new DomainException('form_url_bad_protocol');
}
/* Try to fetch it, if necessary */
if ( $this->options['file'] or $this->options['allowedMimes'] or $this->options['image'] )
{
/* Is rate limiting enabled (the default)? */
if( $this->options['rateLimit'] !== NULL AND \intval( $this->options['rateLimit'] ) > 0 )
if( $this->options['rateLimit'] !== NULL AND intval( $this->options['rateLimit'] ) > 0 )
{
if( $rateLimit = $this->getRateLimitValue() )
{
/* Was it less than 20 seconds ago? If so, make the user wait */
$timeLeft = $rateLimit - ( time() - \intval( $this->options['rateLimit'] ) );
$timeLeft = $rateLimit - ( time() - intval( $this->options['rateLimit'] ) );
if( $timeLeft > 0 )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_too_soon', FALSE, array( 'sprintf' => array( $this->options['rateLimit'], $timeLeft ) ) ) );
throw new DomainException( Member::loggedIn()->language()->addToStack( 'form_url_too_soon', FALSE, array( 'sprintf' => array( $this->options['rateLimit'], $timeLeft ) ) ) );
}
}
@@ -113,21 +130,21 @@ class _Url extends Text
{
if( $e->getMessage() === 'localhost_url_not_followed' )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'form_url_localhost', FALSE, array( 'sprintf' => array( \IPS\Http\Url::internal('')->data['host'] ) ) ) );
throw new DomainException( Member::loggedIn()->language()->addToStack( 'form_url_localhost', FALSE, array( 'sprintf' => array( HttpUrl::internal('')->data['host'] ) ) ) );
}
if( $e->getMessage() === 'protocol_not_followed' )
{
throw new \DomainException('form_url_bad_protocol');
throw new DomainException('form_url_bad_protocol');
}
throw new \DomainException( 'form_url_error' );
throw new DomainException( 'form_url_error' );
}
/* Check MIME */
if ( $this->options['allowedMimes'] or $this->options['image'] )
{
$allowedMimes = $this->options['allowedMimes'] ? ( \is_array( $this->options['allowedMimes'] ) ? $this->options['allowedMimes'] : array( $this->options['allowedMimes'] ) ): \IPS\Image::$imageMimes;
$allowedMimes = $this->options['allowedMimes'] ? ( is_array( $this->options['allowedMimes'] ) ? $this->options['allowedMimes'] : array( $this->options['allowedMimes'] ) ): Image::$imageMimes;
$match = FALSE;
$contentType = ( isset( $response->httpHeaders['Content-Type'] ) ) ? $response->httpHeaders['Content-Type'] : ( ( isset( $response->httpHeaders['content-type'] ) ) ? $response->httpHeaders['content-type'] : NULL );
@@ -145,7 +162,7 @@ class _Url extends Text
if ( !$match )
{
throw new \DomainException( 'form_url_bad_mime' );
throw new DomainException( 'form_url_bad_mime' );
}
}
@@ -154,10 +171,10 @@ class _Url extends Text
{
$maxFileSize = $this->options['maxFileSize'] * 1048576;
if( \strlen( $response ) > $maxFileSize )
if( strlen( $response ) > $maxFileSize )
{
unset( $response );
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'upload_too_big', TRUE, array( 'sprintf' => $this->options['maxFileSize'] ) ), 2 );
throw new DomainException( Member::loggedIn()->language()->addToStack( 'upload_too_big', TRUE, array( 'sprintf' => $this->options['maxFileSize'] ) ), 2 );
}
}
@@ -166,7 +183,7 @@ class _Url extends Text
{
try
{
$image = \IPS\Image::create( $response );
$image = Image::create( $response );
if ( $this->options['maxDimensions'] !== NULL )
{
@@ -174,11 +191,11 @@ class _Url extends Text
$response = (string) $image;
}
}
catch ( \Exception $e )
catch ( Exception $e )
{
if ( $this->options['image'] )
{
throw new \DomainException( 'form_url_bad_mime' );
throw new DomainException( 'form_url_bad_mime' );
}
}
}
@@ -190,11 +207,11 @@ class _Url extends Text
try
{
$this->value = \IPS\File::create( $this->options['file'], $filename, $response );
$this->value = File::create( $this->options['file'], $filename, $response );
}
catch( \InvalidArgumentException $e )
catch( InvalidArgumentException $e )
{
throw new \DomainException( 'form_url_error' );
throw new DomainException( 'form_url_error' );
}
}
}
@@ -214,22 +231,22 @@ class _Url extends Text
* Get the stored rate limit for this field and member
* We use sessions if we're a member to avoid adding data to cache_store
*
* @return int
* @return int|null
*/
protected function getRateLimitValue()
protected function getRateLimitValue(): ?int
{
if ( \IPS\Member::loggedIn()->member_id )
if ( Member::loggedIn()->member_id )
{
return ( isset( $_SESSION[ 'url_fetch_' . $this->htmlId ] ) ? $_SESSION[ 'url_fetch_' . $this->htmlId ] : NULL );
return ( $_SESSION['url_fetch_' . $this->htmlId] ?? NULL );
}
else
{
$cached = NULL;
try
{
$cached = \IPS\Data\Cache::i()->getWithExpire( 'url_fetch_' . $this->htmlId . '-' . \IPS\Member::loggedIn()->ip_address, TRUE );
$cached = Cache::i()->getWithExpire( 'url_fetch_' . $this->htmlId . '-' . Member::loggedIn()->ip_address, TRUE );
}
catch( \Exception $ex ) { }
catch( Exception $ex ) { }
return $cached;
}
@@ -243,15 +260,15 @@ class _Url extends Text
*
* @return void
*/
protected function setRateLimitValue( $value )
protected function setRateLimitValue( mixed $value ) : void
{
if ( !\IPS\Member::loggedIn()->member_id )
if ( !Member::loggedIn()->member_id )
{
$_SESSION[ 'url_fetch_' . $this->htmlId ] = $value;
}
else
{
\IPS\Data\Cache::i()->storeWithExpire( 'url_fetch_' . $this->htmlId . '-' . \IPS\Member::loggedIn()->ip_address, $value, \IPS\DateTime::create()->add( new \DateInterval( 'PT60M' ) ), TRUE );
Cache::i()->storeWithExpire( 'url_fetch_' . $this->htmlId . '-' . Member::loggedIn()->ip_address, $value, DateTime::create()->add( new DateInterval( 'PT60M' ) ), TRUE );
}
}
@@ -260,7 +277,7 @@ class _Url extends Text
*
* @return string
*/
public function getValue()
public function getValue(): mixed
{
$val = str_replace( 'feed://', 'http://', parent::getValue() );
if ( $val and !mb_strpos( $val, '://' ) )
@@ -274,17 +291,17 @@ class _Url extends Text
/**
* Format Value
*
* @return \IPS\Http\Url|string
* @return Url|string
*/
public function formatValue()
public function formatValue(): mixed
{
if ( $this->value and !( $this->value instanceof \IPS\Http\Url ) )
if ( $this->value and !( $this->value instanceof Url ) )
{
try
{
return \IPS\Http\Url::createFromString( $this->value, TRUE, TRUE );
return HttpUrl::createFromString( $this->value, TRUE, TRUE );
}
catch ( \InvalidArgumentException $e )
catch ( InvalidArgumentException $e )
{
return $this->value;
}