Version 4.7.23

This commit is contained in:
Neo committed 2025-12-19 16:21:27 -08:00
1 parent 7124a02564
commit 25ddeb65d6
1791 files changed
+76990 -44452

No files matched your search

+137 -138
View File
@@ -27,9 +27,9 @@ class _Amazon extends \IPS\File
* Looks up $storageExtensions of extension classes
*/
protected static $gzipExtensions = array();
/* !ACP Configuration */
/**
* Settings
*
@@ -39,7 +39,7 @@ class _Amazon extends \IPS\File
public static function settings( $configuration=array() )
{
$default = ( isset( $configuration['custom_url'] ) and ! empty( $configuration['custom_url'] ) ) ? TRUE : FALSE;
return array(
'bucket' => 'Text',
'endpoint' => array( 'type' => 'Text', 'default' => 's3.amazonaws.com' ),
@@ -57,7 +57,7 @@ class _Amazon extends \IPS\File
* @brief Temporarily stored endpoint - when testing settings, we may need to update it automagically
*/
protected static $updatedEndpoint = NULL;
/**
* Test Settings
*
@@ -69,9 +69,9 @@ class _Amazon extends \IPS\File
{
$values['bucket_path'] = trim( $values['bucket_path'], '/' );
$values['bucket'] = trim( $values['bucket'], '/' );
$filename = md5( mt_rand() ) . '.ips.txt';
try
{
$response = static::makeRequest( "test/{$filename}", 'PUT', $values, NULL, "OK" );
@@ -80,14 +80,14 @@ class _Amazon extends \IPS\File
{
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 ) ) ) );
@@ -98,33 +98,33 @@ class _Amazon extends \IPS\File
$values['endpoint'] = static::$updatedEndpoint;
static::$updatedEndpoint = NULL;
}
if ( empty( $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
*
@@ -141,7 +141,7 @@ class _Amazon extends \IPS\File
return TRUE;
}
}
return FALSE;
}
@@ -155,19 +155,19 @@ class _Amazon extends \IPS\File
{
return \IPS\Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack('filehandler__Amazon'), $settings['bucket'] ) ) );
}
/* !File Handling */
/**
* @brief Does this storage method support chunked uploads?
*/
public static $supportsChunking = TRUE;
/**
* @brief Min chunk size (in MB)
*/
public static $minChunkSize = 5;
* @brief Min chunk size (in MB)
*/
public static $minChunkSize = 5;
/**
* @brief Max chunk size (in MB)
*/
@@ -202,7 +202,7 @@ class _Amazon extends \IPS\File
'size' => $this->filesize()
);
}
/**
* Fetch the gzip extensions specific for $this->configurationId
*
@@ -218,17 +218,17 @@ class _Amazon extends \IPS\File
{
$bits = explode( '_', $this->storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$storeGzipExtensions ) and \is_array( $class::$storeGzipExtensions ) and \count( $class::$storeGzipExtensions ) )
{
static::$gzipExtensions[ $this->storageExtension ] = $class::$storeGzipExtensions;
}
}
}
return $this->storageExtension ? static::$gzipExtensions[ $this->storageExtension ] : array();
}
/**
* Is this a private file?
* This means that it is PUT with bucket owner read-only permissions which means it needs a signed URL to download
@@ -243,17 +243,17 @@ class _Amazon extends \IPS\File
{
$bits = explode( '_', $this->storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$isPrivate ) )
{
return $class::$isPrivate;
}
}
}
return false;
}
/**
* AWS does not gzip content when serving it, so if we want gzip compressed JS and CSS, we need to store a copy ourselves.
*
@@ -264,7 +264,7 @@ class _Amazon extends \IPS\File
/* Use filename and not originalFilename so only true .js and .css files are checked, and not renamed uploads */
return \in_array( mb_substr( $this->filename, mb_strrpos( $this->filename, '.' ) + 1 ), $this->getGzipExtensions() );
}
/**
* Return the base URL
*
@@ -274,7 +274,7 @@ class _Amazon extends \IPS\File
{
return preg_replace( '#^http(s)?://#', '//', rtrim( ( empty( $this->configuration['custom_url'] ) ) ? static::buildBaseUrl( $this->configuration ) : $this->configuration['custom_url'], '/' ) );
}
/**
* Load File Data
*
@@ -283,14 +283,14 @@ class _Amazon extends \IPS\File
public function load()
{
parent::load();
/* Change the public URL to the gzipped version if the browser supports it */
if ( $this->needsGzipVersion() and ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) and \strpos( $_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip' ) !== false ) )
/* Change the public URL to the gzipped version if we force gzip versions of this file. */
if ( $this->needsGzipVersion() )
{
$this->url = new \IPS\Http\Url( (string) $this->url . '.gz' );
}
}
/**
* Save File
*
@@ -301,25 +301,28 @@ class _Amazon extends \IPS\File
$this->container = trim( $this->container, '/' );
$this->url = $this->baseUrl() . ( $this->container ? "/{$this->container}" : '' ) . "/{$this->filename}";
$path = $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}";
$response = static::makeRequest( $path, 'PUT', $this->configuration, $this->configurationId, (string) $this->contents(), $this->storageExtension, FALSE, $this->isPrivate() );
if ( $response->httpResponseCode != 200 )
{
throw new \IPS\File\Exception( $path, \IPS\File\Exception::CANNOT_WRITE, $this->originalFilename, $this->getExtraMessage( $response, $path ), $this->getErrorInformation( $response ) );
}
/* Write the gzip version */
if ( $this->needsGzipVersion() )
{
$response = static::makeRequest( "{$path}.gz", 'PUT', $this->configuration, $this->configurationId, gzencode( (string) $this->contents() ) );
if ( $response->httpResponseCode != 200 )
{
throw new \IPS\File\Exception( $path . '.gz', \IPS\File\Exception::CANNOT_WRITE, $this->originalFilename, $this->getExtraMessage( $response, $path . '.gz' ), $this->getErrorInformation( $response ) );
}
}
else
{
$response = static::makeRequest( $path, 'PUT', $this->configuration, $this->configurationId, (string) $this->contents(), $this->storageExtension, FALSE, $this->isPrivate() );
if ( $response->httpResponseCode != 200 )
{
throw new \IPS\File\Exception( $path, \IPS\File\Exception::CANNOT_WRITE, $this->originalFilename, $this->getExtraMessage( $response, $path ), $this->getErrorInformation( $response ) );
}
}
}
/**
* Get Contents
*
@@ -346,7 +349,7 @@ class _Amazon extends \IPS\File
}
return $this->contents;
}
/**
* Delete
*
@@ -356,33 +359,29 @@ class _Amazon extends \IPS\File
{
$this->container = trim( $this->container, '/' );
$path = $this->container ? "{$this->container}/{$this->filename}" : "{$this->filename}";
$debug = array_map( function( $row ) {
return array_filter( $row, function( $key ) {
return \in_array( $key, array( 'class', 'function', 'line' ) );
}, ARRAY_FILTER_USE_KEY );
}, debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ) );
try
{
$response = static::makeRequest( $path, 'DELETE', $this->configuration, $this->configurationId );
/* Got a gzip version? */
if ( $this->needsGzipVersion() )
{
$response = static::makeRequest( "{$path}.gz", 'DELETE', $this->configuration, $this->configurationId );
}
else
{
$response = static::makeRequest( $path, 'DELETE', $this->configuration, $this->configurationId );
}
/* Log deletion request */
$this->log( "file_deletion", 'delete', $debug, 'log' );
if ( $response->httpResponseCode == 204 )
{
/* Got a gzip version? */
if ( $this->needsGzipVersion() )
{
static::makeRequest( "{$path}.gz", 'DELETE', $this->configuration, $this->configurationId );
}
/* Ok */
return;
}
if ( $response->httpResponseCode != 200 )
if ( !\in_array( $response->httpResponseCode, array( 200, 204 ) ) )
{
$this->log( 'COULD_NOT_DELETE_FILE', 'delete', array( $response->httpResponseCode, $response->httpResponseText, $debug ), 'error' );
}
@@ -393,7 +392,7 @@ class _Amazon extends \IPS\File
$this->log( 'HTTP_ERROR_DELETE_FILE', 'delete', array( $e->getCode(), $e->getMessage(), $debug ), 'error' );
}
}
/**
* Delete Container
*
@@ -403,11 +402,11 @@ class _Amazon extends \IPS\File
public function deleteContainer( $container )
{
/* Add it the s3 table */
try
try
{
/* Is it already being deleted? */
\IPS\Db::i()->select( '*', 'core_s3_deletions', array( 's3_container=?', $container ) )->first();
/* Update the added time so we delete everything < that time() */
\IPS\Db::i()->update( 'core_s3_deletions', array( 's3_added' => time(), 's3_last_cycle' => 0, 's3_marker' => '' ), array( 's3_container=?', $container ) );
}
@@ -419,10 +418,10 @@ class _Amazon extends \IPS\File
's3_added' => time(),
's3_last_cycle' => 0
) );
/* Turn on task */
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 's3_delete' ) );
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 's3Delete' ) );
/* Log deletion request */
$realContainer = $this->container;
$this->container = $container;
@@ -430,7 +429,7 @@ class _Amazon extends \IPS\File
$this->container = $realContainer;
}
}
/**
* Return all keys that match the prefix
* @note Future me, you're welcome: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html
@@ -444,12 +443,12 @@ class _Amazon extends \IPS\File
public function getContainerKeys( $prefix = '', $limit = 50, $delimiter = '', $marker='' )
{
$_strip = array( '_strip_querystring' => TRUE, 'bucket_path' => NULL );
if ( $this->configuration['bucket_path'] )
{
$prefix = $this->configuration['bucket_path'] . '/' . $prefix;
}
if ( $marker and $this->configuration['bucket_path'] )
{
$marker = $this->configuration['bucket_path'] . '/' . $marker;
@@ -460,7 +459,7 @@ class _Amazon extends \IPS\File
/* Parse XML document */
$document = \IPS\Xml\SimpleXML::loadString( $response );
$keys = array();
if ( $delimiter )
{
/* Loop over dom document */
@@ -491,10 +490,10 @@ class _Amazon extends \IPS\File
}
}
}
return $keys;
}
/**
* Delete with a known key
* Bit of a cheeky bypass right into AWS S3 guts
@@ -508,25 +507,25 @@ class _Amazon extends \IPS\File
{
$key = $this->configuration['bucket_path'] . '/' . $key;
}
$_strip = array( '_strip_querystring' => TRUE, 'bucket_path' => NULL );
try
{
static::makeRequest( $key, 'DELETE', array_merge( $this->configuration, $_strip ), $this->configurationId );
\IPS\Db::i()->insert( 'core_file_logs', array (
'log_action' => 'delete',
'log_type' => 'log',
'log_configuration_id' => $this->configurationId,
'log_method' => static::$storageConfigurations[ $this->configurationId ]['method'],
'log_filename' => $key,
'log_url' => '',
'log_container' => '',
'log_msg' => 'file_deletion',
'log_date' => time(),
'log_data' => NULL
) );
'log_action' => 'delete',
'log_type' => 'log',
'log_configuration_id' => $this->configurationId,
'log_method' => static::$storageConfigurations[ $this->configurationId ]['method'],
'log_filename' => $key,
'log_url' => '',
'log_container' => '',
'log_msg' => 'file_deletion',
'log_date' => time(),
'log_data' => NULL
) );
}
catch( \IPS\Http\Request\Exception $e )
{
@@ -534,7 +533,7 @@ class _Amazon extends \IPS\File
$this->log( 'HTTP_ERROR_DELETE_FILE', 'delete', array( $e->getCode(), $e->getMessage() ), 'error' );
}
}
/**
* Initiate a chunked upload
*
@@ -561,11 +560,11 @@ class _Amazon extends \IPS\File
'original' => $this->originalFilename
);
}
/**
* Append more contents in a chunked upload
*
* @param mixed $ref The reference for this upload as returned by chunkInit()
* @param mixed $ref The reference for this upload as returned by chunkInit()
* @param string $temporaryFileOrContents The contents to write, or the path to the temporary file on disk with the contents
* @param int $chunkNumber Which chunk this is (0 for the first chunk, 1 for the second, etc)
* @param bool $isContents If TRUE, $temporaryFileOrContents is treated as raw contents. If FALSE, is path to file
@@ -575,14 +574,14 @@ class _Amazon extends \IPS\File
public function chunkProcess( $ref, $temporaryFileOrContents, $chunkNumber, $isContents = FALSE )
{
$r = static::makeRequest( $ref['path'], 'PUT', $this->configuration, $this->configurationId, $isContents ? $temporaryFileOrContents : file_get_contents( $temporaryFileOrContents ), NULL, FALSE, NULL, array( 'partNumber' => ++$chunkNumber, 'uploadId' => $ref['uploadId'] ) );
$ref['etags'][ $chunkNumber ] = trim( $r->httpHeaders['ETag'], '"' );
if ( !$isContents )
{
@unlink( $temporaryFileOrContents );
}
return $ref;
}
@@ -637,14 +636,14 @@ class _Amazon extends \IPS\File
{
return parent::filesize();
}
$this->_cachedFilesize = $response->httpHeaders['Content-Length'];
return $this->_cachedFilesize;
}
/* !Amazon Utility Methods */
/**
* Generate a temporary download URL the user can be redirected to
*
@@ -655,7 +654,7 @@ class _Amazon extends \IPS\File
{
$fileUrl = ( $this->container ? ( rawurlencode( $this->container ) . '/' . rawurlencode( $this->filename ) ) : rawurlencode( $this->filename ) );
$url = \IPS\Http\Url::external( static::buildBaseUrl( $this->configuration ) . $fileUrl );
$headers = array();
$queryString = array(
'X-Amz-Expires' => $validForSeconds,
@@ -664,27 +663,27 @@ class _Amazon extends \IPS\File
);
$signature = $this->signature( $this->configuration, 'GET', $fileUrl, $headers, $queryString, NULL, TRUE );
$queryString['X-Amz-Signature'] = $signature;
$url = $url->setQueryString( $queryString );
$response = $url->request()->head();
if ( $response->httpResponseCode == 400 )
{
{
$xml = $url->request()->get()->decodeXml();
if ( !isset( $xml->Region ) )
{
throw new \IPS\File\Exception( $fileUrl, \IPS\File\Exception::MISSING_REGION, $this->originalFilename );
}
$this->configuration['region'] = (string) $xml->Region;
\IPS\Db::i()->update( 'core_file_storage', array( 'configuration' => json_encode( $this->configuration ) ), array( 'id=?', $this->configurationId ) );
unset( \IPS\Data\Store::i()->storageConfigurations );
return $this->generateTemporaryDownloadUrl( $validForSeconds );
}
return $url;
}
/**
* Sign and make request
*
@@ -706,7 +705,7 @@ class _Amazon extends \IPS\File
$uriPieces = explode( '/', $uri );
$filename = array_pop( $uriPieces );
$uri = ltrim( implode( '/', $uriPieces ) . '/' . rawurlencode( $filename ), '/' );
/* Build a request */
$url = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri );
if ( $queryString )
@@ -714,13 +713,13 @@ class _Amazon extends \IPS\File
$url = $url->setQueryString( $queryString );
}
$request = $url->request( \IPS\VERY_LONG_REQUEST_TIMEOUT, NULL, FALSE ); # Amazon will send a 301 header code, but no Location header, if we need to try another endpoint
/* When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. @link http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html */
if ( \IPS\Request::i()->isSecure() and mb_strstr( $configuration['bucket'], '.' ) )
{
$request->sslCheck( FALSE );
}
/* Set headers. Make sure the file has the correct mime type, even if it is gzipped */
$mimeUri = ( mb_substr( $uri, -3 ) === '.gz' ) ? mb_substr( $uri, 0, -3 ) : $uri;
$headers = array(
@@ -754,23 +753,23 @@ class _Amazon extends \IPS\File
$cacheSeconds = $class::$cacheControlTtl;
}
}
$headers['Cache-Control'] = 'public, max-age=' . $cacheSeconds;
}
/* 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 );
}
/* Sign the request */
$authorization = static::signature( $configuration, $verb, $uri, $headers, $queryString, $content );
$headers['Authorization'] = $authorization;
unset( $headers['Host'] );
$request->setHeaders( $headers );
/* Make the request */
$verb = mb_strtolower( $verb );
$response = $request->$verb( $content );
@@ -780,7 +779,7 @@ class _Amazon extends \IPS\File
{
return $response;
}
/* Change endpoint if necessary */
if ( $response->httpResponseCode == 301 )
{
@@ -825,7 +824,7 @@ class _Amazon extends \IPS\File
return static::makeRequest( $uri, $verb, $configuration, $configurationId, $content );
}
}
/* Change region if necessary */
if ( $response->httpResponseCode == 400 )
{
@@ -847,9 +846,9 @@ class _Amazon extends \IPS\File
}
/* Return */
return $response;
return $response;
}
/**
* Generate a v4 signature
*
@@ -869,7 +868,7 @@ class _Amazon extends \IPS\File
$region = ( isset( $configuration['region'] ) ? $configuration['region'] : 'us-east-1' );
$scope = date( 'Ymd', $time ) . '/' . $region . '/s3/aws4_request';
$contentSha256 = ( $signatureIsForQueryString and !$content ) ? 'UNSIGNED-PAYLOAD' : hash( 'sha256', $content );
/* Figure out the canonical headers and query string */
if ( mb_strstr( $configuration['bucket'], '.' ) )
{
@@ -879,7 +878,7 @@ class _Amazon extends \IPS\File
{
$headers['Host'] = $configuration['bucket'] . '.' . ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" );
}
if ( $signatureIsForQueryString )
{
$queryString['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
@@ -912,7 +911,7 @@ class _Amazon extends \IPS\File
implode( ';', array_map( 'mb_strtolower', array_keys( $headers ) ) ),
$contentSha256
) );
/* Task 2: Create a String to Sign */
$stringToSign = implode( "\n", array(
'AWS4-HMAC-SHA256',
@@ -920,13 +919,13 @@ class _Amazon extends \IPS\File
$scope,
hash( 'sha256', $canonicalRequest )
) );
/* Task 3: Calculate Signature */
$dateKey = hash_hmac( 'sha256', date( 'Ymd', $time ), 'AWS4' . $configuration['secret_key'], true );
$dateRegionKey = hash_hmac( 'sha256', $region, $dateKey, true );
$dateRegionServiceKey = hash_hmac( 'sha256', 's3', $dateRegionKey, true );
$signingKey = hash_hmac( 'sha256', 'aws4_request', $dateRegionServiceKey, true );
/* Return */
$signature = hash_hmac( 'sha256', $stringToSign, $signingKey );
if ( $signatureIsForQueryString )
@@ -938,7 +937,7 @@ class _Amazon extends \IPS\File
return "AWS4-HMAC-SHA256 Credential={$configuration['access_key']}/{$scope},SignedHeaders=" . implode( ';', array_map( 'mb_strtolower', array_keys( $headers ) ) ) . ",Signature={$signature}";
}
}
/**
* Build up the base Amazon URL
* @param array $configuration Configuration data
@@ -949,22 +948,22 @@ class _Amazon extends \IPS\File
if ( mb_strstr( $configuration['bucket'], '.' ) )
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://"
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" )
. "/{$configuration['bucket']}"
. static::bucketPath( $configuration )
. '/';
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://"
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" )
. "/{$configuration['bucket']}"
. static::bucketPath( $configuration )
. '/';
}
else
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://{$configuration['bucket']}."
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" )
. static::bucketPath( $configuration )
. '/';
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://{$configuration['bucket']}."
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.amazonaws.com" )
. static::bucketPath( $configuration )
. '/';
}
}
/**
* Get bucket path
*
@@ -997,7 +996,7 @@ class _Amazon extends \IPS\File
'_done' => FALSE,
'fileIndex' => $fileIndex,
);
$checked = 0;
$skipped = 0;
$_strip = array( '_strip_querystring' => TRUE, 'bucket_path' => NULL );
@@ -1018,12 +1017,12 @@ class _Amazon extends \IPS\File
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 )
{
@@ -1033,7 +1032,7 @@ class _Amazon extends \IPS\File
continue 2;
}
}
/* If we are still here, the file was not valid. Delete and increment count. */
$this->logOrphanedFile( $result->Key );
@@ -1072,7 +1071,7 @@ class _Amazon extends \IPS\File
{
case 'RequestTimeTooSkewed':
return 's3-RequestTimeTooSkewed';
break;
break;
case 'AccessDenied':
case 'AccountProblem':
@@ -1080,21 +1079,21 @@ class _Amazon extends \IPS\File
case 'InvalidPayer':
case 'NotSignedUp':
return 's3-AccountProblem';
break;
break;
case 'EntityTooSmall':
case 'MissingRequestBodyError':
return 's3-FileSizeSmall';
break;
break;
case 'EntityTooLarge':
case 'MaxMessageLengthExceeded':
return 's3-FileSizeLarge';
break;
break;
default:
return 's3-GenericError';
break;
break;
}
}
}
+311
View File
@@ -0,0 +1,311 @@
<?php
/**
* @brief File Handler: Backblaze
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
* @copyright (c) Invision Power Services, Inc.
* @license https://www.invisioncommunity.com/legal/standards/
* @package Invision Community
* @since 10 Oct 2024
*/
namespace IPS\File;
/* Set use statements to make Uncle Matt happy */
use IPS\DateTime AS DT;
use IPS\File\Amazon;
use IPS\Http\Url;
use IPS\Http\Request\Exception as RequestException;
use IPS\Member;
use DateInterval;
use function array_filter;
use function array_map;
use function debug_backtrace;
use function defined;
use function header;
use function in_array;
use function mb_strstr;
use function trim;
use const ARRAY_FILTER_USE_KEY;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
/* 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;
}
class _Backblaze extends Amazon
{
/**
* 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 = parent::settings( $configuration );
unset( $return['toggle'], $return['custom_url'] );
$return['endpoint'] = array( 'type' => 'Text', 'default' => 's3.backblazeb2.com' );
$return['toggle'] = array( 'type' => 'YesNo', 'default' => $default, 'options' => array(
'togglesOn' => array( 'Backblaze_custom_url' )
) );
$return['custom_url'] = array( 'type' => 'Text', 'default' => '' );
return $return;
}
/**
* Display name
*
* @param array $settings Configuration settings
* @return string
*/
public static function displayName( $settings )
{
return Member::loggedIn()->language()->addToStack( 'filehandler_display_name', FALSE, array( 'sprintf' => array( Member::loggedIn()->language()->addToStack('filehandler__Backblaze'), $settings['bucket'] ) ) );
}
/**
* 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
* @param string|null $storageExtension Storage extension
* @param bool $skipExtraChecks Skips the endpoint check (to prevent infinite looping)
* @param bool $isPrivate This can be set to true to access/store private files (i.e. that are not publicly readable)
* @param array $queryString Array of key => value pairs
* @return \IPS\Http\Response
* @throws \IPS\Http\Request\Exception
*/
protected static function makeRequest( $uri, $verb, $configuration, $configurationId, $content=NULL, $storageExtension=NULL, $skipExtraChecks=FALSE, $isPrivate=false, $queryString=array() )
{
/* Amazon requires filename characters to be properly encoded - let's urlencode the filename here */
$uriPieces = explode( '/', $uri );
$filename = array_pop( $uriPieces );
$uri = ltrim( implode( '/', $uriPieces ) . '/' . rawurlencode( $filename ), '/' );
/* Build a request */
$url = \IPS\Http\Url::external( static::buildBaseUrl( $configuration ) . $uri );
if ( $queryString )
{
$url = $url->setQueryString( $queryString );
}
$request = $url->request( \IPS\VERY_LONG_REQUEST_TIMEOUT, NULL, FALSE ); # Amazon will send a 301 header code, but no Location header, if we need to try another endpoint
/* When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. @link http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html */
if ( \IPS\Request::i()->isSecure() and mb_strstr( $configuration['bucket'], '.' ) )
{
$request->sslCheck( FALSE );
}
/* Set headers */
$headers = array(
'Content-Type' => \IPS\File::getMimeType( $uri ),
'Content-MD5' => base64_encode( md5( $content, TRUE ) ),
);
/* If uploading a file, need to specify length and cache control */
if( mb_strtoupper( $verb ) === 'PUT' )
{
$headers['Content-Length'] = \strlen( $content );
$cacheSeconds = 3600 * 24 * 365;
/* Custom Cache-Control */
if( $storageExtension !== NULL AND mb_strpos( $storageExtension, '_' ) !== FALSE )
{
$bits = explode( '_', $storageExtension );
$class = '\IPS\\' . $bits[0] . '\extensions\core\FileStorage\\' . $bits[1];
if ( isset( $class::$cacheControlTtl ) and $class::$cacheControlTtl )
{
$cacheSeconds = $class::$cacheControlTtl;
}
}
$headers['Cache-Control'] = 'public, max-age=' . $cacheSeconds;
}
/* 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 );
}
/* Sign the request */
$authorization = static::signature( $configuration, $verb, $uri, $headers, $queryString, $content );
$headers['Authorization'] = $authorization;
unset( $headers['Host'] );
$request->setHeaders( $headers );
/* Make the request */
$verb = mb_strtolower( $verb );
$response = $request->$verb( $content );
/* If we are skipping extra checks, return response now */
if( $skipExtraChecks )
{
return $response;
}
/* Change endpoint if necessary */
if ( $response->httpResponseCode == 301 )
{
$xml = $response->decodeXml();
if ( isset( $xml->Endpoint ) )
{
/* We have an endpoint, but if we called s3.amazonaws.com then it might be wrong. Try to detect the correct one. */
$configuration['endpoint'] = 's3-us-west-1.amazonaws.com';
$endpointResponse = static::makeRequest( $uri, $verb, $configuration, $configurationId, $content, NULL, TRUE );
$update = FALSE;
/* If the response code is 200, we got lucky and that's our endpoint */
if( $endpointResponse->httpResponseCode == 200 )
{
$update = TRUE;
}
/* If it's a 301 response, we should be able to pull out the correct endpoint now */
elseif( $endpointResponse->httpResponseCode == 301 )
{
$xml = $endpointResponse->decodeXml();
if ( isset( $xml->Endpoint ) )
{
/* Strip out the bucket from the endpoint */
$configuration['endpoint'] = preg_replace( '/^' . preg_quote( $configuration['bucket'], '/' ) . '\./', '', (string) $xml->Endpoint );
$update = TRUE;
}
}
/* If we need to update, do it now and return the result */
if( $update === TRUE )
{
static::$updatedEndpoint = $configuration['endpoint'];
if ( $configurationId )
{
\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 );
}
}
/* Change region if necessary */
if ( $response->httpResponseCode == 400 )
{
try
{
$xml = $response->decodeXml();
if ( isset( $xml->Region ) )
{
$configuration['region'] = (string) $xml->Region;
if ( $configurationId )
{
\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 );
}
}
catch ( \Exception $e ) { }
}
/* Return */
return $response;
}
/**
* Generate a temporary download URL the user can be redirected to
*
* @param $validForSeconds int The number of seconds the link should be valid for
* @return Url
*/
public function generateTemporaryDownloadUrl( $validForSeconds = 1200 )
{
$expires = ( new DT )->add( new DateInterval( "PT{$validForSeconds}S" ) );
/* Authorize */
$authorize = Url::external( "https://api.backblazeb2.com/b2api/v3/b2_authorize_account" )
->request()
->login( $this->configuration['access_key'], $this->configuration['secret_key'] )
->get();
$response = $authorize->decodeJson();
if ( $authorize->httpResponseCode !== '200' )
{
throw new Exception( $response['code'] );
}
$download = URL::external( "{$response['apiInfo']['storageApi']['apiUrl']}/b2api/v3/get_download_authorization" )
->request()
->setHeaders( array(
'Authorization' => $response['authorizationToken']
) )
->post( json_encode( array(
'bucketId' => $response['apiInfo']['storageApi']['bucketId'],
'fileNamePrefix' => "{$this->container}/",
'validDurationInSeconds' => $expires->getTimestamp(),
'b2ContentDisposition' => 'attachment; filename="' . $this->originalFilename . '"',
'b2Expires' => $expires->rfc1123(),
'b2ContentType' => static::getMimeType( $this->originalFilename ),
) ) )
->decodeJson();
$query = http_build_query( array(
'Authorization' => $download['authorizationToken'],
'b2ContentDisposition' => 'attachment; filename="' . $this->originalFilename . '"',
'b2Expires' => $expires->rfc1123(),
'b2ContentType' => static::getMimeType( $this->originalFilename ),
) );
return Url::external( "{$response['apiInfo']['storageApi']['downloadUrl']}/file/{$this->configuration['bucket']}/{$this->container}/{$this->filename}?{$query}" );
}
/**
* Build up the base Amazon URL
* @param array $configuration Configuration data
* @return string
*/
public static function buildBaseUrl( $configuration )
{
if ( mb_strstr( $configuration['bucket'], '.' ) )
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://"
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.backblazeb2.com" )
. "/{$configuration['bucket']}"
. static::bucketPath( $configuration )
. '/';
}
else
{
return (
\IPS\Request::i()->isSecure() ? "https" : "http" ) . "://{$configuration['bucket']}."
. ( isset( $configuration['endpoint'] ) ? $configuration['endpoint'] : "s3.backblazeb2.com" )
. static::bucketPath( $configuration )
. '/';
}
}
/**
* Do not allow gzip versions for Backblaze
*
* @return boolean
*/
public function needsGzipVersion()
{
return false;
}
}
+15 -1
View File
@@ -36,9 +36,23 @@ abstract class _File
{
$return['FileSystem'] = 'IPS\\File\\FileSystem';
}
if ( !\IPS\CIC OR ( $current and $current['method'] === 'Database' ) )
{
$return['Database'] = 'IPS\\File\\Database';
}
$return['Amazon'] = 'IPS\\File\\Amazon';
$return['Database'] = 'IPS\\File\\Database';
if ( $current and $current['method'] === 'Backblaze' )
{
$return['Backblaze'] = 'IPS\\File\\Backblaze';
}
if ( \IPS\CIC AND file_exists( \IPS\ROOT_PATH . '/applications/cloud/sources/Media/Handler.php' ) )
{
$return['Cloud'] = 'IPS\\cloud\\Media\\Handler';
}
if ( $current and $current['method'] === 'Ftp' )
{