Version 4.3.0

This commit is contained in:
Neo committed 2025-12-19 05:47:12 -08:00
1 parent 96997ddd8e
commit fe1acf984a
1349 files changed
+116159 -67711

No files matched your search

+15 -7
View File
@@ -71,7 +71,7 @@ class _Amazon extends \IPS\File
$values['bucket_path'] = trim( $values['bucket_path'], '/' );
$values['bucket'] = trim( $values['bucket'], '/' );
$filename = md5( uniqid() ) . '.ips.txt';
$filename = md5( mt_rand() ) . '.ips.txt';
try
{
@@ -282,7 +282,7 @@ class _Amazon extends \IPS\File
}
catch( \Exception $e ) { }
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
throw new \IPS\File\Exception( $path, \IPS\File\Exception::CANNOT_WRITE );
}
/* Write the gzip version */
@@ -302,7 +302,7 @@ class _Amazon extends \IPS\File
}
catch( \Exception $e ) { }
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
throw new \IPS\File\Exception( $path . '.gz', \IPS\File\Exception::CANNOT_WRITE );
}
}
}
@@ -317,7 +317,15 @@ class _Amazon extends \IPS\File
{
if ( $this->contents === NULL or $refresh === TRUE )
{
$this->contents = (string) static::makeRequest( $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}", 'GET', $this->configuration, $this->configurationId );
$response = static::makeRequest( $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}", 'GET', $this->configuration, $this->configurationId );
if ( $response->httpResponseCode == 404 )
{
throw new \IPS\File\Exception( $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}", \IPS\File\Exception::DOES_NOT_EXIST );
}
else
{
$this->contents = (string) $response;
}
}
return $this->contents;
}
@@ -448,7 +456,8 @@ class _Amazon extends \IPS\File
$headers = array();
$queryString = array(
'X-Amz-Expires' => $validForSeconds,
'response-content-disposition' => 'attachment; filename*=UTF-8\'\'' . rawurlencode( $this->originalFilename )
'response-content-disposition' => 'attachment; filename*=UTF-8\'\'' . rawurlencode( $this->originalFilename ),
'response-content-type' => static::getMimeType( $this->originalFilename ) . ";charset=UTF-8"
);
$signature = $this->signature( $this->configuration, 'GET', $fileUrl, $headers, $queryString, NULL, TRUE );
$queryString['X-Amz-Signature'] = $signature;
@@ -461,14 +470,13 @@ class _Amazon extends \IPS\File
$xml = $url->request()->get()->decodeXml();
if ( !isset( $xml->Region ) )
{
throw new \RuntimeException;
throw new \IPS\File\Exception( $fileUrl, \IPS\File\Exception::MISSING_REGION );
}
$this->configuration['region'] = (string) $xml->Region;
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $this->configuration ) ), array( 'id=?', $this->configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
return $this->generateTemporaryDownloadUrl( $validForSeconds );
}
return $url;
+4 -6
View File
@@ -125,7 +125,7 @@ class _Database extends \IPS\File
}
catch( \UnderflowException $ex )
{
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
throw new \IPS\File\Exception( $this->container . '/' . $this->filename, \IPS\File\Exception::DOES_NOT_EXIST );
}
}
}
@@ -147,13 +147,11 @@ class _Database extends \IPS\File
}
catch( \UnderflowException $ex )
{
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
throw new \IPS\File\Exception( $this->container . '/' . $this->filename, \IPS\File\Exception::DOES_NOT_EXIST );
}
}
return $this->contents;
}
/**
* Replace file contents
@@ -163,7 +161,7 @@ class _Database extends \IPS\File
*/
public function replace( $contents )
{
/* Ensure any existign files with this name are removed otherwise the wrong file may be selected from the database as it will store multiple copies when replace() is used */
/* Ensure any existing files with this name are removed otherwise the wrong file may be selected from the database as it will store multiple copies when replace() is used */
\IPS\Db::i()->delete( 'core_files', array( 'filename=? and container=?', $this->filename, $this->container ) );
parent::replace( $contents );
@@ -176,7 +174,7 @@ class _Database extends \IPS\File
*/
public function save()
{
$salt = md5( uniqid() );
$salt = md5( mt_rand() );
$id = \IPS\Db::i()->insert( 'core_files', array(
'filename' => $this->filename,
+65
View File
@@ -0,0 +1,65 @@
<?php
/**
* @brief File Exception Class
* @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 26 Mar 2013
*/
namespace IPS\File;
/* 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;
}
/**
* File Exception Class
*/
class _Exception extends \RuntimeException
{
/**
* Exception types
*/
const CANNOT_OPEN = 1;
const DOES_NOT_EXIST = 2;
const CANNOT_WRITE = 3;
const CANNOT_COPY = 4;
const CANNOT_MOVE = 5;
const CANNOT_MAKE_DIR = 6;
const MISSING_REGION = 7;
/**
* @brief File path
*/
public $file = NULL;
/**
* Constructor
*
* @param string $file File path
* @param int $error One of the defined exception constants
* @return void
*/
public function __construct( $file, $error )
{
/* Store the file */
$this->file = $file;
/* We use a prettier error message if we have a dispatcher instance */
if( \IPS\Dispatcher::hasInstance() )
{
$message = sprintf( \IPS\Member::loggedIn()->language()->get( 'files-' . $error ), $file );
}
else
{
$message = array_flip( ( new \ReflectionClass( __CLASS__ ) )->getConstants() )[ $error ];
}
parent::__construct( $message, $error );
}
}
+81 -6
View File
@@ -25,8 +25,13 @@ 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 $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', 'svg' );
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' );
/**
* @brief File extensions for HTML5 compatible videos
*/
public static $videoExtensions = array( 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mpg', 'mpeg', 'flv', 'webm', 'wmv', 'avi', 'm4v' );
/**
* @brief Does this storage method support chunked uploads?
*/
@@ -72,12 +77,36 @@ abstract class _File
else
{
$settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
/* 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 )
{
$configurationId = $k;
break;
/* Test the storage config - the first one that works wins! */
$classname = 'IPS\File\\' . $data['method'];
$class = new $classname( json_decode( $data['configuration'], TRUE ) );
$class->configurationId = $k;
try
{
/* Test - if no LogicException is thrown we are ok */
$configSettings = json_decode( $data['configuration'], TRUE );
$class->testSettings( $configSettings );
/* Still here? Then let's use this configuration. */
$configurationId = $k;
/* Now store this for future reference */
$settings["filestorage__{$storageExtension}"] = $configurationId;
\IPS\Settings::i()->changeValues( array( 'upload_settings' => json_encode( $settings ) ) );
break;
}
catch( \LogicException $e )
{
/* Move on to the next file storage configuration */
continue;
}
}
}
else
@@ -113,6 +142,7 @@ abstract class _File
}
else
{
$configurationId = $settings[ "filestorage__{$storageExtension}" ];
$storageConfigurations = static::$storageConfigurations;
static::$storageConfigurations[ $settings[ "filestorage__{$storageExtension}" ] ] = array_shift( $storageConfigurations );
}
@@ -153,6 +183,11 @@ abstract class _File
return static::$storageConfigurations;
}
/**
* @brief Copy files instead of moving them
*/
public static $copyFiles = FALSE;
/**
* Create File
*
@@ -816,7 +851,7 @@ abstract class _File
/* Make sure name doesn't have anything that may break URLs */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', $filename ) )
{
$filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', '', $filename );
$filename = mt_rand() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)\@]#', '', $filename );
}
if ( $obscure )
@@ -859,7 +894,7 @@ abstract class _File
$filename = str_replace( '.' . $matches[2], '_' . $matches[2], $filename );
}
return str_replace( array( ' ', '#' ), '_', $filename ) . '.' . md5( uniqid( \IPS\SUITE_UNIQUE_KEY ) . mt_rand() ) . ( ( $safe ) ? '.' . $ext : '' );
return str_replace( array( ' ', '#' ), '_', $filename ) . '.' . md5( mt_rand() ) . ( ( $safe ) ? '.' . $ext : '' );
}
/**
@@ -1213,6 +1248,44 @@ abstract class _File
return FALSE;
}
/**
* Determine if the file is a video
*
* @return bool
*/
public function isVideo()
{
$ext = mb_substr( $this->originalFilename, mb_strrpos( $this->originalFilename, '.' ) + 1 );
if ( in_array( mb_strtolower( $ext ), \IPS\File::$videoExtensions ) )
{
return TRUE;
}
return FALSE;
}
/**
* Get media type ("file", "image" or "video")
*
* @return bool
*/
public function mediaType()
{
if ( $this->isVideo() )
{
return 'video';
}
elseif ( $this->isImage() )
{
return 'image';
}
else
{
return 'file';
}
}
/**
* If the file is an image, get the dimensions
@@ -1220,6 +1293,7 @@ abstract class _File
* @return array
* @throws \DomainException
* @throws \InvalidArgumentException
* @throws \RuntimeException
*/
public function getImageDimensions()
{
@@ -1492,12 +1566,13 @@ abstract class _File
/**
* Get output for API
*
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
* @return array
* @apiresponse string name The filename
* @apiresponse string url URL to where file is stored
* @apiresponse int size Filesize in bytes
*/
public function apiOutput()
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
return array(
'name' => $this->originalFilename,
+18 -7
View File
@@ -252,12 +252,12 @@ class _FileSystem extends \IPS\File
if ( $this->contents === FALSE )
{
throw new \RuntimeException( 'COULD_NOT_OPEN_FILE' );
throw new \IPS\File\Exception( $this->container . '/' . $this->filename, \IPS\File\Exception::CANNOT_OPEN );
}
}
else
{
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
throw new \IPS\File\Exception( $this->container . '/' . $this->filename, \IPS\File\Exception::DOES_NOT_EXIST );
}
}
@@ -308,10 +308,21 @@ class _FileSystem extends \IPS\File
/* Save the file */
if( $this->temporaryFilePath )
{
if( !@\rename( $this->temporaryFilePath, "{$folder}/{$this->filename}" ) )
if( static::$copyFiles === TRUE )
{
\IPS\Log::log( "Could not move file {$folder}/{$this->filename}" , 'FileSystem' );
throw new \RuntimeException( 'COULD_NOT_MOVE_FILE' );
if( !@\copy( $this->temporaryFilePath, "{$folder}/{$this->filename}" ) )
{
\IPS\Log::log( "Could not copy file {$folder}/{$this->filename}" , 'FileSystem' );
throw new \IPS\File\Exception( "{$folder}/{$this->filename}", \IPS\File\Exception::CANNOT_COPY );
}
}
else
{
if( !@\rename( $this->temporaryFilePath, "{$folder}/{$this->filename}" ) )
{
\IPS\Log::log( "Could not move file {$folder}/{$this->filename}" , 'FileSystem' );
throw new \IPS\File\Exception( "{$folder}/{$this->filename}", \IPS\File\Exception::CANNOT_MOVE );
}
}
@chmod( "{$folder}/{$this->filename}", \IPS\IPS_FILE_PERMISSION );
@@ -323,7 +334,7 @@ class _FileSystem extends \IPS\File
if ( !@\file_put_contents( "{$folder}/{$this->filename}", $contents ) )
{
\IPS\Log::log( "Could not write file {$folder}/{$this->filename}" , 'FileSystem' );
throw new \RuntimeException( 'COULD_NOT_WRITE_FILE' );
throw new \IPS\File\Exception( "{$folder}/{$this->filename}", \IPS\File\Exception::CANNOT_WRITE );
}
@chmod( "{$folder}/{$this->filename}", \IPS\IPS_FILE_PERMISSION );
@@ -489,7 +500,7 @@ class _FileSystem extends \IPS\File
{
if( @mkdir( $folder, \IPS\IPS_FOLDER_PERMISSION, TRUE ) === FALSE or @chmod( $folder, \IPS\IPS_FOLDER_PERMISSION ) === FALSE )
{
throw new \RuntimeException( 'COULD_NOT_CREATE_FOLDER' );
throw new \IPS\File\Exception( $folder, \IPS\File\Exception::CANNOT_MAKE_DIR );
}
@\file_put_contents( $folder . '/index.html', '' );
}
+63 -21
View File
@@ -79,7 +79,7 @@ class _Ftp extends \IPS\File
}
$ftp->chdir( $values['ftp_details']['path'] );
$filename = md5( uniqid() ) . '.ips.txt';
$filename = md5( mt_rand() ) . '.ips.txt';
$tmpFileName = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
\file_put_contents( $tmpFileName, 'OK' );
$ftp->upload( $filename, $tmpFileName );
@@ -113,7 +113,7 @@ class _Ftp extends \IPS\File
*/
public static function displayName( $settings )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__Ftp'), $settings['ftp_details']['server'] ) ) );
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__Ftp'), ( !empty( $settings['ftp_details']['path'] ) ) ? $settings['ftp_details']['server'] . " (" . $settings['ftp_details']['path'] . ")" : $settings['ftp_details']['server'] ) ) );
}
/* !File Handling */
@@ -191,7 +191,11 @@ class _Ftp extends \IPS\File
*/
public function delete()
{
$this->ftp()->delete( $this->container . '/' . $this->filename );
/* Only issue delete command if the file (and container) exist...otherwise a delete command would fail but our intention is complete */
if( $this->checkExists( $this->container . '/' . $this->filename ) )
{
$this->ftp()->delete( $this->container . '/' . $this->filename );
}
/* Log deletion request */
$this->log( "file_deletion", 'delete', NULL, 'log' );
@@ -206,7 +210,11 @@ class _Ftp extends \IPS\File
*/
public function deleteContainer( $container )
{
$this->ftp()->rmdir( $container, TRUE );
/* Actually remove, checking if it exists before attempting */
if( $this->checkExists( $container ) )
{
$this->ftp()->rmdir( $container, TRUE );
}
/* Log deletion request */
$realContainer = $this->container;
@@ -214,13 +222,38 @@ class _Ftp extends \IPS\File
$this->log( "container_deletion", 'delete', NULL, 'log' );
$this->container = $realContainer;
}
/**
* Check if a path exists
*
* @param string $path Path to check
* @return bool
*/
protected function checkExists( $path )
{
$pathBits = explode( '/', $path );
$reconstructed = array();
foreach( $pathBits as $piece )
{
$pathToCheck = count( $reconstructed ) ? implode( '/', $reconstructed ) : '.';
if( $this->ftp()->ls( $pathToCheck ) === FALSE OR !in_array( $piece, $this->ftp()->ls( $pathToCheck ) ) )
{
return FALSE;
}
$reconstructed[] = $piece;
}
return TRUE;
}
/* !FTP Utility Methods */
/**
* @brief FTP Connection
*/
protected $ftp = NULL;
protected static $ftp = array();
/**
* Get FTP Connection
@@ -230,20 +263,22 @@ class _Ftp extends \IPS\File
*/
protected function ftp()
{
if ( $this->ftp === NULL )
$key = md5( json_encode( $this->configuration ) );
if ( !isset( static::$ftp[ $key ] ) OR static::$ftp[ $key ] === NULL )
{
if ( $this->configuration['ftp_details']['protocol'] == 'sftp' )
{
$this->ftp = new \IPS\Ftp\Sftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'] );
static::$ftp[ $key ] = new \IPS\Ftp\Sftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'] );
}
else
{
$this->ftp = new \IPS\Ftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'], ( $this->configuration['ftp_details']['protocol'] == 'ssl_ftp' ) );
static::$ftp[ $key ] = new \IPS\Ftp( $this->configuration['ftp_details']['server'], $this->configuration['ftp_details']['un'], $this->configuration['ftp_details']['pw'], $this->configuration['ftp_details']['port'], ( $this->configuration['ftp_details']['protocol'] == 'ssl_ftp' ) );
}
$this->ftp->chdir( $this->configuration['ftp_details']['path'] );
static::$ftp[ $key ]->chdir( $this->configuration['ftp_details']['path'] );
}
return $this->ftp;
return static::$ftp[ $key ];
}
/**
@@ -329,18 +364,25 @@ class _Ftp extends \IPS\File
*/
public function filesize()
{
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
if( $this->container )
try
{
$this->ftp()->chdir( $this->container );
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
if( $this->container )
{
$this->ftp()->chdir( $this->container );
}
$size = $this->ftp()->size( $this->filename );
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
return $size ?: parent::filesize();
}
catch( \IPS\Ftp\Exception $e )
{
return FALSE;
}
$size = $this->ftp()->size( $this->filename );
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
return $size ?: parent::filesize();
}
/**