Version 4.5.0

This commit is contained in:
Neo committed 2025-12-19 05:51:58 -08:00
1 parent 1a0c7fd3c2
commit f79dcf067a
3791 files changed
+248032 -76372

No files matched your search

+162 -46
View File
@@ -11,7 +11,7 @@
namespace IPS;
/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
exit;
@@ -22,10 +22,36 @@ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
*/
abstract class _File
{
/**
* Get available storage handlers
*
* @param array|NULL $current The file storage configuration being edited
* @return array
*/
public static function storageHandlers( $current )
{
$return = array();
if ( !\IPS\CIC OR ( $current and $current['method'] === 'FileSystem' ) )
{
$return['FileSystem'] = 'IPS\\File\\FileSystem';
}
$return['Amazon'] = 'IPS\\File\\Amazon';
$return['Database'] = 'IPS\\File\\Database';
if ( $current and $current['method'] === 'Ftp' )
{
$return['Ftp'] = 'IPS\\File\\Ftp';
}
return $return;
}
/**
* @brief File extensions considered safe. Not an exhaustive list but these are the files we're most interested in being recognised.
*/
public static $safeFileExtensions = array( 'js', 'css', 'txt', 'xml', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v' );
public static $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' );
/**
* @brief File extensions for HTML5 compatible videos
@@ -47,6 +73,11 @@ abstract class _File
*/
protected static $thumbnailDimensions = array();
/**
* @brief Cached filesize
*/
protected $_cachedFilesize = NULL;
/**
* @brief Ignore errors from uploaded files?
*/
@@ -63,14 +94,14 @@ abstract class _File
* @param string|int $storageExtension Storage extension or configuration ID
* @param bool $tryOldFirst Whether to try the old file storage config first or not
* @throws \RuntimeException
* @return \IPS\File
* @return static
*/
public static function getClass( $storageExtension, $tryOldFirst=FALSE )
{
static::getConfigurations();
$configurationId = NULL;
if ( is_int( $storageExtension ) )
if ( \is_int( $storageExtension ) )
{
$configurationId = $storageExtension;
}
@@ -78,13 +109,45 @@ abstract class _File
{
$settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
/* If we are IN_DEV, make sure this is a valid file storage extension */
if ( \IPS\IN_DEV and !isset( $settings[ "filestorage__{$storageExtension}" ] ) )
{
/* Quick sanity check in case this is a new extension that hasn't been set yet. */
$isValid = FALSE;
foreach( \IPS\Application::allExtensions( 'core', 'FileStorage', FALSE ) as $extension )
{
$extensionNamespace = explode( '\\', \get_class( $extension ) );
$extensionName = array_pop( $extensionNamespace );
if( $storageExtension == $extensionNamespace[1] . '_' . $extensionName )
{
$isValid = TRUE;
break;
}
}
if( !$isValid )
{
throw new \RuntimeException( 'NO_STORAGE_EXTENSION' );
}
}
/* If this storage extension hasn't been set yet, grab the first *valid* configuration and use that (and set it for future use) */
if ( !isset( $settings[ "filestorage__{$storageExtension}" ] ) )
{
foreach ( static::$storageConfigurations as $k => $data )
{
/* Test the storage config - the first one that works wins! */
$classname = 'IPS\File\\' . $data['method'];
$handlers = static::storageHandlers( $data );
/* Check the handler is still available */
if( !isset( $handlers[ $data['method'] ] ) )
{
continue;
}
$classname = $handlers[ $data['method'] ];
$class = new $classname( json_decode( $data['configuration'], TRUE ) );
$class->configurationId = $k;
@@ -112,7 +175,7 @@ abstract class _File
else
{
/* We have an array of IDs when a move is in progress, the first ID is the new storage method, the second ID is the old */
if ( is_array( $settings[ "filestorage__{$storageExtension}" ] ) )
if ( \is_array( $settings[ "filestorage__{$storageExtension}" ] ) )
{
/* Do we want to use the old storage config if available? */
if ( $tryOldFirst === TRUE )
@@ -154,8 +217,9 @@ abstract class _File
throw new \RuntimeException;
}
$classname = 'IPS\File\\' . static::$storageConfigurations[ $configurationId ]['method'];
$class = new $classname( json_decode( static::$storageConfigurations[ $configurationId ]['configuration'], TRUE ) );
$handlers = static::storageHandlers( static::$storageConfigurations[ $configurationId ] );
$classname = $handlers[ static::$storageConfigurations[ $configurationId ]['method'] ];
$class = new $classname( json_decode( static::$storageConfigurations[ $configurationId ]['configuration'], TRUE ) );
$class->configurationId = $configurationId;
return $class;
}
@@ -165,7 +229,7 @@ abstract class _File
*
* @return array
*/
public static function getConfigurations()
public static function getStore()
{
if ( static::$storageConfigurations === NULL )
{
@@ -175,19 +239,35 @@ abstract class _File
}
else
{
static::$storageConfigurations = iterator_to_array( \IPS\Db::i()->select( '*', 'core_file_storage' )->setKeyField('id') );
\IPS\Data\Store::i()->storageConfigurations = static::$storageConfigurations;
\IPS\Data\Store::i()->storageConfigurations = iterator_to_array( \IPS\Db::i()->select( '*', 'core_file_storage' )->setKeyField('id') );
static::$storageConfigurations = \IPS\Data\Store::i()->storageConfigurations;
}
}
return static::$storageConfigurations;
}
/**
* Load storage configurations
*
* @return array
* @deprecated
*/
public static function getConfigurations()
{
return static::getStore();
}
/**
* @brief Copy files instead of moving them
*/
public static $copyFiles = FALSE;
/**
* @brief Temporarily stored EXIF data for an image
*/
public $exifData = NULL;
/**
* Create File
*
@@ -211,7 +291,7 @@ abstract class _File
}
/* Init */
$class = static::getClass( $storageExtension );
$class = static::getClass( $storageExtension, TRUE );
if ( $container !== NULL )
{
$class->container = $container;
@@ -221,7 +301,7 @@ abstract class _File
/* Image-specific stuff */
$ext = mb_substr( $filename, ( mb_strrpos( $filename, '.' ) + 1 ) );
if( in_array( mb_strtolower( $ext ), \IPS\Image::$imageExtensions ) and ( !$isSafe or \IPS\Image::exifSupported() ) )
if( \in_array( mb_strtolower( $ext ), \IPS\Image::$imageExtensions ) and ( !$isSafe or \IPS\Image::exifSupported() ) )
{
/* Get contents */
if( $data === NULL AND $filePath !== NULL )
@@ -250,6 +330,9 @@ abstract class _File
if ( \IPS\Image::exifSupported() )
{
$image = $image ?: \IPS\Image::create( $data );
$class->exifData = $image->parseExif();
if ( $image->hasBeenRotated )
{
$data = (string) \IPS\Image::create( $data );
@@ -283,7 +366,7 @@ abstract class _File
* @param array|NULL $allowedFileTypes Array of allowed file extensions, or NULL to allow any extensions
* @param int|NULL $maxFileSize The maximum file size in MB, or NULL to allow any size
* @param int|NULL $totalMaxSize The maximum total size of all files in MB, or NULL for no limit
* @param int $flags \IPS\File::IGNORE_UPLOAD_ERRORS to skip over invalid files rather than throw exception
* @param int $flags `\IPS\File::IGNORE_UPLOAD_ERRORS` to skip over invalid files rather than throw exception
* @param array|NULL $callback Callback function to run against the file contents before creating the file (useful for resizing images, for instance)
* @param string|null $container Key to identify container for storage
* @param bool $obscure Controls if an md5 hash should be added to the filename
@@ -325,7 +408,7 @@ abstract class _File
$currentTotal += $file['size'];
if ( $currentTotal > ( $totalMaxSize * 1048576 ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('uploaderr_total_size', FALSE, array( 'sprintf' => array( $totalMaxSize ) ) ) );
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack('uploaderr_total_size', FALSE, array( 'sprintf' => array( \IPS\Output\Plugin\Filesize::humanReadableFilesize( $totalMaxSize * 1048576 ) ) ) ) );
}
}
@@ -348,7 +431,7 @@ abstract class _File
if( $callback !== NULL )
{
$contents = file_get_contents( $file['tmp_name'] );
$contents = call_user_func( $callback, $contents, $file['name'] );
$contents = $callback( $contents, $file['name'] );
$fileObjects[] = static::create( $storageLocation, $file['name'], $contents, $container, FALSE, NULL, $obscure );
}
@@ -402,14 +485,14 @@ abstract class _File
}
/* Do we have $_FILES['field'] = array( 'name' => ..., 'size' => ... ) */
if( isset( $file['name'] ) AND !is_array( $file['name'] ) )
if( isset( $file['name'] ) AND !\is_array( $file['name'] ) )
{
$files[] = $file;
}
/* Or do we have $_FILES['field'] = array( 'name' => array( 0 => ..., 1 => ... ), 'size' => array( 0 => ..., 1 => ... ) ) */
else
{
if( is_array( $file['name'] ) )
if( \is_array( $file['name'] ) )
{
foreach( $file as $fieldName => $fields )
{
@@ -491,19 +574,19 @@ abstract class _File
/* Check allowed types */
$ext = mb_substr( $file['name'], mb_strrpos( $file['name'], '.' ) + 1 );
if( $allowedFileTypes !== NULL and is_array( $allowedFileTypes ) and !empty( $allowedFileTypes ) )
if( $allowedFileTypes !== NULL and \is_array( $allowedFileTypes ) and !empty( $allowedFileTypes ) )
{
if( !in_array( mb_strtolower( $ext ), array_map( 'mb_strtolower', $allowedFileTypes ) ) )
if( !\in_array( mb_strtolower( $ext ), array_map( 'mb_strtolower', $allowedFileTypes ) ) )
{
throw new \DomainException( '-601', 3 );
}
}
/* If it's got an image extension, check it's actually a valid image */
if ( in_array( $ext, \IPS\Image::$imageExtensions ) AND !isset( $file['_skipUploadCheck'] ) )
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) AND !isset( $file['_skipUploadCheck'] ) )
{
$imageAttributes = getimagesize( $file['tmp_name'] );
if( !in_array( $imageAttributes[2], array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG ) ) )
if( !\in_array( $imageAttributes[2], array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG ) ) )
{
throw new \DomainException( 'upload_error', 4 );
}
@@ -517,12 +600,13 @@ abstract class _File
*
* @param string $storageExtension Storage extension
* @param string|\IPS\Http|Url $url URL to file
* @param int $cachedFilesize Pre-cache filesize (bytes)
* @return \IPS\File
* @throws \OutOfRangeException
*/
public static function get( $storageExtension, $url )
public static function get( $storageExtension, $url, int $cachedFilesize=NULL )
{
if( mb_strpos( rawurldecode( $url ), '../' ) !== FALSE )
if( mb_strpos( rawurldecode( $url ), '../' ) !== FALSE OR mb_strpos( rawurldecode( $url ), '..\\' ) !== FALSE )
{
throw new \OutOfRangeException( 'INVALID_PATH' );
}
@@ -530,6 +614,7 @@ abstract class _File
$class = static::getClass( $storageExtension, TRUE );
$class->storageExtension = $storageExtension;
$class->url = $url;
$class->_cachedFilesize = $cachedFilesize;
$class->load();
return $class;
}
@@ -556,7 +641,7 @@ abstract class _File
public static function moveCheck( $configuration, $oldConfiguration )
{
$needsMove = FALSE;
if ( count( array_merge( array_diff( $configuration, $oldConfiguration ), array_diff( $oldConfiguration, $configuration ) ) ) )
if ( \count( array_merge( array_diff( $configuration, $oldConfiguration ), array_diff( $oldConfiguration, $configuration ) ) ) )
{
$needsMove = TRUE;
}
@@ -593,6 +678,7 @@ abstract class _File
* This is primarily a utility function to convert old style full URLs (http://site.com/uploads/monthly_04_2015/file.txt) into the new style (monthly_04_2015/file.txt)
*
* @deprecated 4.1.0
* @param string $url The URL to repair
* @return string|boolean URL (string) if URL needed repairing, or FALSE if it did not.
*/
public static function repairUrl( $url )
@@ -667,12 +753,14 @@ abstract class _File
/**
* Constructor
*
* @param array $configuration Storage configuration
* @param array $configuration Storage configuration
* @param int $cachedFilesize Pre-cached filesize
* @return void
*/
public function __construct( $configuration )
public function __construct( $configuration, int $cachedFilesize=null )
{
$this->configuration = $configuration;
$this->_cachedFilesize = $cachedFilesize;
}
/**
@@ -742,7 +830,7 @@ abstract class _File
ob_end_clean();
}
if( $throttle === NULL AND $start === NULL AND function_exists('readfile') )
if( $throttle === NULL AND $start === NULL AND \function_exists('readfile') )
{
readfile( $file );
}
@@ -811,18 +899,25 @@ abstract class _File
/**
* Get filesize (in bytes)
*
* @return string
* @return string|bool
*/
public function filesize()
{
if( $this->_cachedFilesize !== NULL )
{
return $this->_cachedFilesize;
}
try
{
return \strlen( $this->contents() );
$this->_cachedFilesize = \strlen( $this->contents() );
}
catch( \RuntimeException $ex )
{
return FALSE;
}
$this->_cachedFilesize = FALSE;
}
return $this->_cachedFilesize;
}
/**
@@ -881,7 +976,7 @@ abstract class _File
protected function obscureFilename( $filename )
{
$ext = mb_substr( $filename, ( mb_strrpos( $filename, '.' ) + 1 ) );
$safe = in_array( mb_strtolower( $ext ), static::$safeFileExtensions );
$safe = \in_array( mb_strtolower( $ext ), static::$safeFileExtensions );
if ( ! $safe )
{
@@ -1040,6 +1135,7 @@ abstract class _File
/**
* Return the fully qualified URL
*
* @param string $url URL
* @return string
*/
public function fullyQualifiedUrl( $url )
@@ -1055,6 +1151,7 @@ abstract class _File
/**
* Encode the file name for the fully qualified URL
*
* @param string $filename Filename in the URL
* @return string
*/
public function encodeFileUrl( $filename )
@@ -1121,8 +1218,9 @@ abstract class _File
{
/* Load class */
static::getConfigurations();
$classname = '\IPS\File\\' . static::$storageConfigurations[ $storageConfiguration ]['method'];
$class = new $classname( json_decode( static::$storageConfigurations[ $storageConfiguration ]['configuration'], TRUE ) );
$handlers = static::storageHandlers( static::$storageConfigurations[ $storageConfiguration ] );
$classname = $handlers[ static::$storageConfigurations[ $storageConfiguration ]['method'] ];
$class = new $classname( json_decode( static::$storageConfigurations[ $storageConfiguration ]['configuration'], TRUE ) );
/* Store it there */
if ( $this->container !== NULL )
@@ -1174,8 +1272,8 @@ abstract class _File
/**
* Make into an attachment
*
* @param string $postKey Post key
* @param \IPS\Member|NULL Member who uploaded the attachment
* @param string $postKey Post key
* @param \IPS\Member|NULL $member Member who uploaded the attachment
* @return array
* @throws \DomainException
*/
@@ -1240,7 +1338,7 @@ abstract class _File
{
$ext = mb_substr( $this->originalFilename, mb_strrpos( $this->originalFilename, '.' ) + 1 );
if ( in_array( mb_strtolower( $ext ), \IPS\Image::$imageExtensions ) )
if ( \in_array( mb_strtolower( $ext ), \IPS\Image::$imageExtensions ) )
{
return TRUE;
}
@@ -1257,7 +1355,7 @@ abstract class _File
{
$ext = mb_substr( $this->originalFilename, mb_strrpos( $this->originalFilename, '.' ) + 1 );
if ( in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
if ( \in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
{
return TRUE;
}
@@ -1286,6 +1384,17 @@ abstract class _File
}
}
/**
* Generate a temporary download URL the user can be redirected to
*
* @param $validForSeconds int The number of seconds the link should be valid for
* @return \IPS\Http\Url
*/
public function generateTemporaryDownloadUrl( $validForSeconds = 1200 )
{
return NULL;
}
/**
* If the file is an image, get the dimensions
*
@@ -1318,7 +1427,7 @@ abstract class _File
* @note If you call this, it is your responsibility to call unclaimAttachments if/when the thing is deleted
*/
public static function claimAttachments( $autoSaveKey, $id1=NULL, $id2=NULL, $id3=NULL, $translatable=FALSE )
{
{
if ( $translatable )
{
foreach ( \IPS\Lang::languages() as $lang )
@@ -1421,7 +1530,7 @@ abstract class _File
$thumbnailName = $this->thumbnailName ?: mb_substr( $this->originalFilename, 0, mb_strrpos( $this->originalFilename, '.' ) ) . '.thumb' . mb_substr( $this->originalFilename, mb_strrpos( $this->originalFilename, '.' ) );
/* Create and return */
return \IPS\File::create( $storageExtension, $thumbnailName, $image, $this->thumbnailContainer, FALSE, NULL, $this->thumbnailName ? FALSE : TRUE );
return \IPS\File::create( $storageExtension, $thumbnailName, (string) $image, $this->thumbnailContainer, FALSE, NULL, $this->thumbnailName ? FALSE : TRUE );
}
/**
@@ -1435,6 +1544,11 @@ abstract class _File
*/
protected function log( $message, $action, $data=NULL, $type='error' )
{
if ( $this->configurationId === NULL )
{
return;
}
\IPS\Db::i()->insert( 'core_file_logs', array (
'log_action' => $action,
'log_type' => $type,
@@ -1445,7 +1559,7 @@ abstract class _File
'log_container' => $this->container,
'log_msg' => $message,
'log_date' => time(),
'log_data' => is_array( $data ) ? json_encode( $data ) : NULL
'log_data' => \is_array( $data ) ? json_encode( $data ) : NULL
) );
}
@@ -1511,8 +1625,9 @@ abstract class _File
}
/**
* Get mime type
* Get mime type based on file extension
*
* @param string $filename Filename (with extension)
* @return string
*/
public static function getMimeType( $filename )
@@ -1531,6 +1646,7 @@ abstract class _File
* Return a value pulled from php.ini in bytes
*
* @note This function is intended to normalize values for things like post_max_size which could be -1, 0, 8383900, 8M, 1G, etc.
* @param string|int $size The size we want to convert to bytes (see note)
* @return float
*/
public static function returnBytes( $size )
@@ -1546,7 +1662,7 @@ abstract class _File
$last = mb_strtolower( $size[ \strlen( $size ) - 1 ] );
/* Convert $size to a number */
$size = intval( preg_replace( '/[^0-9]/', '', $size ) );
$size = \intval( preg_replace( '/[^0-9]/', '', $size ) );
/* Adjust value as necessary - note that we do not break intentionally */
switch( $last )
@@ -2491,4 +2607,4 @@ abstract class _File
'zmm' => 'application/vnd.handheld-entertainment+xml'
);
}
}