Version 4.0.13.1

This commit is contained in:
Neo committed 2025-12-19 04:57:54 -08:00
1 parent f9e857d255
commit c6a86f0d9b
7517 files changed
+597400 -668041

No files matched your search

+411
View File
@@ -0,0 +1,411 @@
<?php
/**
* @brief File Handler: Amazon S3
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 12 Jul 2013
* @version SVN_VERSION_NUMBER
*/
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 Handler: Amazon S3
*/
class _Amazon extends \IPS\File
{
/* !ACP Configuration */
/**
* Settings
*
* @param array $configuration Configuration if editing a setting, or array() if creating a setting.
* @return array
*/
public static function settings( $configuration=array() )
{
$default = ( isset( $configuration['custom_url'] ) and ! empty( $configuration['custom_url'] ) ) ? TRUE : FALSE;
return array(
'bucket' => 'Text',
'bucket_path' => 'Text',
'access_key' => 'Text',
'secret_key' => 'Text',
'toggle' => array( 'type' => 'YesNo', 'default' => $default, 'options' => array(
'togglesOn' => array( 'Amazon_custom_url' )
) ),
'custom_url' => array( 'type' => 'Text', 'default' => '' )
);
}
/**
* Test Settings
*
* @param array $values The submitted values
* @return void
* @throws \LogicException
*/
public static function testSettings( &$values )
{
$values['bucket_path'] = trim( $values['bucket_path'], '/' );
$values['bucket'] = trim( $values['bucket'], '/' );
$filename = md5( uniqid() ) . '.ips.txt';
try
{
$response = static::makeRequest( "test/{$filename}", 'PUT', $values, NULL, "OK" );
}
catch ( \IPS\Http\Request\Exception $e )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_error_amazon_unreachable', FALSE, array( 'sprintf' => array( $values['bucket'] ) ) ) );
}
if ( $response->httpResponseCode != 200 AND $response->httpResponseCode != 307 )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_error_amazon', FALSE, array( 'sprintf' => array( $values['bucket'], $response->httpResponseCode ) ) ) );
}
$response = static::makeRequest( "test/{$filename}", 'DELETE', $values, NULL );
if ( $response->httpResponseCode == 403 )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_error_amazon_d403', FALSE, array( 'sprintf' => array( $values['bucket'], $response->httpResponseCode ) ) ) );
}
if ( ! $values['toggle'] )
{
$values['custom_url'] = NULL;
}
if ( ! empty( $values['custom_url'] ) )
{
if ( mb_substr( $values['custom_url'], 0, 2 ) !== '//' AND mb_substr( $values['custom_url'], 0, 4 ) !== 'http' )
{
$values['custom_url'] = '//' . $values['custom_url'];
}
$test = $values['custom_url'];
if ( mb_substr( $test, 0, 2 ) === '//' )
{
$test = 'http:' . $test;
}
if ( filter_var( $test, FILTER_VALIDATE_URL ) === false )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'url_is_not_real', FALSE, array( 'sprintf' => array( $values['custom_url'] ) ) ) );
}
}
}
/**
* Determine if the change in configuration warrants a move process
*
* @param array $configuration New Storage configuration
* @param array $oldConfiguration Existing Storage Configuration
* @return boolean
*/
public static function moveCheck( $configuration, $oldConfiguration )
{
foreach( array( 'bucket', 'bucket_path' ) as $field )
if ( $configuration[ $field ] !== $oldConfiguration[ $field ] )
{
return TRUE;
}
return FALSE;
}
/**
* Display name
*
* @param array $settings Configuration settings
* @return string
*/
public static function displayName( $settings )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__Amazon'), $settings['bucket'] ) ) );
}
/* !File Handling */
/**
* Constructor
*
* @param array $configuration Storage configuration
* @return void
*/
public function __construct( $configuration )
{
$this->container = 'monthly_' . date( 'Y' ) . '_' . date( 'm' );
parent::__construct( $configuration );
}
/**
* Return the base URL
*
* @return string
*/
public function baseUrl()
{
return preg_replace( '#^http(s)?://#', '//', ( empty( $this->configuration['custom_url'] ) ) ? rtrim( static::buildBaseUrl( $this->configuration ), '/' ) : $this->configuration['custom_url'] );
}
/**
* Save File
*
* @return void
*/
public function save()
{
/* @link http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html regarding cleaning filenames */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
{
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
}
$this->container = trim( $this->container, '/' );
$this->url = static::buildBaseUrl( $this->configuration ) . "{$this->container}/{$this->filename}";
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'PUT', $this->configuration, $this->configurationId, (string) $this->contents() );
if ( $response->httpResponseCode != 200 )
{
throw new \RuntimeException('COULD_NOT_SAVE_FILE');
}
}
/**
* Get Contents
*
* @param bool $refresh If TRUE, will fetch again
* @return string
*/
public function contents( $refresh=FALSE )
{
if ( $this->contents === NULL or $refresh === TRUE )
{
$this->contents = (string) static::makeRequest( "{$this->container}/{$this->filename}", 'GET', $this->configuration, $this->configurationId );
}
return $this->contents;
}
/**
* Delete
*
* @return void
*/
public function delete()
{
$this->container = trim( $this->container, '/' );
$response = static::makeRequest( "{$this->container}/{$this->filename}", 'DELETE', $this->configuration, $this->configurationId );
if ( $response->httpResponseCode == 204 )
{
/* Ok */
return;
}
if ( $response->httpResponseCode != 200 )
{
$this->log( 'COULD_NOT_DELETE_FILE', 'delete', array( $response->httpResponseCode, $response->httpResponseText ), 'error' );
throw new \RuntimeException('COULD_NOT_DELETE_FILE');
}
}
/**
* Delete Container
*
* @param string $container Key
* @return void
*/
public function deleteContainer( $container )
{
$_strip = array( '_strip_querystring' => TRUE, 'bucket_path' => NULL );
if ( $this->configuration['bucket_path'] )
{
$container = $this->configuration['bucket_path'] . '/' . $container;
}
$response = static::makeRequest( "?prefix=" . urlencode( $container . "/" ), 'GET', array_merge( $this->configuration, $_strip ), $this->configurationId );
/* Parse XML document */
$document = \IPS\Xml\SimpleXML::loadString( $response );
/* Loop over dom document */
foreach( $document->Contents as $result )
{
if ( $this->configuration['bucket_path'] )
{
$result->Key = mb_substr( $result->Key, ( mb_strlen( $this->configuration['bucket_path'] ) + 1 ) );
}
static::makeRequest( $result->Key, 'DELETE', $this->configuration, $this->configurationId );
}
}
/* !Amazon Utility Methods */
/**
* Sign and make request
*
* @param string $uri The URI (relative to the bucket)
* @param string $verb The HTTP verb to use
* @param array $configuration The configuration for this instance
* @param int $configurationId The configuration ID
* @param string|null $content The content to send
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\Exception
*/
protected static function makeRequest( $uri, $verb, $configuration, $configurationId, $content=NULL )
{
/* Build a request */
$request = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri )->request();
$date = date('r');
$headers = array( 'Date' => $date, 'Content-Type' => \IPS\File::getMimeType( $uri ), 'Content-MD5' => base64_encode( md5( $content, TRUE ) ), 'x-amz-acl' => 'public-read' );
if( mb_strtoupper( $verb ) === 'PUT' )
{
$headers['Content-Length'] = \strlen( $content );
}
/* We need to strip query string parameters for the signature, but not always (e.g. a subresource such as ?acl needs to be included and multi-
object delete requests must include the query string params). Let the callee decide to do this or not. */
if( isset( $configuration['_strip_querystring'] ) AND $configuration['_strip_querystring'] === TRUE )
{
$uri = preg_replace( "/^(.*?)\?.*$/", "$1", $uri );
}
$string = implode( "\n", array(
mb_strtoupper( $verb ),
$headers['Content-MD5'],
$headers['Content-Type'],
$date,
"x-amz-acl:{$headers['x-amz-acl']}",
"/{$configuration['bucket']}" . ( ( isset( $configuration['bucket_path'] ) AND ! empty( $configuration['bucket_path'] ) ) ? '/' . trim( $configuration['bucket_path'], '/' ) . '/' : '/' ) . $uri
) );
/* Build a signature */
$signature = base64_encode( hash_hmac( 'sha1', $string, $configuration['secret_key'], true ) );
/* Sign the request */
$headers['Authorization'] = "AWS {$configuration['access_key']}:{$signature}";
$request->setHeaders( $headers );
/* Make the request */
$verb = mb_strtolower( $verb );
$response = $request->$verb( $content );
/* Need a different region? */
if ( $response->httpResponseCode == 307 AND $configurationId )
{
$xml = $response->decodeXml();
$configuration['region'] = mb_substr( $xml->Endpoint, mb_strlen( $configuration['bucket'] ) + 1, -mb_strlen( '.amazonaws.com' ) );
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $configuration ) ), array( 'id=?', $configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
}
/* Return */
return $response;
}
/**
* Build up the base Amazon URL
* @param array $configuration Configuration data
* @return string
*/
public static function buildBaseUrl( $configuration )
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" )
. "://{$configuration['bucket']}"
. ( ( isset( $configuration['region'] ) AND ! empty( $configuration['region'] ) ) ? ".{$configuration['region']}" : '.s3' )
. ".amazonaws.com"
. ( ( isset( $configuration['bucket_path'] ) AND ! empty( $configuration['bucket_path'] ) ) ? '/' . trim( $configuration['bucket_path'], '/' ) . '/' : '/' );
}
/**
* Remove orphaned files
*
* @param int $fileIndex The file offset to start at in a listing
* @param array $engines All file storage engine extension objects
* @return array
*/
public function removeOrphanedFiles( $fileIndex, $engines )
{
/* Start off our results array */
$results = array(
'_done' => FALSE,
'fileIndex' => $fileIndex,
);
$checked = 0;
$skipped = 0;
$_strip = array( '_strip_querystring' => TRUE, 'bucket_path' => NULL );
if( $fileIndex )
{
$response = static::makeRequest( "?marker={$fileIndex}&max-keys=100", 'GET', array_merge( $this->configuration, $_strip ), $this->configurationId );
}
else
{
$response = static::makeRequest( "?max-keys=100", 'GET', array_merge( $this->configuration, $_strip ), $this->configurationId );
}
/* Parse XML document */
$document = \IPS\Xml\SimpleXML::loadString( $response );
/* Loop over dom document */
foreach( $document->Contents as $result )
{
$checked++;
if ( $this->configuration['bucket_path'] )
{
$result->Key = mb_substr( $result->Key, ( mb_strlen( $this->configuration['bucket_path'] ) + 1 ) );
}
/* Next we will have to loop through each storage engine type and call it to see if the file is valid */
foreach( $engines as $engine )
{
/* If this file is valid for the engine, skip to the next file */
if( $engine->isValidFile( $result->Key ) )
{
continue 2;
}
}
/* If we are still here, the file was not valid. Delete and increment count. */
$this->logOrphanedFile( $result->Key );
$_lastKey = $result->Key;
}
if( $document->IsTruncated == 'true' AND $checked == 100 )
{
$results['fileIndex'] = $_lastKey;
}
/* Are we done? */
if( !$checked OR $checked < 100 )
{
$results['_done'] = TRUE;
}
return $results;
}
}
+222
View File
@@ -0,0 +1,222 @@
<?php
/**
* @brief File Handler: Database
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 06 May 2013
* @version SVN_VERSION_NUMBER
*/
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 Handler: Database
*/
class _Database extends \IPS\File
{
/* !ACP Configuration */
/**
* Settings
*
* @param array $configuration Configuration if editing a setting, or array() if creating a setting.
* @return array
*/
public static function settings( $configuration=array() )
{
return array();
}
/**
* Display name
*
* @param array $settings Configuration settings
* @return string
*/
public static function displayName( $settings )
{
return \IPS\Member::loggedIn()->language()->addToStack('filehandler__Database');
}
/* !File Handling */
/**
* Is this URL valid for this engine?
*
* @param \IPS\Http\Url $url URL
* @param array $configuration Specific configuration for this method
* @return bool
*/
public static function isValidUrl( $url, $configuration )
{
$check = \IPS\Http\Url::internal( "applications/core/interface/file/index.php", 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE );
if ( mb_substr( $url, 0, mb_strlen( $check ) ) === $check )
{
return TRUE;
}
return FALSE;
}
/**
* Constructor
*
* @param array $configuration Storage configuration
* @return void
*/
public function __construct( $configuration )
{
$this->container = 'monthly_' . date( 'Y' ) . '_' . date( 'm' );
parent::__construct( $configuration );
}
/**
* Return the base URL
*
* @return string
*/
public function baseUrl()
{
return (string) \IPS\Http\Url::internal( "applications/core/interface/file/index.php?file=", 'none', NULL, array(), \IPS\Http\Url::PROTOCOL_RELATIVE );
}
/**
* Load File Data
*
* @return void
*/
public function load()
{
parent::load();
$this->filename = urldecode( $this->filename );
$this->container = urldecode( $this->container );
$this->originalFilename = urldecode( $this->originalFilename );
}
/**
* Get Contents
*
* @param bool $refresh If TRUE, will fetch again
* @return string
*/
public function contents( $refresh=FALSE )
{
if ( $this->contents === NULL or $refresh === TRUE )
{
try
{
$record = \IPS\Db::i()->select( '*', 'core_files', array( 'filename=? AND container=?', $this->filename, $this->container ) )->first();
$this->contents = $record['contents'];
}
catch( \UnderflowException $ex )
{
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
}
}
return $this->contents;
}
/**
* Save File
*
* @return void
*/
public function save()
{
$salt = md5( uniqid() );
/* Make sure name doesn't have anything that may break URLs */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
{
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
}
$id = \IPS\Db::i()->insert( 'core_files', array(
'filename' => $this->filename,
'salt' => $salt,
'contents' => $this->contents(),
'container' => $this->container
) );
$this->url = new \IPS\Http\Url( $this->fullyQualifiedUrl( "{$this->container}/{$this->filename}" ) );
}
/**
* Delete
*
* @return void
*/
public function delete()
{
\IPS\Db::i()->delete( 'core_files', array( 'filename=? AND container=?', $this->filename, $this->container ) );
}
/**
* Delete Container
*
* @param string $container Key
* @return void
*/
public function deleteContainer( $container )
{
\IPS\Db::i()->delete( 'core_files', array( 'container=?', $container ) );
}
/**
* Remove orphaned files
*
* @param int $fileIndex The file offset to start at in a listing
* @param array $engines All file storage engine extension objects
* @return array
*/
public function removeOrphanedFiles( $fileIndex, $engines )
{
/* Start off our results array */
$results = array(
'_done' => FALSE,
'fileIndex' => $fileIndex,
);
/* Init */
$checked = 0;
/* Loop over files */
foreach( \IPS\DB::i()->select( '*', 'core_files', array(), 'id ASC', array( $fileIndex, 100 ) ) as $file )
{
$checked++;
/* Next we will have to loop through each storage engine type and call it to see if the file is valid */
foreach( $engines as $engine )
{
/* If this file is valid for the engine, skip to the next file */
if( $engine->isValidFile( $file['container'] . '/' . $file['filename'] ) )
{
continue 2;
}
}
/* If we are still here, the file was not valid. Delete and increment count. */
$this->logOrphanedFile( $file['container'] . '/' . $file['filename'] );
}
$results['fileIndex'] += $checked;
/* Are we done? */
if( !$checked OR $checked < 100 )
{
$results['_done'] = TRUE;
}
return $results;
}
}
+2174
View File
File diff suppressed because it is too large. Load diff
+576
View File
@@ -0,0 +1,576 @@
<?php
/**
* @brief File Handler: File System
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 06 May 2013
* @version SVN_VERSION_NUMBER
*/
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 Handler: File System
*/
class _FileSystem extends \IPS\File
{
/* !ACP Configuration */
/**
* Settings
*
* @param array $configuration Configuration if editing a setting, or array() if creating a setting.
* @return array
*/
public static function settings( $configuration=array() )
{
$default = ( isset( $configuration['custom_url'] ) and ! empty( $configuration['custom_url'] ) ) ? TRUE : FALSE;
return array(
'dir' => array( 'type' => 'Text', 'default' => '{root}/uploads' ),
'toggle' => array( 'type' => 'YesNo', 'default' => $default, 'options' => array(
'togglesOn' => array( 'FileSystem_custom_url' )
) ),
'custom_url' => array( 'type' => 'Text', 'default' => '' ),
);
}
/**
* Test Settings
*
* @param array $values The submitted values
* @return void
* @throws \LogicException
*/
public static function testSettings( &$values )
{
$values['dir'] = rtrim( $values['dir'], '/' );
$testDir = str_replace( '{root}', \IPS\ROOT_PATH, $values['dir'] );
$values['url'] = trim( str_replace( \IPS\ROOT_PATH, '', $testDir ), '/' );
if ( empty( $values['toggle'] ) )
{
$values['custom_url'] = NULL;
}
if ( !is_dir( $testDir ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'dir_does_not_exist', FALSE, array( 'sprintf' => array( $testDir ) ) ) );
}
if ( !is_writable( $testDir ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'dir_is_not_writable', FALSE, array( 'sprintf' => array( $testDir ) ) ) );
}
if ( ! empty( $values['custom_url'] ) )
{
if ( mb_substr( $values['custom_url'], 0, 2 ) !== '//' AND mb_substr( $values['custom_url'], 0, 4 ) !== 'http' )
{
$values['custom_url'] = '//' . $values['custom_url'];
}
$test = $values['custom_url'];
if ( mb_substr( $test, 0, 2 ) === '//' )
{
$test = 'http:' . $test;
}
if ( filter_var( $test, FILTER_VALIDATE_URL ) === false )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'url_is_not_real', FALSE, array( 'sprintf' => array( $values['custom_url'] ) ) ) );
}
}
}
/**
* Determine if the change in configuration warrants a move process
*
* @param array $configuration New Storage configuration
* @param array $oldConfiguration Existing Storage Configuration
* @return boolean
*/
public static function moveCheck( $configuration, $oldConfiguration )
{
if ( str_replace( '{root}', \IPS\ROOT_PATH, $configuration['dir'] ) !== str_replace( '{root}', \IPS\ROOT_PATH, $oldConfiguration['dir'] ) )
{
return TRUE;
}
return FALSE;
}
/**
* Display name
*
* @param array $settings Configuration settings
* @return string
*/
public static function displayName( $settings )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__FileSystem'), str_replace( '{root}', \IPS\ROOT_PATH, $settings['dir'] ) ) ) );
}
/* !File Handling */
/**
* @brief Does this storage method support chunked uploads?
*/
public static $supportsChunking = TRUE;
/**
* Constructor
*
* @param array $configuration Storage configuration
* @return void
*/
public function __construct( $configuration )
{
$this->container = 'monthly_' . date( 'Y' ) . '_' . date( 'm' );
$configuration['dir'] = str_replace( '{root}', \IPS\ROOT_PATH, $configuration['dir'] );
parent::__construct( $configuration );
}
/**
* @brief Store the path to the file so we can just move it later
*/
protected $temporaryFilePath = NULL;
/**
* Set the file
*
* @param string $filepath The path to the file on disk
* @return void
*/
public function setFile( $filepath )
{
$this->temporaryFilePath = $filepath;
}
/**
* Return the base URL
*
* @return string
*/
public function baseUrl()
{
$url = ( empty( $this->configuration['custom_url'] ) ) ? \IPS\Settings::i()->base_url . $this->configuration['url'] : $this->configuration['custom_url'];
if ( \IPS\Request::i()->isSecure() )
{
$url = str_replace( 'http://', 'https://', $url );
}
return $url;
}
/**
* Move file to a different storage location
*
* @param int $storageConfiguration New storage configuration ID
* @param int $flags Bitwise Flags
* @return \IPS\File
*/
public function move( $storageConfiguration, $flags=0 )
{
if ( $this->configurationId === $storageConfiguration and isset( $this->configuration['old_url'] ) )
{
/* We're just updating the URL, not actually moving the file */
if ( mb_substr( $this->url, 0, mb_strlen( $this->configuration['old_url'] ) ) == $this->configuration['old_url'] )
{
$this->url = str_replace( $this->configuration['old_url'], $this->configuration['url'], $this->url );
return $this;
}
/* Is this the new url, then? */
if ( mb_substr( $this->url, 0, mb_strlen( $this->configuration['url'] ) ) != $this->configuration['url'] )
{
/* No? Something has gone wrong */
throw new \RuntimeException('url_update_incorrect_url');
}
else
{
return $this;
}
}
else if ( static::$storageConfigurations[ $storageConfiguration ]['method'] === 'FileSystem' )
{
$newConfig = json_decode( static::$storageConfigurations[ $storageConfiguration ]['configuration'], TRUE );
/* Do both configs have the same path? */
if ( str_replace( '{root}', \IPS\ROOT_PATH, $newConfig['dir'] ) == $this->configuration['dir'] )
{
/* Don't move */
return $this;
}
}
return parent::move( $storageConfiguration );
}
/**
* Print the contents of the file
*
* @param int|null $start Start point to print from (for ranges)
* @param int|null $length Length to print to (for ranges)
* @param int|null $throttle Throttle speed
* @return void
*/
public function printFile( $start=NULL, $length=NULL, $throttle=NULL )
{
$file = $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename;
/* Turn off output buffering if it is on */
while( ob_get_level() > 0 )
{
ob_end_clean();
}
if( $throttle === NULL AND !($start AND $length) AND function_exists('readfile') )
{
readfile( $file );
}
else
{
if( $fh = fopen( $file, 'rb' ) )
{
$read = ( $throttle !== NULL ) ? $throttle : 4096;
if( $start AND $length )
{
fseek( $fh, $start );
while( $length AND !feof( $fh ) )
{
if( $read > $length )
{
$read = $length;
}
echo fread( $fh, $read );
flush();
$length -= $read;
if( $throttle )
{
sleep( 1 );
}
}
}
else
{
while( ! feof( $fh ) )
{
echo fread( $fh, $read );
flush();
if( $throttle )
{
sleep( 1 );
}
}
}
fclose( $fh );
}
}
}
/**
* Get Contents
*
* @param bool $refresh If TRUE, will fetch again
* @return string
* @throws \RuntimeException
*/
public function contents( $refresh=FALSE )
{
if ( $this->contents === NULL or $refresh === TRUE )
{
if( file_exists( $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename ) )
{
if ( ! ( $this->contents = @file_get_contents( $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename ) ) )
{
throw new \RuntimeException( 'COULD_NOT_OPEN_FILE' );
}
}
else
{
throw new \RuntimeException( 'FILE_DOES_NOT_EXIST' );
}
}
return $this->contents;
}
/**
* If the file is an image, get the dimensions
*
* @return array
* @throws \DomainException
* @throws \InvalidArgumentException
*/
public function getImageDimensions()
{
if( !$this->isImage() )
{
throw new \DomainException;
}
$file = $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename;
if( ( $image = getimagesize( $file ) ) === FALSE )
{
return parent::getImageDimensions();
}
return array( $image[0], $image[1] );
}
/**
* Save File
*
* @return void
* @throws \RuntimeException
*/
public function save()
{
/* Make sure name doesn't have anything that may break URLs */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
{
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
}
/* Make the folder */
$folder = $this->configuration['dir'] . '/' . $this->getFolder();
/* Save the file */
if( $this->temporaryFilePath )
{
if( !@\rename( $this->temporaryFilePath, "{$folder}/{$this->filename}" ) OR @chmod( "{$folder}/{$this->filename}", \IPS\IPS_FILE_PERMISSION ) === FALSE )
{
throw new \RuntimeException( 'COULD_NOT_MOVE_FILE' );
}
}
else
{
if ( $contents = $this->contents() )
{
if ( !@\file_put_contents( "{$folder}/{$this->filename}", $contents ) OR @chmod( "{$folder}/{$this->filename}", \IPS\IPS_FILE_PERMISSION ) === FALSE )
{
throw new \RuntimeException( 'COULD_NOT_WRITE_FILE' );
}
}
else
{
$return = touch( "{$folder}/{$this->filename}" );
@chmod( "{$folder}/{$this->filename}", \IPS\IPS_FILE_PERMISSION );
}
}
/* Set the URL */
$this->url = new \IPS\Http\Url( $this->fullyQualifiedUrl( "{$this->container}/{$this->filename}" ) );
}
/**
* Delete
*
* @return bool
*/
public function delete()
{
if( file_exists( "{$this->configuration['dir']}/{$this->container}/{$this->filename}" ) )
{
return @unlink( "{$this->configuration['dir']}/{$this->container}/{$this->filename}" );
}
else
{
return false;
}
}
/**
* Delete Container
*
* @param string $container Key
* @return void
*/
public function deleteContainer( $container )
{
$dir = $this->configuration['dir'] . '/' . $container;
if ( is_dir( $dir ) )
{
foreach ( new \DirectoryIterator( $dir ) as $f )
{
if ( !$f->isDot() )
{
unlink( $f->getPathname() );
}
}
$handle = opendir( $dir );
closedir ( $handle );
rmdir( $dir );
}
}
/**
* Append more contents in a chunked upload
*
* @param string $filename The filename that was created
* @param string $chunk The temp filename for the new chunk to append
* @param string|NULL $rename If a value is provided, rename the file after appending (will be provided for the last chunk)
* @param string|NULL $container Key to identify container for storage
* @return string
* @throws \RuntimeException
*/
public function chunkAppend( $filename, $chunk, $rename=NULL, $container=NULL )
{
$folder = $this->getFolder( $container );
$file = fopen( "{$this->configuration['dir']}/{$folder}/{$filename}", 'ab' );
$_chunk = fopen( $chunk, 'rb' );
while ( $buffer = fread( $_chunk, 4096 ) )
{
\fwrite( $file, $buffer );
}
fclose( $file );
@unlink( $chunk );
if ( $rename )
{
$newFileName = static::obscureFilename( $rename );
rename( "{$this->configuration['dir']}/{$folder}/{$filename}", "{$this->configuration['dir']}/{$folder}/{$newFileName}" );
return "{$folder}/{$newFileName}";
}
return "{$folder}/{$filename}";
}
/* !File System Utility Methods */
/**
* Get the path to the folder
*
* @param string|null $folderName Folder name - if NULL, a monthly name will be used
* @return string
*/
protected function getFolder( $folderName=NULL )
{
$folderName = $folderName ?: $this->container;
$folder = $this->configuration['dir'] . '/' . $folderName;
if( !is_dir( $folder ) )
{
if( @mkdir( $folder, \IPS\IPS_FOLDER_PERMISSION, TRUE ) === FALSE or @chmod( $folder, \IPS\IPS_FOLDER_PERMISSION ) === FALSE )
{
throw new \RuntimeException( 'COULD_NOT_CREATE_FOLDER' );
}
@\file_put_contents( $folder . '/index.html', '' );
}
return $folderName;
}
/**
* Remove orphaned files
*
* @param int $fileIndex The file offset to start at in a listing
* @param array $engines All file storage engine extension objects
* @return array
*/
public function removeOrphanedFiles( $fileIndex, $engines )
{
/* Start off our results array */
$results = array(
'_done' => FALSE,
'fileIndex' => $fileIndex,
);
/* Some basic init */
$checked = 0;
$skipped = 0;
/* We need to open our storage directory and start looping over it */
$dir = $this->configuration['dir'];
if ( is_dir( $dir ) )
{
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS ) );
foreach ( $iterator as $f )
{
/* We aren't checking directories */
if( $f->isDir() OR $f->getFilename() == 'index.html' OR mb_substr( $f->getFilename(), 0, 1 ) === '.' OR mb_substr( $iterator->getSubPathname(), 0, 5 ) === 'logs/' )
{
continue;
}
/* Have we hit our limit? If so we need to stop. */
if( $checked >= 100 )
{
break;
}
/* Is there an offset? If so we need to skip */
if( $fileIndex > 0 AND $fileIndex > $skipped )
{
$skipped++;
continue;
}
$checked++;
/* Next we will have to loop through each storage engine type and call it to see if the file is valid */
foreach( $engines as $engine )
{
/* If this file is valid for the engine, skip to the next file */
try
{
if( $engine->isValidFile( $iterator->getSubPathname() ) )
{
continue 2;
}
}
catch( \InvalidArgumentException $e )
{
continue 2;
}
}
/* If we are still here, the file was not valid */
$this->logOrphanedFile( $iterator->getSubPathname() );
}
}
$results['fileIndex'] += $checked;
/* Are we done? */
if( !$checked OR $checked < 100 )
{
$results['_done'] = TRUE;
}
return $results;
}
/**
* Get filesize (in bytes)
*
* @return string
*/
public function filesize()
{
return file_exists( $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename ) ? @filesize( $this->configuration['dir'] . '/' . $this->container . '/' . $this->filename ) : NULL;
}
}
+341
View File
@@ -0,0 +1,341 @@
<?php
/**
* @brief File Handler: FTP
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 06 May 2013
* @version SVN_VERSION_NUMBER
*/
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 Handler: FTP
*/
class _Ftp extends \IPS\File
{
/* !ACP Configuration */
/**
* Settings
*
* @param array $configuration Configuration if editing a setting, or array() if creating a setting.
* @return array
*/
public static function settings( $configuration=array() )
{
return array(
'ftp_details' => array( 'type' => 'Ftp', 'options' => array( 'validate' => FALSE ) ),
'url' => array( 'type' => 'Text', 'options' => array( 'placeholder' => 'http://www.example.com/example' ) ),
);
}
/**
* Test Settings
*
* @param array $values The submitted values
* @return void
* @throws \LogicException
*/
public static function testSettings( &$values )
{
if ( !function_exists( 'ftp_connect' ) )
{
throw new \BadFunctionCallException( 'ftp_err_no_ext' );
}
elseif ( $values['ftp_details']['protocol'] === 'ssl_ftp' and !function_exists( 'ftp_ssl_connect' ) )
{
throw new \BadFunctionCallException( 'ftp_err_no_ssl' );
}
elseif ( $values['ftp_details']['protocol'] === 'sftp' and !function_exists( 'ssh2_connect' ) )
{
throw new \BadFunctionCallException( 'ftp_err_no_sftp' );
}
$values['url'] = rtrim( $values['url'], '/' );
if ( filter_var( $values['url'], FILTER_VALIDATE_URL ) === false )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'url_is_not_real', FALSE, array( 'sprintf' => array( $values['url'] ) ) ) );
}
try
{
if ( $values['ftp_details']['protocol'] == 'sftp' )
{
$ftp = new \IPS\Ftp\Sftp( $values['ftp_details']['server'], $values['ftp_details']['un'], $values['ftp_details']['pw'], $values['ftp_details']['port'] );
}
else
{
$ftp = new \IPS\Ftp( $values['ftp_details']['server'], $values['ftp_details']['un'], $values['ftp_details']['pw'], $values['ftp_details']['port'], ( $values['ftp_details']['protocol'] == 'ssl_ftp' ) );
}
$ftp->chdir( $values['ftp_details']['path'] );
$filename = md5( uniqid() ) . '.ips.txt';
$tmpFileName = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
\file_put_contents( $tmpFileName, 'OK' );
$ftp->upload( $filename, $tmpFileName );
$result = \IPS\Http\Url::external( $values['url'] . '/' . $filename)->request()->get();
$ftp->delete( $filename );
unlink( $tmpFileName );
if ( (string) $result !== 'OK' )
{
if ( $result->httpResponseCode == 200 )
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_ftp_unexpected_response', FALSE, array( 'sprintf' => array( $values['ftp_details']['server'] ) ) ) );
}
else
{
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'file_storage_test_error_ftp', FALSE, array( 'sprintf' => array( $values['ftp_details']['server'], $result->httpResponseCode ) ) ) );
}
}
}
catch ( \IPS\Ftp\Exception $e )
{
throw new \DomainException( 'ftp_err-' . $e->getMessage() );
}
}
/**
* Display name
*
* @param array $settings Configuration settings
* @return string
*/
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'] ) ) );
}
/* !File Handling */
/**
* Constructor
*
* @param array $configuration Storage configuration
* @return void
*/
public function __construct( $configuration )
{
$this->container = 'monthly_' . date( 'm' ) . '_' . date( 'Y' );
parent::__construct( $configuration );
}
/**
* Return the base URL
*
* @return string
*/
public function baseUrl()
{
return $this->configuration['url'];
}
/**
* Save File
*
* @return void
* @throws \IPS\Ftp\Exception
*/
public function save()
{
/* Make sure name doesn't have anything that may break URLs */
if ( preg_match( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', $this->filename ) )
{
$this->filename = uniqid() . '_' . preg_replace( '#[^a-zA-Z0-9!\-_\.\*\(\)]#', '', $this->filename );
}
/* Get contents */
$contents = $this->contents();
/* Create a temporary file */
$tmpFileName = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
\file_put_contents( $tmpFileName, $contents );
/* Move into the correct folder */
if( !in_array( $this->container, $this->ftp()->ls() ) )
{
/* Make dir */
$this->ftp()->mkdir( $this->container );
/* Stick an index.html file in */
$tmpIndexFile = tempnam( \IPS\TEMP_DIRECTORY, 'IPS' );
\file_put_contents( $tmpIndexFile, '' );
$this->ftp()->upload( 'index.html', $tmpIndexFile );
unlink( $tmpIndexFile );
}
$this->ftp()->chdir( $this->container );
/* Upload */
$this->ftp()->upload( $this->filename, $tmpFileName );
/* Move back */
$this->ftp()->cdup();
/* Destroy the temporary file */
unlink( $tmpFileName );
/* Set the URL */
$this->url = new \IPS\Http\Url( $this->fullyQualifiedUrl( "{$this->container}/{$this->filename}" ) );
}
/**
* Delete
*
* @return void
* @throws \IPS\Ftp\Exception
*/
public function delete()
{
$this->ftp()->delete( $this->container . '/' . $this->filename );
}
/**
* Delete Container
*
* @param string $container Key
* @return void
* @throws \IPS\Ftp\Exception
*/
public function deleteContainer( $container )
{
$this->ftp()->rmdir( $container, TRUE );
}
/* !FTP Utility Methods */
/**
* @brief FTP Connection
*/
protected static $ftp = NULL;
/**
* Get FTP Connection
*
* @return \IPS\Ftp
* @throws \IPS\Ftp\Exception
*/
protected function ftp()
{
if ( static::$ftp === NULL )
{
if ( $this->configuration['ftp_details']['protocol'] == 'sftp' )
{
static::$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'] );
}
else
{
static::$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->chdir( $this->configuration['ftp_details']['path'] );
}
return static::$ftp;
}
/**
* Remove orphaned files
*
* @param int $fileIndex The file offset to start at in a listing
* @param array $engines All file storage engine extension objects
* @return array
*/
public function removeOrphanedFiles( $fileIndex, $engines )
{
/* Start off our results array */
$results = array(
'_done' => FALSE,
'fileIndex' => $fileIndex,
);
/* We don't really care about the container index for the database method...just look for files based on the file offset */
$checked = 0;
$skipped = 0;
$iterator = new \RecursiveIteratorIterator( new \IPS\Ftp\RecursiveDirectoryFtpIterator( $this->ftp(), $this->configuration['ftp_details']['path'] ) );
while( $iterator->valid() )
{
/* We aren't checking directories */
if( $iterator->current()->isDir() OR $iterator->current()->getFilename() == 'index.html' )
{
$iterator->next();
continue;
}
/* Have we hit our limit? If so we need to stop. */
if( $checked >= 100 )
{
break;
}
/* Is there an offset? If so we need to skip */
if( $fileIndex > 0 AND $fileIndex > $skipped )
{
$skipped++;
$iterator->next();
continue;
}
$checked++;
$currentName = str_replace( $this->configuration['ftp_details']['path'] . '/', '', $iterator->current()->getPathname() );
/* Next we will have to loop through each storage engine type and call it to see if the file is valid */
foreach( $engines as $engine )
{
/* If this file is valid for the engine, skip to the next file */
if( $engine->isValidFile( $currentName ) )
{
$iterator->next();
continue 2;
}
}
/* If we are still here, the file was not valid */
$this->logOrphanedFile( $currentName );
$iterator->next();
}
$results['fileIndex'] += $checked;
/* Are we done? */
if( !$checked OR $checked < 100 )
{
$results['_done'] = TRUE;
}
return $results;
}
/**
* Get filesize (in bytes)
*
* @return string
*/
public function filesize()
{
$this->ftp()->chdir( $this->configuration['ftp_details']['path'] );
if( $this->container )
{
$this->ftp()->chdir( $this->container );
}
$size = $this->ftp()->size( $this->filename );
return $size ?: parent::filesize();
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
/**
* @brief File IteratorIterator
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
* @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Social Suite
* @since 10 Oct 2013
* @version SVN_VERSION_NUMBER
*/
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 IteratorIterator
*/
class _Iterator extends \IteratorIterator implements \Countable
{
/**
* @brief Stroage Extension
*/
protected $storageExtension;
/**
* @brief URL Field
*/
protected $urlField;
/**
* @brief URLs Only
*/
protected $fileUrlsOnly;
/**
* Constructor
*
* @param Traversable $iterator The iterator
* @param string $storageExtension The storage extension
* @param string|NULL $urlField If passed a string, will look for an element with that key in the data returned from the iterator
* @return void
*/
public function __construct( \Traversable $iterator, $storageExtension, $urlField=NULL, $fileUrlsOnly=FALSE )
{
$this->storageExtension = $storageExtension;
$this->urlField = $urlField;
$this->fileUrlsOnly = $fileUrlsOnly;
return parent::__construct( $iterator );
}
/**
* Get current
*
* @return \IPS\File
*/
public function current()
{
try
{
$data = $this->data();
$obj = \IPS\File::get( $this->storageExtension, $this->urlField ? $data[ $this->urlField ] : $data );
return ( $this->fileUrlsOnly ) ? (string) $obj->url : $obj;
}
catch ( \Exception $e )
{
$this->next();
return $this->current();
}
}
/**
* Get data
*
* @return mixed
*/
public function data()
{
return parent::current();
}
/**
* Get count
*
* @return int
*/
public function count()
{
return $this->getInnerIterator()->count();
}
}
View File
Whitespace-only changes.