feat: download and get storage files as private (#5192)

This commit is contained in:
Alexis Saettler
2021-06-22 18:37:38 +02:00
committed by GitHub
parent c3f3eac1b9
commit 7fdc4453b6
23 changed files with 678 additions and 48 deletions
+1 -1
View File
@@ -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);
+4 -1
View File
@@ -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();
}
+136
View File
@@ -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();
+1 -1
View File
@@ -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'));
}
}
+4 -2
View File
@@ -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]);
}
/**
+6 -6
View File
@@ -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]);
}
/**
+4 -2
View File
@@ -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
View File
@@ -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);
});
});
}
/**
+5 -2
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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/",
+32
View File
@@ -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;
+2
View File
@@ -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",
+2
View File
@@ -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');
+3 -14
View File
@@ -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>
+374
View File
@@ -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;
}
}
+2 -2
View 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()
);
}
+1 -1
View File
@@ -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()
);
}
+1 -1
View File
@@ -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()
);
}