diff --git a/app/Http/Controllers/TextureController.php b/app/Http/Controllers/TextureController.php index d7efa10f..f1a8b93c 100644 --- a/app/Http/Controllers/TextureController.php +++ b/app/Http/Controllers/TextureController.php @@ -7,23 +7,18 @@ use Option; use Storage; use Response; use Exception; +use Carbon\Carbon; use App\Models\User; use App\Models\Player; use App\Models\Texture; use App\Services\Minecraft; +use Illuminate\Support\Arr; use App\Events\GetSkinPreview; use App\Events\GetAvatarPreview; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class TextureController extends Controller { - /** - * Return Player Profile formatted in JSON. - * - * @param string $player_name - * @param string $api - * @return \Illuminate\Http\Response - */ public function json($player_name, $api = '') { $player = $this->getPlayerInstance($player_name); @@ -33,11 +28,12 @@ class TextureController extends Controller } elseif ($api == 'usm') { $content = $player->getJsonProfile(Player::USM_API); } else { - $content = $player->getJsonProfile(Option::get('api_type')); + $content = $player->getJsonProfile(option('api_type')); } - return Response::jsonProfile($content, 200, [ - 'Last-Modified' => strtotime($player->last_modified), + return response($content, 200, [ + 'Content-type' => 'application/json', + 'Last-Modified' => $player->last_modified, ]); } @@ -50,9 +46,9 @@ class TextureController extends Controller { try { if (Storage::disk('textures')->has($hash)) { - return Response::png(Storage::disk('textures')->get($hash), 200, array_merge([ - 'Last-Modified' => Storage::disk('textures')->lastModified($hash), - 'Accept-Ranges' => 'bytes', + return $this->outputImage(Storage::disk('textures')->get($hash), array_merge([ + 'Last-Modified' => Storage::disk('textures')->lastModified($hash), + 'Accept-Ranges' => 'bytes', 'Content-Length' => Storage::disk('textures')->size($hash), ], $headers)); } @@ -90,16 +86,16 @@ class TextureController extends Controller $player = $this->getPlayerInstance($player_name); if ($hash = $player->getTexture($type)) { - return $this->texture($hash, [ - 'Last-Modified' => strtotime($player->last_modified), - ], trans('general.texture-deleted')); + return $this->texture( + $hash, + ['Last-Modified' => $player->last_modified], + trans('general.texture-deleted') + ); } else { abort(404, trans('general.texture-not-uploaded', ['type' => $type])); } } - // @codeCoverageIgnore - public function avatarByTid($tid, $size = 128) { if ($t = Texture::find($tid)) { @@ -110,9 +106,11 @@ class TextureController extends Controller if (isset($responses[0]) && $responses[0] instanceof SymfonyResponse) { return $responses[0]; // @codeCoverageIgnore } else { - $png = Minecraft::generateAvatarFromSkin(Storage::disk('textures')->read($t->hash), $size); + $png = Minecraft::generateAvatarFromSkin( + Storage::disk('textures')->read($t->hash), $size + ); - return Response::png(png($png)); + return $this->outputImage(png($png)); } } } catch (Exception $e) { @@ -157,12 +155,16 @@ class TextureController extends Controller $binary = Storage::disk('textures')->read($t->hash); if ($t->type == 'cape') { - $png = Minecraft::generatePreviewFromCape($binary, $size * 0.8, $size * 1.125, $size); + $png = Minecraft::generatePreviewFromCape( + $binary, $size * 0.8, $size * 1.125, $size + ); } else { - $png = Minecraft::generatePreviewFromSkin($binary, $size, ($t->type == 'alex'), 'both', 4); + $png = Minecraft::generatePreviewFromSkin( + $binary, $size, ($t->type == 'alex'), 'both', 4 + ); } - return Response::png(png($png)); + return $this->outputImage(png($png)); } } } catch (Exception $e) { @@ -200,12 +202,34 @@ class TextureController extends Controller $size ); - return Response::png(png($png)); + return $this->outputImage(png($png)); } return abort(404); } + protected function outputImage($content, $headers = []) + { + $request = request(); + + $ifNoneMatch = $request->header('If-None-Match'); + $eTag = md5($content); + + $ifModifiedSince = Carbon::parse($request->header('If-Modified-Since', 0)); + $lastModified = Carbon::parse(Arr::pull($headers, 'Last-Modified', time())); + + if ($eTag === $ifNoneMatch || $lastModified <= $ifModifiedSince) { + return response(null)->withHeaders($headers)->setNotModified(); + } + + return response($content, 200, $headers)->withHeaders([ + 'Content-Type' => 'image/png', + 'ETag' => $eTag, + 'Last-Modified' => $lastModified->toRfc7231String(), + 'Cache-Control' => 'max-age='.option('cache_expire_time').', public', + ]); + } + protected function getPlayerInstance($player_name) { $player = Player::where('name', $player_name)->first(); diff --git a/app/Listeners/CacheAvatarPreview.php b/app/Listeners/CacheAvatarPreview.php index 99faa9e1..73959d54 100644 --- a/app/Listeners/CacheAvatarPreview.php +++ b/app/Listeners/CacheAvatarPreview.php @@ -20,6 +20,6 @@ class CacheAvatarPreview return png(Minecraft::generateAvatarFromSkin($res, $size)); }); - return response()->png($content); + return response($content, 200, ['Content-Type' => 'image/png']); } } diff --git a/app/Listeners/CacheSkinPreview.php b/app/Listeners/CacheSkinPreview.php index ed6c87bd..1e4e1481 100644 --- a/app/Listeners/CacheSkinPreview.php +++ b/app/Listeners/CacheSkinPreview.php @@ -26,7 +26,8 @@ class CacheSkinPreview return png($png); }); - return response()->png($content, 200, [ + return response($content, 200, [ + 'Content-Type' => 'image/png', 'Last-Modified' => Storage::disk('textures')->lastModified($texture->hash), ]); } diff --git a/app/Providers/ResponseMacroServiceProvider.php b/app/Providers/ResponseMacroServiceProvider.php deleted file mode 100644 index 89d61fe7..00000000 --- a/app/Providers/ResponseMacroServiceProvider.php +++ /dev/null @@ -1,57 +0,0 @@ -headers->get('If-Modified-Since')); - $if_none_match = strtotime(request()->headers->get('If-None-Match')); - $etag = md5($src); - - // Return `304 Not Modified` if given `If-Modified-Since` header - // is newer than our `Last-Modified` time or the `Etag` matches. - if ($if_modified_since >= $last_modified || $if_none_match == $etag) { - $src = ''; - $status = 304; - } - - return Response::make($src, $status, array_merge([ - 'Content-type' => 'image/png', - 'Last-Modified' => format_http_date($last_modified), - 'Cache-Control' => 'public, max-age='.option('cache_expire_time'), - 'Etag' => $etag, - ], $header)); - }); - - Response::macro('jsonProfile', function ($src = '', $status = 200, $header = []) { - $last_modified = Arr::pull($header, 'Last-Modified', time()); - $if_modified_since = strtotime(request()->headers->get('If-Modified-Since')); - - if ($if_modified_since && $if_modified_since >= $last_modified) { - $src = ''; - $status = 304; - } - - return Response::make($src, $status, array_merge([ - 'Content-type' => 'application/json', - 'Cache-Control' => 'public, max-age='.option('cache_expire_time'), - 'Last-Modified' => format_http_date($last_modified), - ], $header)); - }); - } -} diff --git a/app/helpers.php b/app/helpers.php index 2c87dd2c..b7a3d32a 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -48,17 +48,17 @@ if (! function_exists('json')) { { $args = func_get_args(); - if (count($args) == 1 && is_array($args[0])) { - return Response::json($args[0]); - } elseif (count($args) == 3 && is_array($args[2])) { + if (count($args) === 1 && is_array($args[0])) { + return response()->json($args[0]); + } elseif (count($args) === 3 && is_array($args[2])) { // The third argument is array of extra fields - return Response::json([ + return response()->json([ 'code' => $args[1], 'message' => $args[0], 'data' => $args[2], ]); } else { - return Response::json([ + return response()->json([ 'code' => Arr::get($args, 1, 1), 'message' => $args[0], ]); diff --git a/config/app.php b/config/app.php index 867beeab..6ce33c60 100644 --- a/config/app.php +++ b/config/app.php @@ -114,7 +114,7 @@ return [ | */ - 'key' => env('APP_KEY', 'base64:MfnScX0W/ViN8bZtRt0P481rWP3igcOK80QstjbXUxI='), + 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', @@ -174,7 +174,6 @@ return [ App\Providers\EventServiceProvider::class, App\Providers\PluginServiceProvider::class, App\Providers\RouteServiceProvider::class, - App\Providers\ResponseMacroServiceProvider::class, App\Providers\ValidatorExtendServiceProvider::class, ], diff --git a/tests/TextureControllerTest.php b/tests/TextureControllerTest.php index c7cbe5d0..7085bd25 100644 --- a/tests/TextureControllerTest.php +++ b/tests/TextureControllerTest.php @@ -5,6 +5,7 @@ namespace Tests; use Event; use Mockery; use Exception; +use Carbon\Carbon; use App\Models\User; use App\Models\Player; use App\Models\Texture; @@ -111,10 +112,20 @@ class TextureControllerTest extends TestCase $this->get('/textures/'.$steve->hash) ->assertHeader('Content-Type', 'image/png') ->assertHeader('Last-Modified') + ->assertHeader('ETag') + ->assertHeader('Cache-Control', 'max-age='.option('cache_expire_time').', public') ->assertHeader('Accept-Ranges', 'bytes') ->assertHeader('Content-Length', Storage::disk('textures')->size($steve->hash)) ->assertSuccessful(); + // Cache test + $this->get('/textures/'.$steve->hash, [ + 'If-None-Match' => md5(''), + ])->assertStatus(304); + $this->get('/textures/'.$steve->hash, [ + 'If-Modified-Since' => Carbon::now()->addHours(1)->toRfc7231String(), + ])->assertStatus(304); + Storage::shouldReceive('disk')->with('textures')->andThrow(new Exception); $this->get('/textures/'.$steve->hash)->assertNotFound(); }