Version 4.0.13.1
This commit is contained in:
1 parent
f9e857d255
commit
c6a86f0d9b
7517 files changed
+597400
-668041
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Reference in new issue
Block a user