Merge branch 'master' into v2.2

This commit is contained in:
Andy Heathershaw 2019-09-14 15:40:14 +01:00
commit 6692c530e4
22 changed files with 1224 additions and 206 deletions

View File

@ -2,6 +2,7 @@
namespace App;
use App\AlbumSources\AlbumSourceBase;
use App\AlbumSources\IAlbumSource;
use App\AlbumSources\LocalFilesystemSource;
use App\Helpers\MiscHelper;
@ -158,10 +159,7 @@ class Album extends Model
*/
public function getAlbumSource()
{
$fullClassName = sprintf('App\AlbumSources\%s', $this->storage->source);
/** @var IAlbumSource $source */
$source = new $fullClassName;
$source = AlbumSourceBase::make($this->storage->source);
$source->setAlbum($this);
$source->setConfiguration($this->storage);

View File

@ -17,6 +17,31 @@ abstract class AlbumSourceBase
*/
protected $configuration;
/**
* @var mixed
*/
private static $albumSourceCache = [];
/**
* Makes an album source class for the given source name (relative class name.)
* @param string $sourceName Name of the source.
* @return IAlbumSource
*/
public static function make($sourceName)
{
$fullClassName = sprintf('App\AlbumSources\%s', $sourceName);
if (!array_key_exists($fullClassName, self::$albumSourceCache))
{
/** @var IAlbumSource $source */
$source = app($fullClassName);
self::$albumSourceCache[$fullClassName] = $source;
}
return self::$albumSourceCache[$fullClassName];
}
public function setAlbum(Album $album)
{
$this->album = $album;

View File

@ -0,0 +1,248 @@
<?php
namespace App\AlbumSources;
use App\BackblazeB2FileIdCache;
use App\Photo;
use App\Services\BackblazeB2Service;
use App\Storage;
use Guzzle\Http\EntityBody;
use Illuminate\Support\Facades\Log;
class BackblazeB2Source extends AlbumSourceBase implements IAlbumSource
{
const BUCKET_TYPE_AUTO = 0;
const BUCKET_TYPE_PRIVATE = 1;
const BUCKET_TYPE_PUBLIC = 2;
/**
* @var BackblazeB2Service
*/
private $backblaze;
/**
* Type of bucket which determines what type of URLs to generate to images.
* @var integer
*/
private $bucketType;
/**
* Token used to download files from a private bucket.
* @var string
*/
private $downloadToken;
/**
* Deletes an entire album's media contents.
* @return void
*/
public function deleteAlbumContents()
{
// No need to do anything for the album container - once the files are gone, the virtual folder is also gone
}
/**
* Deletes a thumbnail file for a photo.
* @param Photo $photo Photo to delete the thumbnail from.
* @param string $thumbnail Thumbnail to delete (or null to delete the original.)
* @return void
*/
public function deleteThumbnail(Photo $photo, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
// Create or update our cache record
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return;
}
$this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage);
$b2Cache->delete();
}
/**
* Fetches the contents of a thumbnail for a photo.
* @param Photo $photo Photo to fetch the thumbnail for.
* @param string $thumbnail Thumbnail to fetch (or null to fetch the original.)
* @return EntityBody
*/
public function fetchPhotoContent(Photo $photo, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
// First we need the file ID
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return EntityBody::fromString('');
}
return EntityBody::fromString(
$this->getClient()->downloadFile($b2Cache->b2_file_id)
);
}
/**
* Gets the name of this album source.
* @return string
*/
public function getName()
{
return 'global.album_sources.backblaze_b2';
}
/**
* Gets the absolute URL to the given photo file.
* @param Photo $photo Photo to get the URL to.
* @param string $thumbnail Thumbnail to get the image to.
* @return string
*/
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
{
$client = $this->getClient();
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
switch ($this->bucketType)
{
case self::BUCKET_TYPE_PRIVATE:
if (is_null($this->downloadToken))
{
$this->downloadToken = $client->getDownloadAuthToken();
}
// Once I sort out the issue with b2_download_file_by_id, this line can be removed
return sprintf('%s/file/%s/%s?Authorization=%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage, $this->downloadToken);
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (is_null($b2Cache))
{
return '';
}
return sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s&Authorization=%s', $client->getDownloadUrl(), urlencode($b2Cache->b2_file_id), urlencode($this->downloadToken));
case self::BUCKET_TYPE_PUBLIC:
/*
* From https://www.backblaze.com/b2/docs/b2_download_file_by_name.html:
* The base URL to use comes from the b2_authorize_account call, and looks something like
* https://f345.backblazeb2.com. The "f" in the URL stands for "file", and the number is the cluster
* number containing your account. To this base, you add "file/", your bucket name, a "/", and then the
* name of the file. The file name may itself include more "/" characters.
*/
return sprintf('%s/file/%s/%s', $client->getDownloadUrl(), $this->configuration->container_name, $pathOnStorage);
}
}
/**
* Saves a generated thumbnail to its permanent location.
* @param Photo $photo Photo the image relates to.
* @param string $tempFilename Filename containing the image.
* @param string $thumbnail Name of the thumbnail (or null for the original.)
* @return mixed
*/
public function saveThumbnail(Photo $photo, $tempFilename, $thumbnail = null)
{
$pathOnStorage = $this->getPathToPhoto($photo, $thumbnail);
$b2Cache = $this->getB2FileFromCache($pathOnStorage);
if (!is_null($b2Cache))
{
// Delete the current file version if we're replacing a file that already exists
$this->getClient()->deleteFile($b2Cache->b2_file_id, $pathOnStorage);
}
// Upload the file to B2
$b2FileID = $this->getClient()->uploadFile($tempFilename, $pathOnStorage);
// Create or update our cache record
if (is_null($b2Cache))
{
$b2Cache = new BackblazeB2FileIdCache([
'photo_id' => $photo->id,
'storage_path' => $pathOnStorage,
'b2_file_id' => $b2FileID
]);
}
else
{
$b2Cache->b2_file_id = $b2FileID;
}
$b2Cache->save();
}
public function setConfiguration(Storage $configuration)
{
parent::setConfiguration($configuration);
}
/**
* @param $pathOnStorage
* @return BackblazeB2FileIdCache|null
*/
private function getB2FileFromCache($pathOnStorage)
{
$b2Cache = BackblazeB2FileIdCache::where('storage_path', $pathOnStorage)->first();
if (is_null($b2Cache))
{
// TODO: lookup the file on B2 to get the file ID
Log::warning(sprintf('B2 file ID not found in cache: %s', $pathOnStorage));
return null;
}
return $b2Cache;
}
private function getClient()
{
if (is_null($this->backblaze))
{
$this->backblaze = new BackblazeB2Service();
$this->backblaze->setCredentials(decrypt($this->configuration->access_key), decrypt($this->configuration->secret_key));
$this->backblaze->authorizeAccount();
$this->backblaze->setBucketName($this->configuration->container_name);
if (intval($this->configuration->b2_bucket_type) == self::BUCKET_TYPE_AUTO)
{
/* Auto-detect the type of bucket in use on B2 */
switch ($this->backblaze->getBucketType())
{
case 'allPrivate':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PRIVATE;
break;
case 'allPublic':
$this->configuration->b2_bucket_type = self::BUCKET_TYPE_PUBLIC;
break;
}
$this->configuration->save();
}
// Set the bucket type
$this->bucketType = $this->configuration->b2_bucket_type;
}
return $this->backblaze;
}
private function getOriginalsFolder()
{
return '_originals';
}
private function getPathToPhoto(Photo $photo, $thumbnail = null)
{
return sprintf(
'%s/%s/%s',
$this->album->url_alias,
is_null($thumbnail) ? $this->getOriginalsFolder() : $thumbnail,
$photo->storage_file_name
);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class BackblazeB2FileIdCache extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'photo_id',
'storage_path',
'b2_file_id'
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Exceptions;
use Throwable;
class BackblazeRetryException extends \Exception
{
private $innerException;
/**
* @return mixed
*/
public function getInnerException()
{
return $this->innerException;
}
public function __construct($httpCode, \Exception $innerException)
{
parent::__construct('Backblaze requested to retry the request');
$this->innerException = $innerException;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Helpers;
use App\AlbumSources\AmazonS3Source;
use App\AlbumSources\BackblazeB2Source;
use App\AlbumSources\IAlbumSource;
use App\AlbumSources\LocalFilesystemSource;
use App\AlbumSources\OpenStackSource;
@ -46,6 +47,7 @@ class ConfigHelper
$classes = [
LocalFilesystemSource::class,
AmazonS3Source::class,
BackblazeB2Source::class,
OpenStackSource::class,
RackspaceSource::class
];

View File

@ -88,7 +88,8 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
]));
$storage->is_active = true;
$storage->is_default = (strtolower($request->get('is_default')) == 'on');
@ -217,7 +218,8 @@ class StorageController extends Controller
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
]));
$storage->is_active = (strtolower($request->get('is_active')) == 'on');
$storage->is_default = (strtolower($request->get('is_default')) == 'on');

View File

@ -0,0 +1,400 @@
<?php
namespace App\Services;
use App\Exceptions\BackblazeRetryException;
use Illuminate\Support\Facades\Log;
class BackblazeB2Service
{
/**
* The individual URL for the account to use to access the API
* @var string
*/
private $accountApiUrl;
/**
* ID of the account in Backblaze B2.
* @var string
*/
private $accountID;
/**
* The base URL for public access to the account's files.
* @var string
*/
private $downloadUrl;
/**
* Authorisation header for authenticating to the API.
* @var string
*/
private $authHeader;
/**
* Authorisation token for accessing the API post-authentication.
* @var string
*/
private $authToken;
/**
* ID of the bucket.
* @var string
*/
private $bucketId;
/**
* Type of the bucket.
* @var integer
*/
private $bucketType;
/**
* Configuration related to the Backblaze B2 service.
* @var \Illuminate\Config\Repository|mixed
*/
private $config;
/**
* Current file upload token.
* @var string
*/
private $uploadAuthToken;
/**
* Current upload URL.
* @var string
*/
private $uploadUrl;
public function __construct()
{
$this->config = config('services.backblaze_b2');
}
public function authorizeAccount($force = false)
{
if (empty($this->authToken) || $force)
{
$result = $this->sendRequest($this->config['auth_url']);
if (!isset($result->authorizationToken))
{
throw new \Exception('Authorisation to Backblaze failed. Is the API key correct?');
}
$this->authToken = $result->authorizationToken;
$this->accountApiUrl = $result->apiUrl;
$this->accountID = $result->accountId;
$this->downloadUrl = $result->downloadUrl;
}
}
public function deleteFile($fileID, $fileName)
{
$this->sendRequest(
sprintf('%s/b2api/v2/b2_delete_file_version', $this->accountApiUrl),
'POST',
[
'fileId' => $fileID,
'fileName' => $fileName
]
);
}
public function downloadFile($fileID)
{
return $this->sendRequest(
sprintf('%s/b2api/v2/b2_download_file_by_id?fileId=%s', $this->accountApiUrl, urlencode($fileID)),
'GET',
null,
[
'http_headers' => [
sprintf('Authorization: %s', $this->authToken)
],
'response_body_is_json' => false
]
);
}
public function getBucketType()
{
return $this->bucketType;
}
public function getDownloadAuthToken()
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_get_download_authorization', $this->accountApiUrl),
'POST',
[
'bucketId' => $this->bucketId,
'validDurationInSeconds' => intval($this->config['download_token_lifetime']),
'fileNamePrefix' => ''
]
);
return $result->authorizationToken;
}
public function getDownloadUrl()
{
return $this->downloadUrl;
}
public function setBucketName($bucketName)
{
$bucketDetails = $this->getBucketDetailsFromName($bucketName);
$this->bucketId = $bucketDetails->bucketId;
$this->bucketType = $bucketDetails->bucketType;
}
public function setCredentials($applicationKeyID, $applicationKey)
{
$this->authHeader = sprintf('%s:%s', $applicationKeyID, $applicationKey);
}
public function uploadFile($pathToFileToUpload, $pathToStorage)
{
// Get a URL to upload our file to
list($uploadUrl, $authorizationToken) = $this->getUploadUrl();
if (empty($uploadUrl) || empty($authorizationToken))
{
throw new \Exception('No upload URL/authorization token returned from Backblaze B2.');
}
$exponentialBackoff = 1;
$numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16)
$numberOfTimesTried = 0;
while ($numberOfTimesTried < $numberOfRetries)
{
try
{
return $this->uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken);
}
catch (BackblazeRetryException $ex)
{
sleep($exponentialBackoff);
// Get a new upload token
$this->uploadAuthToken = null;
$this->uploadUrl = null;
list($uploadUrl, $authorizationToken) = $this->getUploadUrl();
// Keep backing off
$exponentialBackoff *= $exponentialBackoff;
$numberOfTimesTried++;
}
}
}
private function uploadFileReal($pathToFileToUpload, $pathToStorage, $uploadUrl, $authorizationToken)
{
$fileSize = filesize($pathToFileToUpload);
$handle = fopen($pathToFileToUpload, 'r');
$fileContents = fread($handle, $fileSize);
fclose($handle);
$fileContentsSha1 = sha1_file($pathToFileToUpload);
$httpHeaders = [
sprintf('Authorization: %s', $authorizationToken),
'Content-Type: b2/x-auto',
sprintf('X-Bz-Content-Sha1: %s', $fileContentsSha1),
sprintf('X-Bz-File-Name: %s', urlencode($pathToStorage))
];
$result = $this->sendRequestReal(
$uploadUrl,
'POST',
$fileContents,
[
'http_headers' => $httpHeaders,
'post_body_is_json' => false
]
);
return $result->fileId;
}
private function getBucketDetailsFromName($bucketName)
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_list_buckets', $this->accountApiUrl),
'POST',
[
'accountId' => $this->accountID,
'bucketName' => $bucketName
]
);
if (isset($result->buckets) && is_array($result->buckets) && count($result->buckets) >= 1)
{
return $result->buckets[0];
}
throw new \Exception(sprintf('The bucket \'%s\' was not found or your API key does not have access.', $bucketName));
}
private function getUploadUrl($alwaysGetNewToken = false)
{
if (is_null($this->uploadAuthToken) || $alwaysGetNewToken)
{
$result = $this->sendRequest(
sprintf('%s/b2api/v2/b2_get_upload_url', $this->accountApiUrl),
'POST',
['bucketId' => $this->bucketId]
);
$this->uploadAuthToken = $result->authorizationToken;
$this->uploadUrl = $result->uploadUrl;
}
return [$this->uploadUrl, $this->uploadAuthToken];
}
private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = [])
{
$httpHeaders = array_merge(
[
'Accept: application/json'
],
$httpHeaders
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
switch (strtoupper($method))
{
case 'GET':
curl_setopt($ch, CURLOPT_HTTPGET, true);
break;
case 'POST':
curl_setopt($ch, CURLOPT_POST, true);
break;
}
return $ch;
}
private function sendRequest($url, $method = 'GET', $postData = null, array $postOptions = [])
{
$exponentialBackoff = 1;
$numberOfRetries = 5; // this effectively gives us 31 seconds of retries (1+2+4+8+16)
$numberOfTimesTried = 0;
while ($numberOfTimesTried < $numberOfRetries)
{
try
{
return $this->sendRequestReal($url, $method, $postData, $postOptions);
}
catch (BackblazeRetryException $ex)
{
// Clear the upload token if requested
if (isset($postOptions['clear_upload_token_on_retry']) && $postOptions['clear_upload_token_on_retry'])
{
$this->uploadAuthToken = null;
$this->uploadUrl = null;
}
// Keep backing off
sleep($exponentialBackoff);
$exponentialBackoff *= $exponentialBackoff;
$numberOfTimesTried++;
}
}
}
private function sendRequestReal($url, $method = 'GET', $postData = null, array $postOptions = [])
{
$postOptions = array_merge(
[
'authorization_token' => null,
'http_headers' => [],
'post_body_is_json' => true,
'response_body_is_json' => true
],
$postOptions
);
$httpHeaders = $postOptions['http_headers'];
// Some methods may need to override the authorization token used
if (empty($postOptions['authorization_token']))
{
// No override - work out which auth token to use
if (is_null($this->authToken))
{
// No auth token yet, use username/password
$httpHeaders[] = sprintf('Authorization: Basic %s', base64_encode($this->authHeader));
}
else
{
// Use the auth token we have
$httpHeaders[] = sprintf('Authorization: %s', $this->authToken);
}
}
else
{
// Override - use the auth token specified
$httpHeaders[] = sprintf('Authorization: %s', $postOptions['authorization_token']);
}
$ch = $this->getBasicHttpClient($url, $method, $httpHeaders);
if (!is_null($postData))
{
if ($postOptions['post_body_is_json'])
{
$postData = json_encode($postData);
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
}
Log::info(sprintf('%s: %s', strtoupper($method), $url));
Log::debug('HTTP headers:', $httpHeaders);
// Only log a post body if we have one and it's in JSON format (i.e. not a file upload)
if (!is_null($postData) && $postOptions['post_body_is_json'])
{
Log::debug($postData);
}
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Log::info(sprintf('Received HTTP code %d', $httpCode));
// Only log a result if we have one and it's in JSON format (i.e. not a file download)
if (!is_null($result) && $result !== false && $postOptions['response_body_is_json'])
{
Log::debug($result);
}
// According to the Backblaze B2 Protocol, if we get a 500/503, we should retry the request
if ($httpCode == 500 || $httpCode == 503)
{
throw new BackblazeRetryException(
$httpCode,
new \Exception(sprintf('Exception from Backblaze B2: %s', $result))
);
}
else if ($httpCode != 200 && $httpCode != 304)
{
throw new \Exception(sprintf('Exception from Backblaze B2: %s', $result));
}
curl_close($ch);
return $postOptions['response_body_is_json']
? json_decode($result)
: $result;
}
}

View File

@ -30,7 +30,8 @@ class Storage extends Model
'container_name',
'cdn_url',
'access_key',
'secret_key'
'secret_key',
'b2_bucket_type'
];
public function albums()

View File

@ -6,6 +6,7 @@
"type": "project",
"require": {
"php": ">=7.0.0",
"ext-curl": "*",
"ext-json": "*",
"laravel/framework": "5.5.*",
"rackspace/php-opencloud": "^1.16",

380
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "359fa33910865037863ae6ae724af956",
"content-hash": "53d67647c5a4d0d450470522c903f745",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.105.0",
"version": "3.111.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "3a1159eeb14f707780817bebf73dd3eeeeb710cc"
"reference": "a31376012346118b2b88df6d2f0c185af71e3096"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3a1159eeb14f707780817bebf73dd3eeeeb710cc",
"reference": "3a1159eeb14f707780817bebf73dd3eeeeb710cc",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a31376012346118b2b88df6d2f0c185af71e3096",
"reference": "a31376012346118b2b88df6d2f0c185af71e3096",
"shasum": ""
},
"require": {
@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2019-07-08T18:16:57+00:00"
"time": "2019-09-09T18:13:28+00:00"
},
{
"name": "doctrine/cache",
@ -389,28 +389,30 @@
},
{
"name": "doctrine/lexer",
"version": "1.0.2",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8"
"reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8",
"reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
"reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
"php": "^7.2"
},
"require-dev": {
"phpunit/phpunit": "^4.5"
"doctrine/coding-standard": "^6.0",
"phpstan/phpstan": "^0.11.8",
"phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
@ -423,14 +425,14 @@
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
@ -445,20 +447,20 @@
"parser",
"php"
],
"time": "2019-06-08T11:03:04+00:00"
"time": "2019-07-30T19:33:28+00:00"
},
{
"name": "egulias/email-validator",
"version": "2.1.9",
"version": "2.1.11",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25"
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/128cc721d771ec2c46ce59698f4ca42b73f71b25",
"reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
"reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
"shasum": ""
},
"require": {
@ -468,7 +470,8 @@
"require-dev": {
"dominicsayers/isemail": "dev-master",
"phpunit/phpunit": "^4.8.35||^5.7||^6.0",
"satooshi/php-coveralls": "^1.0.1"
"satooshi/php-coveralls": "^1.0.1",
"symfony/phpunit-bridge": "^4.4@dev"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
@ -476,7 +479,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
"dev-master": "2.1.x-dev"
}
},
"autoload": {
@ -502,7 +505,7 @@
"validation",
"validator"
],
"time": "2019-06-23T10:14:27+00:00"
"time": "2019-08-13T17:33:27+00:00"
},
{
"name": "erusev/parsedown",
@ -835,25 +838,25 @@
},
{
"name": "kylekatarnls/update-helper",
"version": "1.1.1",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/kylekatarnls/update-helper.git",
"reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9"
"reference": "5786fa188e0361b9adf9e8199d7280d1b2db165e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/b34a46d7f5ec1795b4a15ac9d46b884377262df9",
"reference": "b34a46d7f5ec1795b4a15ac9d46b884377262df9",
"url": "https://api.github.com/repos/kylekatarnls/update-helper/zipball/5786fa188e0361b9adf9e8199d7280d1b2db165e",
"reference": "5786fa188e0361b9adf9e8199d7280d1b2db165e",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1.0",
"composer-plugin-api": "^1.1.0 || ^2.0.0",
"php": ">=5.3.0"
},
"require-dev": {
"codeclimate/php-test-reporter": "dev-master",
"composer/composer": "^2.0.x-dev",
"composer/composer": "2.0.x-dev || ^2.0.0-dev",
"phpunit/phpunit": ">=4.8.35 <6.0"
},
"type": "composer-plugin",
@ -876,20 +879,20 @@
}
],
"description": "Update helper",
"time": "2019-06-05T08:34:23+00:00"
"time": "2019-07-29T11:03:54+00:00"
},
{
"name": "laravel/framework",
"version": "v5.5.45",
"version": "v5.5.48",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "52c79ecf54b6168a54730ccb6c4c9f3561732a80"
"reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/52c79ecf54b6168a54730ccb6c4c9f3561732a80",
"reference": "52c79ecf54b6168a54730ccb6c4c9f3561732a80",
"url": "https://api.github.com/repos/laravel/framework/zipball/e3e8d585dcfab5abe6261b060f4df0d48f9924bf",
"reference": "e3e8d585dcfab5abe6261b060f4df0d48f9924bf",
"shasum": ""
},
"require": {
@ -1010,7 +1013,7 @@
"framework",
"laravel"
],
"time": "2019-01-28T20:53:19+00:00"
"time": "2019-08-20T15:46:40+00:00"
},
{
"name": "laravel/socialite",
@ -1077,16 +1080,16 @@
},
{
"name": "league/flysystem",
"version": "1.0.53",
"version": "1.0.55",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "08e12b7628f035600634a5e76d95b5eb66cea674"
"reference": "33c91155537c6dc899eacdc54a13ac6303f156e6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/08e12b7628f035600634a5e76d95b5eb66cea674",
"reference": "08e12b7628f035600634a5e76d95b5eb66cea674",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6",
"reference": "33c91155537c6dc899eacdc54a13ac6303f156e6",
"shasum": ""
},
"require": {
@ -1157,7 +1160,7 @@
"sftp",
"storage"
],
"time": "2019-06-18T20:09:29+00:00"
"time": "2019-08-24T11:17:19+00:00"
},
{
"name": "league/oauth1-client",
@ -1251,16 +1254,16 @@
},
{
"name": "monolog/monolog",
"version": "1.24.0",
"version": "1.25.1",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
"reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
"reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
"shasum": ""
},
"require": {
@ -1325,7 +1328,7 @@
"logging",
"psr-3"
],
"time": "2018-11-05T09:00:11+00:00"
"time": "2019-09-06T13:49:17+00:00"
},
{
"name": "mtdowling/cron-expression",
@ -1534,22 +1537,22 @@
},
{
"name": "php-amqplib/php-amqplib",
"version": "v2.9.2",
"version": "v2.10.0",
"source": {
"type": "git",
"url": "https://github.com/php-amqplib/php-amqplib.git",
"reference": "76faddcd668dabb8d4f7c00e86b8a9decd781a59"
"reference": "04e5366f032906d5f716890427e425e71307d3a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/76faddcd668dabb8d4f7c00e86b8a9decd781a59",
"reference": "76faddcd668dabb8d4f7c00e86b8a9decd781a59",
"url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/04e5366f032906d5f716890427e425e71307d3a8",
"reference": "04e5366f032906d5f716890427e425e71307d3a8",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"ext-sockets": "*",
"php": ">=5.4.0"
"php": ">=5.6"
},
"replace": {
"videlalvaro/php-amqplib": "self.version"
@ -1557,14 +1560,14 @@
"require-dev": {
"ext-curl": "*",
"nategood/httpful": "^0.2.20",
"phpdocumentor/phpdocumentor": "^2.9",
"phpunit/phpunit": "^4.8",
"phpdocumentor/phpdocumentor": "dev-master",
"phpunit/phpunit": "^5.7|^6.5|^7.0",
"squizlabs/php_codesniffer": "^2.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
"dev-master": "2.10-dev"
}
},
"autoload": {
@ -1583,18 +1586,18 @@
},
{
"name": "John Kelly",
"email": "johnmkelly86@gmail.com",
"role": "Maintainer"
"role": "Maintainer",
"email": "johnmkelly86@gmail.com"
},
{
"name": "Raúl Araya",
"email": "nubeiro@gmail.com",
"role": "Maintainer"
"role": "Maintainer",
"email": "nubeiro@gmail.com"
},
{
"name": "Luke Bakken",
"email": "luke@bakken.io",
"role": "Maintainer"
"role": "Maintainer",
"email": "luke@bakken.io"
}
],
"description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.",
@ -1604,7 +1607,7 @@
"queue",
"rabbitmq"
],
"time": "2019-04-24T15:36:21+00:00"
"time": "2019-08-08T18:28:18+00:00"
},
{
"name": "psr/container",
@ -2043,16 +2046,16 @@
},
{
"name": "symfony/console",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c4d2f3529755ffc0be9fb823583b28d8744eeb3d"
"reference": "4510f04e70344d70952566e4262a0b11df39cb10"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c4d2f3529755ffc0be9fb823583b28d8744eeb3d",
"reference": "c4d2f3529755ffc0be9fb823583b28d8744eeb3d",
"url": "https://api.github.com/repos/symfony/console/zipball/4510f04e70344d70952566e4262a0b11df39cb10",
"reference": "4510f04e70344d70952566e4262a0b11df39cb10",
"shasum": ""
},
"require": {
@ -2111,7 +2114,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2019-06-05T11:33:52+00:00"
"time": "2019-08-26T07:52:58+00:00"
},
{
"name": "symfony/css-selector",
@ -2168,16 +2171,16 @@
},
{
"name": "symfony/debug",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "1172dc1abe44dfadd162239153818b074e6e53bf"
"reference": "0b600300918780001e2821db77bc28b677794486"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/1172dc1abe44dfadd162239153818b074e6e53bf",
"reference": "1172dc1abe44dfadd162239153818b074e6e53bf",
"url": "https://api.github.com/repos/symfony/debug/zipball/0b600300918780001e2821db77bc28b677794486",
"reference": "0b600300918780001e2821db77bc28b677794486",
"shasum": ""
},
"require": {
@ -2220,7 +2223,7 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2019-06-18T21:26:03+00:00"
"time": "2019-08-20T13:31:17+00:00"
},
{
"name": "symfony/event-dispatcher",
@ -2284,16 +2287,16 @@
},
{
"name": "symfony/finder",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "5f80266a729e30bbcc37f8bf0e62c3d5a38c8208"
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/5f80266a729e30bbcc37f8bf0e62c3d5a38c8208",
"reference": "5f80266a729e30bbcc37f8bf0e62c3d5a38c8208",
"url": "https://api.github.com/repos/symfony/finder/zipball/1fcad80b440abcd1451767349906b6f9d3961d37",
"reference": "1fcad80b440abcd1451767349906b6f9d3961d37",
"shasum": ""
},
"require": {
@ -2329,20 +2332,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2019-05-30T15:47:52+00:00"
"time": "2019-08-14T09:39:58+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "8cfbf75bb3a72963b12c513a73e9247891df24f8"
"reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/8cfbf75bb3a72963b12c513a73e9247891df24f8",
"reference": "8cfbf75bb3a72963b12c513a73e9247891df24f8",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/b3d57a1c325f39f703b249bed7998ce8c64236b4",
"reference": "b3d57a1c325f39f703b249bed7998ce8c64236b4",
"shasum": ""
},
"require": {
@ -2383,20 +2386,20 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2019-06-22T20:10:25+00:00"
"time": "2019-08-26T07:50:50+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2"
"reference": "f6d35bb306b26812df007525f5757a8b0e95857e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2",
"reference": "abbb38dbab652ddc40a86d0c3b0e14ca52d58ed2",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6d35bb306b26812df007525f5757a8b0e95857e",
"reference": "f6d35bb306b26812df007525f5757a8b0e95857e",
"shasum": ""
},
"require": {
@ -2472,20 +2475,20 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
"time": "2019-06-26T13:56:39+00:00"
"time": "2019-08-26T16:36:29+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "82ebae02209c21113908c229e9883c419720738a"
"reference": "550ebaac289296ce228a706d0867afc34687e3f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a",
"reference": "82ebae02209c21113908c229e9883c419720738a",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
"reference": "550ebaac289296ce228a706d0867afc34687e3f4",
"shasum": ""
},
"require": {
@ -2497,7 +2500,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2513,13 +2516,13 @@
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
@ -2530,20 +2533,20 @@
"polyfill",
"portable"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7"
"reference": "685968b11e61a347c18bf25db32effa478be610f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f037ea22acfaee983e271dd9c3b8bb4150bd8ad7",
"reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
"reference": "685968b11e61a347c18bf25db32effa478be610f",
"shasum": ""
},
"require": {
@ -2555,7 +2558,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2589,20 +2592,20 @@
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af"
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af",
"reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
"reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
"shasum": ""
},
"require": {
@ -2616,7 +2619,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2632,13 +2635,13 @@
"MIT"
],
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
},
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
@ -2651,20 +2654,20 @@
"portable",
"shim"
],
"time": "2019-03-04T13:44:35+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609"
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609",
"reference": "fe5e94c604826c35a32fa832f35bd036b6799609",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
"reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
"shasum": ""
},
"require": {
@ -2676,7 +2679,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2710,20 +2713,20 @@
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-php70",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php70.git",
"reference": "bc4858fb611bda58719124ca079baff854149c89"
"reference": "54b4c428a0054e254223797d2713c31e08610831"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/bc4858fb611bda58719124ca079baff854149c89",
"reference": "bc4858fb611bda58719124ca079baff854149c89",
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/54b4c428a0054e254223797d2713c31e08610831",
"reference": "54b4c428a0054e254223797d2713c31e08610831",
"shasum": ""
},
"require": {
@ -2733,7 +2736,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2769,20 +2772,20 @@
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.11.0",
"version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c"
"reference": "04ce3335667451138df4307d6a9b61565560199e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c",
"reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
"reference": "04ce3335667451138df4307d6a9b61565560199e",
"shasum": ""
},
"require": {
@ -2791,7 +2794,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.11-dev"
"dev-master": "1.12-dev"
}
},
"autoload": {
@ -2824,20 +2827,20 @@
"portable",
"shim"
],
"time": "2019-02-06T07:57:58+00:00"
"time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/process",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "d129c017e8602507688ef2c3007951a16c1a8407"
"reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/d129c017e8602507688ef2c3007951a16c1a8407",
"reference": "d129c017e8602507688ef2c3007951a16c1a8407",
"url": "https://api.github.com/repos/symfony/process/zipball/d822cb654000a95b7855362c0d5b127f6a6d8baa",
"reference": "d822cb654000a95b7855362c0d5b127f6a6d8baa",
"shasum": ""
},
"require": {
@ -2873,20 +2876,20 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2019-05-30T15:47:52+00:00"
"time": "2019-08-26T07:52:58+00:00"
},
{
"name": "symfony/routing",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "8d804d8a65a26dc9de1aaf2ff3a421e581d050e6"
"reference": "8b0faa681c4ee14701e76a7056fef15ac5384163"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/8d804d8a65a26dc9de1aaf2ff3a421e581d050e6",
"reference": "8d804d8a65a26dc9de1aaf2ff3a421e581d050e6",
"url": "https://api.github.com/repos/symfony/routing/zipball/8b0faa681c4ee14701e76a7056fef15ac5384163",
"reference": "8b0faa681c4ee14701e76a7056fef15ac5384163",
"shasum": ""
},
"require": {
@ -2949,26 +2952,26 @@
"uri",
"url"
],
"time": "2019-06-26T11:14:13+00:00"
"time": "2019-08-26T07:50:50+00:00"
},
{
"name": "symfony/translation",
"version": "v4.3.2",
"version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "934ab1d18545149e012aa898cf02e9f23790f7a0"
"reference": "28498169dd334095fa981827992f3a24d50fed0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/934ab1d18545149e012aa898cf02e9f23790f7a0",
"reference": "934ab1d18545149e012aa898cf02e9f23790f7a0",
"url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f",
"reference": "28498169dd334095fa981827992f3a24d50fed0f",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^1.1.2"
"symfony/translation-contracts": "^1.1.6"
},
"conflict": {
"symfony/config": "<3.4",
@ -3025,20 +3028,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2019-06-13T11:03:18+00:00"
"time": "2019-08-26T08:55:16+00:00"
},
{
"name": "symfony/translation-contracts",
"version": "v1.1.5",
"version": "v1.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c"
"reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/cb4b18ad7b92a26e83b65dde940fab78339e6f3c",
"reference": "cb4b18ad7b92a26e83b65dde940fab78339e6f3c",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
"reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
"shasum": ""
},
"require": {
@ -3082,20 +3085,20 @@
"interoperability",
"standards"
],
"time": "2019-06-13T11:15:36+00:00"
"time": "2019-08-02T12:15:04+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v3.4.29",
"version": "v3.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "7b92618169c44af4bb226f69dbac42b56b1a7745"
"reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/7b92618169c44af4bb226f69dbac42b56b1a7745",
"reference": "7b92618169c44af4bb226f69dbac42b56b1a7745",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/5408ad7194737ee1bc5ab7a9683fb6925f92c3e4",
"reference": "5408ad7194737ee1bc5ab7a9683fb6925f92c3e4",
"shasum": ""
},
"require": {
@ -3151,7 +3154,7 @@
"debug",
"dump"
],
"time": "2019-06-13T16:26:35+00:00"
"time": "2019-08-26T07:50:50+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@ -3311,16 +3314,16 @@
},
{
"name": "filp/whoops",
"version": "2.4.1",
"version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "6fb502c23885701a991b0bba974b1a8eb6673577"
"reference": "cde50e6720a39fdacb240159d3eea6865d51fd96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/6fb502c23885701a991b0bba974b1a8eb6673577",
"reference": "6fb502c23885701a991b0bba974b1a8eb6673577",
"url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96",
"reference": "cde50e6720a39fdacb240159d3eea6865d51fd96",
"shasum": ""
},
"require": {
@ -3368,7 +3371,7 @@
"throwable",
"whoops"
],
"time": "2019-07-04T09:00:00+00:00"
"time": "2019-08-07T09:00:00+00:00"
},
{
"name": "fzaninotto/faker",
@ -3532,16 +3535,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.9.1",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72"
"reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
"reference": "e6828efaba2c9b79f4499dae1d66ef8bfa7b2b72",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
"reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
"shasum": ""
},
"require": {
@ -3576,7 +3579,7 @@
"object",
"object graph"
],
"time": "2019-04-07T13:18:21+00:00"
"time": "2019-08-09T12:45:53+00:00"
},
{
"name": "phar-io/manifest",
@ -4501,16 +4504,16 @@
},
{
"name": "sebastian/exporter",
"version": "3.1.0",
"version": "3.1.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
"reference": "06a9a5947f47b3029d76118eb5c22802e5869687"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
"reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687",
"reference": "06a9a5947f47b3029d76118eb5c22802e5869687",
"shasum": ""
},
"require": {
@ -4537,6 +4540,10 @@
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@ -4545,17 +4552,13 @@
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
},
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
},
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
@ -4564,7 +4567,7 @@
"export",
"exporter"
],
"time": "2017-04-03T13:19:02+00:00"
"time": "2019-08-11T12:43:14+00:00"
},
{
"name": "sebastian/global-state",
@ -4945,16 +4948,16 @@
},
{
"name": "webmozart/assert",
"version": "1.4.0",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
"reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
"reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
"shasum": ""
},
"require": {
@ -4962,8 +4965,7 @@
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
"sebastian/version": "^1.0.1"
"phpunit/phpunit": "^4.8.36 || ^7.5.13"
},
"type": "library",
"extra": {
@ -4992,7 +4994,7 @@
"check",
"validate"
],
"time": "2018-12-25T11:19:39+00:00"
"time": "2019-08-24T08:43:50+00:00"
}
],
"aliases": [],
@ -5001,7 +5003,9 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.0.0"
"php": ">=7.0.0",
"ext-curl": "*",
"ext-json": "*"
},
"platform-dev": []
}

View File

@ -14,6 +14,11 @@ return [
|
*/
'backblaze_b2' => [
'auth_url' => 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account',
'download_token_lifetime' => 300
],
'gitea' => [
'api_url' => 'https://apps.andysh.uk/api/v1',
'cache_time_seconds' => 3600,

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddBackblazeStorageColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('storages', function (Blueprint $table)
{
$table->tinyInteger('b2_bucket_type')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('storages', function (Blueprint $table)
{
$table->dropColumn('b2_bucket_type');
});
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBackblazeB2FileIdCachesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('backblaze_b2_file_id_caches', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('photo_id');
$table->string('storage_path');
$table->string('b2_file_id');
$table->timestamps();
$table->foreign('photo_id')
->references('id')->on('photos')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('backblaze_b2_file_id_caches');
}
}

143
public/b2_test.php Normal file
View File

@ -0,0 +1,143 @@
<?php
function b2_authorize_account()
{
$application_key_id = "0023254ec9bda08000000000a"; // Obtained from your B2 account page
$application_key = "K002eARNPUlxdj1XaVJbwEYPMz0c7e8"; // Obtained from your B2 account page
$credentials = base64_encode($application_key_id . ":" . $application_key);
$url = "https://api.backblazeb2.com/b2api/v2/b2_authorize_account";
$session = curl_init($url);
// Add headers
$headers = array();
$headers[] = "Accept: application/json";
$headers[] = "Authorization: Basic " . $credentials;
curl_setopt($session, CURLOPT_HTTPHEADER, $headers); // Add headers
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
$server_output = curl_exec($session);
curl_close ($session);
echo ($server_output);
return json_decode($server_output);
}
function b2_download_file_by_id($download_url, $auth_token)
{
//$download_url = ""; // From b2_authorize_account call
$file_id = "4_z731245f41efc196b6dda0018_f116729ca6de74b38_d20190910_m132847_c002_v0001127_t0021"; // The ID of the file you want to download
$uri = $download_url . "/b2api/v2/b2_download_file_by_id?fileId=" . $file_id;
$session = curl_init($uri);
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
echo '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // Tell me about the rabbits, George!
}
curl_close ($session); // Clean up
//$download_url = ""; // From b2_authorize_account call
$file_id = "4_z731245f41efc196b6dda0018_f116729ca6de74b38_d20190910_m132847_c002_v0001127_t0021"; // The ID of the file you want to download
$uri = $download_url . "/b2api/v2/b2_download_file_by_id?fileId=" . $file_id . '&Authorization=' . $auth_token;
$session = curl_init($uri);
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
echo '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // Tell me about the rabbits, George!
}
curl_close ($session); // Clean up
}
function b2_download_file_by_name($download_url, $auth_token)
{
//$download_url = ""; // From b2_authorize_account call
$bucket_name = "andysh-bt-test"; // The NAME of the bucket you want to download from
$file_name = "B2-Test-Album/preview/7tgoy55do1vjv180ytlp.jpeg"; // The name of the file you want to download
$uri = $download_url . "/file/" . $bucket_name . "/" . $file_name;
$session = curl_init($uri);
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP GET
curl_setopt($session, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
echo '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // Tell me about the rabbits, George!
}
curl_close ($session); // Clean up
// You will need to use the account authorization token if your bucket's type is allPrivate.
//$download_url = ""; // From b2_authorize_account call
$bucket_name = "andysh-bt-test"; // The NAME of the bucket you want to download from
$file_name = "B2-Test-Album/preview/7tgoy55do1vjv180ytlp.jpeg"; // The name of the file you want to download
//$auth_token = ""; // From b2_authorize_account call
$uri = $download_url . "/file/" . $bucket_name . "/" . $file_name . '?Authorization=' . $auth_token;
$session = curl_init($uri);
curl_setopt($session, CURLOPT_HTTPGET, true); // HTTP POST
curl_setopt($session, CURLOPT_RETURNTRANSFER, true); // Receive server response
echo '<p>' . $uri . '</p>';
$server_output = curl_exec($session); // Let's do this!
if (curl_getinfo($session, CURLINFO_HTTP_CODE) != 200)
{
echo '<p>' . $server_output . '</p>';
}
else
{
echo '<p>' . (strlen($server_output) . ' bytes received') . '</p>'; // Tell me about the rabbits, George!
}
curl_close ($session); // Clean up
}
?>
<h2>b2_authorize_account</h2>
<?php $authorize_account_result = b2_authorize_account(); ?>
<h2>b2_download_file_by_name</h2>
<?php b2_download_file_by_name($authorize_account_result->downloadUrl, $authorize_account_result->authorizationToken); ?>
<h2>b2_download_file_by_id</h2>
<?php b2_download_file_by_id($authorize_account_result->downloadUrl, $authorize_account_result->authorizationToken); ?>

View File

@ -6,28 +6,24 @@ It takes advantage of modern frameworks (Laravel, Bootstrap 4, VueJS) as well as
You can see Blue Twilight in action on my own photo gallery - the reason I wrote Blue Twilight - at: [photos.andysh.uk](https://photos.andysh.uk)
## Version 2 Branch (2.0, 2.1, etc.)
## Blue Twilight Cloud
Version 2 is the first version I have released as open-source. The previous version (1.1.2) was only ever used on my own gallery.
This is a major update that includes 2 key new features: fine-grained security controls, and nested albums. It also updates the default template to Bootstrap v4 and VueJS (replacing KnockoutJS.)
With the launch of version 2.0.0, this has now been officially released - see the [Releases](https://apps.andysh.uk/aheathershaw/blue-twilight/releases) page for the latest version.
If you want your own dedicated, private instance of Blue Twilight without the hassle of managing servers, hosting and updates - check out [Blue Twilight Cloud](https://showmy.photos).
## Demo System
See Blue Twilight in action using the demo system. Full details are [available here](https://andysh.uk/software/blue-twilight-php-photo-gallery/demo/).
See Blue Twilight in action using the demo system. Full details are [available here](https://showmy.photos/demo/).
The link to the demo system is: http://demo.showmy.photos. Login with:
The link to the demo system is: https://demo.showmy.photos. Login with:
* Username: **demo@demo.com**
* Password: **demo123**
## Useful Links
* [Blue Twilight website](https://andysh.uk/software/blue-twilight-php-photo-gallery/)
* [User Manual](https://andysh.uk/software/blue-twilight-php-photo-gallery/manual/)
* [Installation Guide](https://andysh.uk/software/blue-twilight-php-photo-gallery/manual/installation/)
* [Blue Twilight website](https://showmy.photos/)
* [User Manual](https://showmy.photos/user-guide/)
* [Installation Guide](https://showmy.photos/user-guide/installation/)
* [Issues/Tasks](https://apps.andysh.uk/aheathershaw/blue-twilight/issues)
* [Roadmap](https://apps.andysh.uk/aheathershaw/blue-twilight/milestones)

View File

@ -323,6 +323,7 @@ return [
'photos' => 'photo|photos',
'users' => 'user|users',
],
'storage_backblaze_access_key_id_help' => 'To use your account\'s master key, enter your account ID here.',
'storage_title' => 'Storage Locations',
'sysinfo_panel' => 'System information',
'sysinfo_widget' => [

View File

@ -98,7 +98,15 @@ return [
'storage_access_key_label' => 'Access key:',
'storage_active_label' => 'Location is active. Uncheck to prevent creating new albums in this location.',
'storage_api_key_label' => 'API key:',
'storage_application_key_id_label' => 'Application key ID:',
'storage_application_key_label' => 'Application key:',
'storage_auth_url_label' => 'Authentication URL:',
'storage_b2_bucket_type' => [
'autodetect' => 'Auto-detect',
'label' => 'Bucket type:',
'private' => 'Private',
'public' => 'Public'
],
'storage_bucket_name_label' => 'Bucket name:',
'storage_cdn_url_label' => 'Public CDN URL (if supported and enabled):',
'storage_container_name_label' => 'Container name:',

View File

@ -2,6 +2,7 @@
return [
'album_sources' => [
'amazon_s3' => 'Amazon S3 (or S3-compatible)',
'backblaze_b2' => 'Backblaze B2 Cloud',
'filesystem' => 'Local filesystem',
'openstack' => 'OpenStack cloud storage',
'rackspace' => 'Rackspace cloud storage'

View File

@ -63,6 +63,10 @@
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
</div>
<div v-if="storage_driver == 'BackblazeB2Source'">
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="is-default" name="is_default"@if (old('is_default', $storage->is_default)) checked="checked"@endif>
<label class="form-check-label" for="is-default">@lang('forms.default_storage_label')</label>

View File

@ -61,6 +61,11 @@
@include(Theme::viewName('partials.admin_storages_rackspace_options'))
@endif
<div v-if="storage_driver == 'BackblazeB2Source'">
<hr/>
@include(Theme::viewName('partials.admin_storages_backblaze_b2_options'))
</div>
<div class="text-right">
<a href="{{ route('storage.index') }}" class="btn btn-default">@lang('forms.cancel_action')</a>
<button type="submit" class="btn btn-success"><i class="fa fa-fw fa-check"></i> @lang('forms.save_action')</button>

View File

@ -0,0 +1,58 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="access-key">@lang('forms.storage_application_key_id_label')</label>
<input type="text" class="form-control{{ $errors->has('access_key') ? ' is-invalid' : '' }}" id="access-key" name="access_key" value="{{ old('access_key', $storage->access_key) }}">
<small class="form-text text-muted">@lang('admin.storage_backblaze_access_key_id_help')</small>
@if ($errors->has('access_key'))
<div class="invalid-feedback">
<strong>{{ $errors->first('access_key') }}</strong>
</div>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="secret-key">@lang('forms.storage_application_key_label')</label>
<input type="text" class="form-control{{ $errors->has('secret_key') ? ' is-invalid' : '' }}" id="secret-key" name="secret_key" value="{{ old('secret_key', $storage->secret_key) }}">
@if ($errors->has('secret_key'))
<div class="invalid-feedback">
<strong>{{ $errors->first('secret_key') }}</strong>
</div>
@endif
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="container-name">@lang('forms.storage_bucket_name_label')</label>
<input type="text" class="form-control{{ $errors->has('container_name') ? ' is-invalid' : '' }}" id="container-name" name="container_name" value="{{ old('container_name', $storage->container_name) }}">
@if ($errors->has('container_name'))
<div class="invalid-feedback">
<strong>{{ $errors->first('container_name') }}</strong>
</div>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="b2-bucket-type">@lang('forms.storage_b2_bucket_type.label')</label>
<select class="form-control{{ $errors->has('b2_bucket_type') ? ' is-invalid' : '' }}" id="b2-bucket-type" name="b2_bucket_type">
<option value="0"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 0 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.autodetect')</option>
<option value="1"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 1 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.private')</option>
<option value="2"{{ old('b2_bucket_type', $storage->b2_bucket_type) === 2 ? ' selected="selected"' : '' }}>@lang('forms.storage_b2_bucket_type.public')</option>
</select>
@if ($errors->has('b2_bucket_type'))
<div class="invalid-feedback">
<strong>{{ $errors->first('b2_bucket_type') }}</strong>
</div>
@endif
</div>
</div>
</div>