diff --git a/app/Exceptions/HttpFetchException.php b/app/Exceptions/HttpFetchException.php new file mode 100644 index 000000000..48e30e1e6 --- /dev/null +++ b/app/Exceptions/HttpFetchException.php @@ -0,0 +1,5 @@ +app->make(Image::class), $this->app->make(ImageManager::class), $this->app->make(Factory::class), - $this->app->make(Repository::class) + $this->app->make(Repository::class), + $this->app->make(HttpFetcher::class) ); }); } diff --git a/app/Uploads/HttpFetcher.php b/app/Uploads/HttpFetcher.php new file mode 100644 index 000000000..3ebe17eee --- /dev/null +++ b/app/Uploads/HttpFetcher.php @@ -0,0 +1,34 @@ + $uri, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_CONNECTTIMEOUT => 5 + ]); + + $data = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + + if ($err) { + throw new HttpFetchException($err); + } + + return $data; + } + +} \ No newline at end of file diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index d5f4068ef..1dd8b713d 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -1,6 +1,7 @@ image = $image; $this->imageTool = $imageTool; $this->cache = $cache; + $this->http = $http; parent::__construct($fileSystem); } @@ -95,8 +99,9 @@ class ImageService extends UploadService private function saveNewFromUrl($url, $type, $imageName = false) { $imageName = $imageName ? $imageName : basename($url); - $imageData = file_get_contents($url); - if ($imageData === false) { + try { + $imageData = $this->http->fetch($url); + } catch (HttpFetchException $exception) { throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); } return $this->saveNew($imageName, $imageData, $type); @@ -322,7 +327,13 @@ class ImageService extends UploadService */ protected function getAvatarUrl() { - return trim(config('services.avatar_url')); + $url = trim(config('services.avatar_url')); + + if (empty($url) && !config('services.disable_services')) { + $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon'; + } + + return $url; } /** @@ -392,14 +403,7 @@ class ImageService extends UploadService } } else { try { - $ch = curl_init(); - curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]); - $imageData = curl_exec($ch); - $err = curl_error($ch); - curl_close($ch); - if ($err) { - throw new \Exception("Image fetch failed, Received error: " . $err); - } + $imageData = $this->http->fetch($uri); } catch (\Exception $e) { } } diff --git a/composer.json b/composer.json index 2850eb235..48b977e23 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "ext-xml": "*", "ext-mbstring": "*", "ext-gd": "*", + "ext-curl": "*", "laravel/framework": "~5.5.44", "fideloper/proxy": "~3.3", "intervention/image": "^2.4", diff --git a/config/services.php b/config/services.php index 7b9cf4e74..a07fab23f 100644 --- a/config/services.php +++ b/config/services.php @@ -21,9 +21,7 @@ return [ 'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)), // URL for fetching avatars - 'avatar_url' => env('AVATAR_URL', - env('DISABLE_EXTERNAL_SERVICES', false) ? false : 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon' - ), + 'avatar_url' => env('AVATAR_URL', ''), // Callback URL for social authentication methods 'callback_url' => env('APP_URL', false), diff --git a/phpunit.xml b/phpunit.xml index 3d18d9bbf..804afcf5d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,6 +31,7 @@ + diff --git a/tests/AttachmentTest.php b/tests/Uploads/AttachmentTest.php similarity index 100% rename from tests/AttachmentTest.php rename to tests/Uploads/AttachmentTest.php diff --git a/tests/Uploads/AvatarTest.php b/tests/Uploads/AvatarTest.php new file mode 100644 index 000000000..ecf7037a9 --- /dev/null +++ b/tests/Uploads/AvatarTest.php @@ -0,0 +1,84 @@ +asAdmin()->post('/settings/users/create', [ + 'name' => $user->name, + 'email' => $user->email, + 'password' => 'testing', + 'password-confirm' => 'testing', + ]); + return User::where('email', '=', $user->email)->first(); + } + + protected function assertImageFetchFrom(string $url) + { + $http = \Mockery::mock(HttpFetcher::class); + $this->app->instance(HttpFetcher::class, $http); + + $http->shouldReceive('fetch') + ->once()->with($url) + ->andReturn($this->getTestImageContent()); + } + + protected function deleteUserImage(User $user) + { + $this->deleteImage($user->avatar->path); + } + + public function test_gravatar_fetched_on_user_create() + { + config()->set([ + 'services.disable_services' => false, + ]); + $user = factory(User::class)->make(); + $this->assertImageFetchFrom('https://www.gravatar.com/avatar/'.md5(strtolower($user->email)).'?s=500&d=identicon'); + + $user = $this->createUserRequest($user); + $this->assertDatabaseHas('images', [ + 'type' => 'user', + 'created_by' => $user->id + ]); + $this->deleteUserImage($user); + } + + + public function test_custom_url_used_if_set() + { + config()->set([ + 'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}', + ]); + + $user = factory(User::class)->make(); + $url = 'https://example.com/'. urlencode(strtolower($user->email)) .'/'. md5(strtolower($user->email)).'/500'; + $this->assertImageFetchFrom($url); + + $user = $this->createUserRequest($user); + $this->deleteUserImage($user); + } + + public function test_avatar_not_fetched_if_no_custom_url_and_services_disabled() + { + config()->set([ + 'services.disable_services' => true, + ]); + + $user = factory(User::class)->make(); + + $http = \Mockery::mock(HttpFetcher::class); + $this->app->instance(HttpFetcher::class, $http); + $http->shouldNotReceive('fetch'); + + $this->createUserRequest($user); + } + +} diff --git a/tests/ImageTest.php b/tests/Uploads/ImageTest.php similarity index 85% rename from tests/ImageTest.php rename to tests/Uploads/ImageTest.php index 38bac2cca..6dafa7f5a 100644 --- a/tests/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -1,67 +1,15 @@ -getTestImageFilePath(), $fileName, 'image/png', 5238); - } - - /** - * Get the path for a test image. - * @param $type - * @param $fileName - * @return string - */ - protected function getTestImagePath($type, $fileName) - { - return '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/' . $fileName; - } - - /** - * Uploads an image with the given name. - * @param $name - * @param int $uploadedTo - * @return \Illuminate\Foundation\Testing\TestResponse - */ - protected function uploadImage($name, $uploadedTo = 0) - { - $file = $this->getTestImage($name); - return $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); - } - - /** - * Delete an uploaded image. - * @param $relPath - */ - protected function deleteImage($relPath) - { - $path = public_path($relPath); - if (file_exists($path)) { - unlink($path); - } - } + use UsesImages; public function test_image_upload() { diff --git a/tests/Uploads/UsesImages.php b/tests/Uploads/UsesImages.php new file mode 100644 index 000000000..16cb7c2b9 --- /dev/null +++ b/tests/Uploads/UsesImages.php @@ -0,0 +1,69 @@ +getTestImageFilePath(), $fileName, 'image/png', 5238); + } + + /** + * Get the raw file data for the test image. + * @return false|string + */ + protected function getTestImageContent() + { + return file_get_contents($this->getTestImageFilePath()); + } + + /** + * Get the path for a test image. + * @param $type + * @param $fileName + * @return string + */ + protected function getTestImagePath($type, $fileName) + { + return '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/' . $fileName; + } + + /** + * Uploads an image with the given name. + * @param $name + * @param int $uploadedTo + * @return \Illuminate\Foundation\Testing\TestResponse + */ + protected function uploadImage($name, $uploadedTo = 0) + { + $file = $this->getTestImage($name); + return $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + } + + /** + * Delete an uploaded image. + * @param $relPath + */ + protected function deleteImage($relPath) + { + $path = public_path($relPath); + if (file_exists($path)) { + unlink($path); + } + } + +} \ No newline at end of file