feat: download and get storage files as private (#5192)
This commit is contained in:
@@ -96,7 +96,7 @@ class MoveAvatars extends Command
|
||||
}
|
||||
if (! $this->option('dryrun')) {
|
||||
$avatarFile = $storage->get($avatarFileName);
|
||||
$newStorage->put($avatarFileName, $avatarFile, 'public');
|
||||
$newStorage->put($avatarFileName, $avatarFile, config('filesystems.default_visibility'));
|
||||
}
|
||||
|
||||
$this->line(' File pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE);
|
||||
|
||||
@@ -462,7 +462,10 @@ class ContactsController extends Controller
|
||||
}
|
||||
$contact->has_avatar = true;
|
||||
$contact->avatar_location = config('filesystems.default');
|
||||
$contact->avatar_file_name = $request->avatar->storePublicly('avatars', $contact->avatar_location);
|
||||
$contact->avatar_file_name = $request->file('avatar')->store('avatars', [
|
||||
'disk' => $contact->avatar_location,
|
||||
'visibility' => config('filesystems.default_visibility'),
|
||||
]);
|
||||
$contact->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Account\Photo;
|
||||
use App\Helpers\StorageHelper;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Models\Contact\Contact;
|
||||
use App\Models\Contact\Document;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use League\Flysystem\FileNotFoundException;
|
||||
|
||||
class StorageController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['setEtag', 'ifMatch', 'ifNoneMatch']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file with authorization.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $file
|
||||
* @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\StreamedResponse|null
|
||||
*/
|
||||
public function show(Request $request, string $file)
|
||||
{
|
||||
$filename = $this->getFilename($request, $file);
|
||||
|
||||
try {
|
||||
$disk = StorageHelper::disk(config('filesystems.default'));
|
||||
|
||||
$lastModified = Carbon::createFromTimestamp($disk->lastModified($file), 'UTC')->locale('en');
|
||||
|
||||
$headers = [
|
||||
'Last-Modified' => $lastModified->isoFormat('ddd\, DD MMM YYYY HH\:mm\:ss \G\M\T'),
|
||||
'Cache-Control' => config('filesystems.default_cache_control'),
|
||||
];
|
||||
|
||||
if (! $this->checkConditions($request, $lastModified)) {
|
||||
return Response::noContent(304, $headers)->setNotModified();
|
||||
}
|
||||
|
||||
return $disk->response($file, $filename, $headers);
|
||||
} catch (FileNotFoundException $e) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename for this file.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $file
|
||||
* @return string
|
||||
*/
|
||||
private function getFilename(Request $request, string $file): string
|
||||
{
|
||||
$accountId = $request->user()->account_id;
|
||||
$folder = Str::before($file, '/');
|
||||
|
||||
switch ($folder) {
|
||||
case 'avatars':
|
||||
$obj = Contact::where([
|
||||
'account_id' => $accountId,
|
||||
['avatar_default_url', 'like', "$file%"],
|
||||
])->first();
|
||||
$filename = Str::after($file, '/');
|
||||
break;
|
||||
|
||||
case 'photos':
|
||||
$obj = Photo::where([
|
||||
'account_id' => $accountId,
|
||||
'new_filename' => $file,
|
||||
])->first();
|
||||
$filename = $obj ? $obj->original_filename : null;
|
||||
break;
|
||||
|
||||
case 'documents':
|
||||
$obj = Document::where([
|
||||
'account_id' => $accountId,
|
||||
'new_filename' => $file,
|
||||
])->first();
|
||||
$filename = $obj ? $obj->original_filename : null;
|
||||
break;
|
||||
|
||||
default:
|
||||
$obj = false;
|
||||
$filename = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($obj === false || $obj === null || ! $obj->exists) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for If-Modified-Since and If-Unmodified-Since conditions.
|
||||
* Return true if the condition does not match.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Carbon $lastModified Last modified date
|
||||
* @return bool
|
||||
*/
|
||||
private function checkConditions(Request $request, Carbon $lastModified): bool
|
||||
{
|
||||
if (! $request->header('If-None-Match') && ($ifModifiedSince = $request->header('If-Modified-Since'))) {
|
||||
// The If-Modified-Since header contains a date. We will only
|
||||
// return the entity if it has been changed since that date.
|
||||
$date = Carbon::parse($ifModifiedSince);
|
||||
|
||||
if ($lastModified->lessThanOrEqualTo($date)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ifUnmodifiedSince = $request->header('If-Unmodified-Since')) {
|
||||
// The If-Unmodified-Since will allow the request if the
|
||||
// entity has not changed since the specified date.
|
||||
$date = Carbon::parse($ifUnmodifiedSince);
|
||||
|
||||
// We must only check the date if it's valid
|
||||
if ($lastModified->greaterThan($date)) {
|
||||
abort(412, 'An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ class MoveContactAvatarToPhotosDirectory implements ShouldQueue
|
||||
|
||||
if (! $this->dryrun) {
|
||||
$avatarFile = $this->storage->get($avatarFileName);
|
||||
$newStorage->put($newAvatarFilename, $avatarFile, 'public');
|
||||
$newStorage->put($newAvatarFilename, $avatarFile, config('filesystems.default_visibility'));
|
||||
|
||||
$this->contact->avatar_location = config('filesystems.default');
|
||||
$this->contact->save();
|
||||
|
||||
@@ -63,6 +63,6 @@ class ResizeAvatars implements ShouldQueue
|
||||
$avatar = Image::make($avatarFile);
|
||||
$avatar->fit($size);
|
||||
|
||||
$storage->put($avatarFileName, (string) $avatar->stream(), 'public');
|
||||
$storage->put($avatarFileName, (string) $avatar->stream(), config('filesystems.default_visibility'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,11 @@ class Photo extends Model
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
$url = $this->new_filename;
|
||||
if (config('filesystems.default_visibility') === 'public') {
|
||||
return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename));
|
||||
}
|
||||
|
||||
return asset(StorageHelper::disk(config('filesystems.default'))->url($url));
|
||||
return route('storage', ['file' => $this->new_filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Support\Str;
|
||||
use App\Helpers\LocaleHelper;
|
||||
use App\Models\Account\Photo;
|
||||
use App\Models\Journal\Entry;
|
||||
use function Safe\preg_split;
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Helpers\WeatherHelper;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -1049,17 +1048,18 @@ class Contact extends Model
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
$matches = preg_split('/\?/', $this->avatar_default_url);
|
||||
if (config('filesystems.default_visibility') === 'public') {
|
||||
$matches = Str::of($this->avatar_default_url)->split('/\?/');
|
||||
|
||||
$url = asset(StorageHelper::disk(config('filesystems.default'))->url($matches[0]));
|
||||
if (count($matches) > 1) {
|
||||
if ($matches->count() > 1) {
|
||||
$url .= '?'.$matches[1];
|
||||
}
|
||||
|
||||
return $url;
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return route('storage', ['file' => $this->avatar_default_url]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,8 +59,10 @@ class Document extends Model
|
||||
*/
|
||||
public function getDownloadLink(): string
|
||||
{
|
||||
$url = $this->new_filename;
|
||||
if (config('filesystems.default_visibility') === 'public') {
|
||||
return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename));
|
||||
}
|
||||
|
||||
return asset(StorageHelper::disk(config('filesystems.default'))->url($url));
|
||||
return route('storage', ['file' => $this->new_filename]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use App\Notifications\EmailMessaging;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Werk365\EtagConditionals\EtagConditionals;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -74,6 +76,14 @@ class AppServiceProvider extends ServiceProvider
|
||||
Limit::perDay(5000),
|
||||
];
|
||||
});
|
||||
|
||||
EtagConditionals::etagGenerateUsing(function (\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response) {
|
||||
$url = $request->getRequestUri();
|
||||
|
||||
return Cache::rememberForever('etag.'.$url, function () use ($url) {
|
||||
return md5($url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,10 @@ class UploadPhoto extends BaseService
|
||||
'original_filename' => $photo->getClientOriginalName(),
|
||||
'filesize' => $photo->getSize(),
|
||||
'mime_type' => (new \Mimey\MimeTypes)->getMimeType($photo->guessClientExtension()),
|
||||
'new_filename' => $photo->storePublicly('photos', config('filesystems.default')),
|
||||
'new_filename' => $photo->store('photos', [
|
||||
'disk' => config('filesystems.default'),
|
||||
'visibility' => config('filesystems.default_visibility'),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -146,7 +149,7 @@ class UploadPhoto extends BaseService
|
||||
private function storeImage(string $disk, $image, string $filename): ?string
|
||||
{
|
||||
$result = Storage::disk($disk)
|
||||
->put($path = $filename, (string) $image->stream(), 'public');
|
||||
->put($path = $filename, (string) $image->stream(), config('filesystems.default_visibility'));
|
||||
|
||||
return $result ? $path : null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services\Contact\Avatar;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\BaseService;
|
||||
use App\Models\Contact\Contact;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Laravolt\Avatar\Facade as Avatar;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
@@ -47,6 +48,8 @@ class GenerateDefaultAvatar extends BaseService
|
||||
$contact->avatar_default_url = $filename;
|
||||
$contact->save();
|
||||
|
||||
Cache::forget('etag'.Str::before('?', $filename));
|
||||
|
||||
return $contact;
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ class GenerateDefaultAvatar extends BaseService
|
||||
|
||||
$filename = 'avatars/'.$contact->uuid.'.jpg';
|
||||
Storage::disk(config('filesystems.default'))
|
||||
->put($filename, $img, 'public');
|
||||
->put($filename, $img, config('filesystems.default_visibility'));
|
||||
|
||||
// This will force the browser to reload the new avatar
|
||||
return $filename.'?'.now()->format('U');
|
||||
|
||||
@@ -65,7 +65,10 @@ class UploadDocument extends BaseService
|
||||
'mime_type' => (new \Mimey\MimeTypes)->getMimeType($document->guessClientExtension()),
|
||||
];
|
||||
|
||||
$filename = $document->storePublicly('documents', config('filesystems.default'));
|
||||
$filename = $document->store('documents', [
|
||||
'disk' => config('filesystems.default'),
|
||||
'visibility' => config('filesystems.default_visibility'),
|
||||
]);
|
||||
|
||||
return array_merge($data, [
|
||||
'new_filename' => $filename,
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"web-token/jwt-signature-algorithm-ecdsa": "^2.1",
|
||||
"web-token/jwt-signature-algorithm-eddsa": "^2.1",
|
||||
"web-token/jwt-signature-algorithm-rsa": "^2.1",
|
||||
"werk365/etagconditionals": "^1.2",
|
||||
"xantios/mimey": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
Generated
+79
-12
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6e935211274a98900df063306a02f2d2",
|
||||
"content-hash": "c0c0b45e6b8264564db678658a42159c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asbiin/laravel-webauthn",
|
||||
@@ -12575,6 +12575,66 @@
|
||||
},
|
||||
"time": "2021-03-09T10:59:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "werk365/etagconditionals",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/365Werk/etagconditionals.git",
|
||||
"reference": "947743c7b711a0669d1dd39f4e8797b62159dcb8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/365Werk/etagconditionals/zipball/947743c7b711a0669d1dd39f4e8797b62159dcb8",
|
||||
"reference": "947743c7b711a0669d1dd39f4e8797b62159dcb8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "~7|~8"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "~5|~6",
|
||||
"phpunit/phpunit": "~8.0|~9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Werk365\\EtagConditionals\\EtagConditionalsServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"EtagConditionals": "Werk365\\EtagConditionals\\Facades\\EtagConditionals"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Werk365\\EtagConditionals\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Hergen Dillema",
|
||||
"email": "h.dillema@365werk.nl",
|
||||
"homepage": "https://365werk.nl"
|
||||
}
|
||||
],
|
||||
"description": "Laravel package to enable support for ETags and handling If-Match and If-None-Match conditional requests",
|
||||
"homepage": "https://github.com/werk365/etagconditionals",
|
||||
"keywords": [
|
||||
"EtagConditionals",
|
||||
"laravel"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/365Werk/etagconditionals/issues",
|
||||
"source": "https://github.com/365Werk/etagconditionals/tree/1.2.0"
|
||||
},
|
||||
"time": "2021-06-03T09:16:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "xantios/mimey",
|
||||
"version": "v2.2.0",
|
||||
@@ -15957,7 +16017,7 @@
|
||||
"barrelstrength/sprout-forms": "<3.9",
|
||||
"baserproject/basercms": ">=4,<=4.3.6|>=4.4,<4.4.1",
|
||||
"bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
|
||||
"bolt/bolt": "<3.7.1",
|
||||
"bolt/bolt": "<3.7.2",
|
||||
"bolt/core": "<4.1.13",
|
||||
"brightlocal/phpwhois": "<=4.2.5",
|
||||
"buddypress/buddypress": "<5.1.2",
|
||||
@@ -15987,8 +16047,9 @@
|
||||
"doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
|
||||
"dolibarr/dolibarr": "<11.0.4",
|
||||
"dompdf/dompdf": ">=0.6,<0.6.2",
|
||||
"drupal/core": ">=7,<7.74|>=8,<8.8.11|>=8.9,<8.9.9|>=9,<9.0.8",
|
||||
"drupal/drupal": ">=7,<7.74|>=8,<8.8.11|>=8.9,<8.9.9|>=9,<9.0.8",
|
||||
"drupal/core": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
|
||||
"drupal/drupal": ">=7,<7.80|>=8,<8.9.14|>=9,<9.0.12|>=9.1,<9.1.7",
|
||||
"dweeves/magmi": "<=0.7.24",
|
||||
"endroid/qr-code-bundle": "<3.4.2",
|
||||
"enshrined/svg-sanitize": "<0.13.1",
|
||||
"erusev/parsedown": "<1.7.2",
|
||||
@@ -16013,7 +16074,9 @@
|
||||
"flarum/tags": "<=0.1-beta.13",
|
||||
"fluidtypo3/vhs": "<5.1.1",
|
||||
"fooman/tcpdf": "<6.2.22",
|
||||
"forkcms/forkcms": "<5.8.3",
|
||||
"fossar/tcpdf-parser": "<6.2.22",
|
||||
"francoisjacquet/rosariosis": "<6.5.1",
|
||||
"friendsofsymfony/oauth2-php": "<1.3",
|
||||
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
|
||||
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
|
||||
@@ -16044,7 +16107,8 @@
|
||||
"laravel/framework": "<6.20.26|>=7,<8.40",
|
||||
"laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
|
||||
"league/commonmark": "<0.18.3",
|
||||
"librenms/librenms": "<1.53",
|
||||
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
|
||||
"librenms/librenms": "<21.1",
|
||||
"livewire/livewire": ">2.2.4,<2.2.6",
|
||||
"magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
|
||||
"magento/magento1ce": "<1.9.4.3",
|
||||
@@ -16065,11 +16129,12 @@
|
||||
"nystudio107/craft-seomatic": "<3.3",
|
||||
"nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
|
||||
"october/backend": "<1.1.2",
|
||||
"october/cms": "= 1.0.469|>=1.0.319,<1.0.469",
|
||||
"october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469",
|
||||
"october/october": ">=1.0.319,<1.0.466",
|
||||
"october/rain": "<1.0.472|>=1.1,<1.1.2",
|
||||
"onelogin/php-saml": "<2.10.4",
|
||||
"oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
|
||||
"opencart/opencart": "<=3.0.3.2",
|
||||
"openid/php-openid": "<2.3",
|
||||
"openmage/magento-lts": "<=19.4.12|>=20,<=20.0.8",
|
||||
"orchid/platform": ">=9,<9.4.4",
|
||||
@@ -16163,20 +16228,21 @@
|
||||
"symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
|
||||
"symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
|
||||
"symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1",
|
||||
"symfony/mime": ">=4.3,<4.3.8",
|
||||
"symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/polyfill": ">=1,<1.10",
|
||||
"symfony/polyfill-php55": ">=1,<1.10",
|
||||
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
|
||||
"symfony/routing": ">=2,<2.0.19",
|
||||
"symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/security": ">=2,<2.7.51|>=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||
"symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
|
||||
"symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||
"symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
|
||||
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
|
||||
"symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||
"symfony/serializer": ">=2,<2.0.11",
|
||||
"symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5",
|
||||
"symfony/symfony": ">=2,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
|
||||
"symfony/translation": ">=2,<2.0.17",
|
||||
"symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
|
||||
"symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
|
||||
@@ -16241,7 +16307,8 @@
|
||||
"zetacomponents/mail": "<1.8.2",
|
||||
"zf-commons/zfc-user": "<1.2.2",
|
||||
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
|
||||
"zfr/zfr-oauth2-server-module": "<0.1.2"
|
||||
"zfr/zfr-oauth2-server-module": "<0.1.2",
|
||||
"zoujingli/thinkadmin": "<6.0.22"
|
||||
},
|
||||
"type": "metapackage",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
|
||||
@@ -86,4 +86,36 @@ return [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem visibility
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If this is set to private, all files are stored privately, and are
|
||||
| delivered using a proxy-url by monica, providing access of the files in
|
||||
| the storage.
|
||||
| This means only the authenticated user will be able to open the files.
|
||||
|
|
||||
| You might store the files publicly if you're on a private instance and if
|
||||
| you want to make files accessible from the outside - the url files are
|
||||
| still private and not easy to guess.
|
||||
|
|
||||
| Supported: "private", "public"
|
||||
|
|
||||
*/
|
||||
|
||||
'default_visibility' => env('FILESYSTEM_DEFAULT_VISIBILITY', 'private'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache control for files
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Defines the Cache-Control header used to serve files.
|
||||
| Default: 'max-age=2628000' for 1 month cache.
|
||||
|
|
||||
*/
|
||||
|
||||
'default_cache_control' => env('DEFAULT_CACHE_CONTROL', 'private, max-age=2628000'),
|
||||
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Helpers\MoneyHelper;
|
||||
use App\Models\Account\Account;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Money\Currencies\ISOCurrencies;
|
||||
use Money\Currency as MoneyCurrency;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"hot": "mix watch --hot",
|
||||
"prod": "yarn production",
|
||||
"production": "mix --production",
|
||||
"predevelopment": "php artisan lang:generate -vvv",
|
||||
"prewatch": "php artisan lang:generate -vvv",
|
||||
"preproduction": "php artisan lang:generate -vvv",
|
||||
"heroku-postbuild": "yarn run production",
|
||||
"e2e": "cypress run",
|
||||
|
||||
@@ -46,6 +46,8 @@ Route::middleware(['auth', 'verified', 'mfa'])->group(function () {
|
||||
Route::post('/dashboard/setTab', 'DashboardController@setTab');
|
||||
});
|
||||
|
||||
Route::get('/store/{file}', 'StorageController@show')->where('file', '.*')->name('storage');
|
||||
|
||||
Route::get('/compliance', 'ComplianceController@index')->name('compliance');
|
||||
Route::post('/compliance/sign', 'ComplianceController@store');
|
||||
Route::get('/changelog', 'ChangelogController@index')->name('changelog.index');
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Activate HSTS
|
||||
Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"
|
||||
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload;"
|
||||
|
||||
Header always set Referrer-Policy "no-referrer"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
@@ -22,19 +22,8 @@
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
|
||||
# Assets expire after 1 month
|
||||
<filesMatch ".(css|js|woff2?|svg|png|gif|json)$">
|
||||
Header set Cache-Control "public, max-age=15768000"
|
||||
|
||||
# Activate HSTS
|
||||
Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"
|
||||
|
||||
Header always set Referrer-Policy "no-referrer"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-Download-Options "noopen"
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Permitted-Cross-Domain-Policies "none"
|
||||
Header always set X-Robots-Tag "none"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
<filesMatch ".(css|js|json|woff2?|ttf|eot|svg)$">
|
||||
Header set Cache-Control "public, max-age=2628000"
|
||||
</filesMatch>
|
||||
</IfModule>
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Tests\FeatureTestCase;
|
||||
use App\Models\Account\Photo;
|
||||
use App\Models\Contact\Contact;
|
||||
use App\Models\Contact\Document;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
class StorageControllerTest extends FeatureTestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
/**
|
||||
* Returns an array containing a user object along with
|
||||
* a contact for that user.
|
||||
* @return array
|
||||
*/
|
||||
private function fetchUser()
|
||||
{
|
||||
$user = $this->signIn();
|
||||
|
||||
$contact = factory(Contact::class)->create([
|
||||
'account_id' => $user->account_id,
|
||||
]);
|
||||
|
||||
return [$user, $contact];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_get_photo_content()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_get_avatar_content()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeAvatar($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_get_document_content()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeDocument($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_if_avatar_not_exist()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$response = $this->get('/store/avatars/test');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_404_if_folder_unknown()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$response = $this->get('/store/xxx/test');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_200_if_modified_after_IfModifiedSince()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Modified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_304_if_not_modified_since_IfModifiedSince()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Modified-Since' => 'Sat, 26 Jun 2021 07:00:00 GMT',
|
||||
]);
|
||||
|
||||
$response->assertNoContent(304);
|
||||
$response->assertHeaderMissing('Last-Modified');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_200_if_not_modified_after_IfUnmodifiedSince()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Unmodified-Since' => 'Sat, 26 Jun 2021 07:00:00 GMT',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_412_if_modified_after_IfUnmodifiedSince()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Unmodified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT',
|
||||
]);
|
||||
|
||||
$response->assertStatus(412);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_fails_if_file_not_found()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$response = $this->get('/store/photos/fail.png');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_fails_if_file_not_exist()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$photo = factory(Photo::class)->create([
|
||||
'account_id' => $contact->account_id,
|
||||
'original_filename' => 'avatar.png',
|
||||
'filesize' => 0,
|
||||
'mime_type' => '',
|
||||
'new_filename' => 'avatar.png',
|
||||
]);
|
||||
|
||||
$contact->photos()->syncWithoutDetaching([$photo->id]);
|
||||
|
||||
$response = $this->get('/store/photos/avatar.png');
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_fails_if_file_not_owned_by_user()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$this->signIn();
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Unmodified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT',
|
||||
]);
|
||||
|
||||
$response->assertStatus(404);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_200_if_matching_IfMatch()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-Match' => '"'.md5('/store/'.$file).'"',
|
||||
]);
|
||||
|
||||
$response->assertNoContent(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_200_with_none_matching_IfNoneMatch()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-None-Match' => '"test"',
|
||||
]);
|
||||
|
||||
$response->assertNoContent(200);
|
||||
$response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_returns_304_if_matching_IfNoneMatch()
|
||||
{
|
||||
config(['filesystems.default' => 'local']);
|
||||
|
||||
[$user, $contact] = $this->fetchUser();
|
||||
|
||||
$file = $this->storeImage($contact);
|
||||
|
||||
$response = $this->get('/store/'.$file, [
|
||||
'If-None-Match' => '"'.md5('/store/'.$file).'"',
|
||||
]);
|
||||
|
||||
$response->assertNoContent(304);
|
||||
$response->assertHeaderMissing('Last-Modified');
|
||||
$response->assertHeader('Cache-Control', 'max-age=2628000, private');
|
||||
$response->assertHeader('etag', '"'.md5('/store/'.$file).'"');
|
||||
}
|
||||
|
||||
public function storeImage(Contact $contact)
|
||||
{
|
||||
$disk = Storage::fake('local', [
|
||||
'cache' => [
|
||||
'store' => 'file',
|
||||
'expire' => 600,
|
||||
'prefix' => 'local',
|
||||
],
|
||||
]);
|
||||
$image = File::createWithContent('avatar.png', file_get_contents(base_path('public/img/favicon.png')));
|
||||
|
||||
$file = $disk->put('/photos', $image, 'private');
|
||||
|
||||
$photo = factory(Photo::class)->create([
|
||||
'account_id' => $contact->account_id,
|
||||
'original_filename' => 'avatar.png',
|
||||
'filesize' => $image->getSize(),
|
||||
'mime_type' => $image->getMimeType(),
|
||||
'new_filename' => $file,
|
||||
]);
|
||||
|
||||
$contact->photos()->syncWithoutDetaching([$photo->id]);
|
||||
|
||||
$adapter = $disk->getDriver()->getAdapter();
|
||||
$adapter->getCache()->updateObject($file, [
|
||||
'timestamp' => Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp,
|
||||
]);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
public function storeDocument(Contact $contact)
|
||||
{
|
||||
$disk = Storage::fake('local', [
|
||||
'cache' => [
|
||||
'store' => 'file',
|
||||
'expire' => 600,
|
||||
'prefix' => 'local',
|
||||
],
|
||||
]);
|
||||
$image = File::createWithContent('file.png', file_get_contents(base_path('public/img/favicon.png')));
|
||||
|
||||
$file = $disk->put('/documents', $image, 'private');
|
||||
|
||||
$document = factory(Document::class)->create([
|
||||
'account_id' => $contact->account_id,
|
||||
'contact_id' => $contact->id,
|
||||
'original_filename' => 'file.png',
|
||||
'new_filename' => $file,
|
||||
]);
|
||||
|
||||
$adapter = $disk->getDriver()->getAdapter();
|
||||
$adapter->getCache()->updateObject($file, [
|
||||
'timestamp' => Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp,
|
||||
]);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
public function storeAvatar(Contact $contact)
|
||||
{
|
||||
$disk = Storage::fake('local', [
|
||||
'cache' => [
|
||||
'store' => 'file',
|
||||
'expire' => 600,
|
||||
'prefix' => 'local',
|
||||
],
|
||||
]);
|
||||
$image = File::createWithContent('avatar.png', file_get_contents(base_path('public/img/favicon.png')));
|
||||
|
||||
$file = $disk->put('/avatars', $image, 'private');
|
||||
|
||||
$contact->avatar_source = 'default';
|
||||
$contact->avatar_default_url = $file.'?123';
|
||||
$contact->save();
|
||||
|
||||
$adapter = $disk->getDriver()->getAdapter();
|
||||
$adapter->getCache()->updateObject($file, [
|
||||
'timestamp' => Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp,
|
||||
]);
|
||||
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
@@ -524,7 +524,7 @@ class ContactTest extends FeatureTestCase
|
||||
]);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'storage/defaultURL',
|
||||
'store/defaultURL',
|
||||
$contact->getAvatarURL()
|
||||
);
|
||||
|
||||
@@ -559,7 +559,7 @@ class ContactTest extends FeatureTestCase
|
||||
$contact->save();
|
||||
|
||||
$this->assertEquals(
|
||||
config('app.url').'/storage/'.$photo->new_filename,
|
||||
config('app.url').'/store/'.$photo->new_filename,
|
||||
$contact->getAvatarURL()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class DocumentTest extends TestCase
|
||||
$document = factory(Document::class)->create();
|
||||
|
||||
$this->assertEquals(
|
||||
config('app.url').'/storage/'.$document->new_filename,
|
||||
config('app.url').'/store/'.$document->new_filename,
|
||||
$document->getDownloadLink()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class PhotoTest extends TestCase
|
||||
{
|
||||
$photo = factory(Photo::class)->create();
|
||||
$this->assertEquals(
|
||||
config('app.url').'/storage/'.$photo->new_filename,
|
||||
config('app.url').'/store/'.$photo->new_filename,
|
||||
$photo->url()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user