Abstracts imageservice http interaction.
Closes #1193
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class HttpFetchException extends Exception {}
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
+use BookStack\Uploads\HttpFetcher;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Contracts\Cache\Repository;
$this->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)
);
});
}
--- /dev/null
+<?php namespace BookStack\Uploads;
+
+use BookStack\Exceptions\HttpFetchException;
+
+class HttpFetcher
+{
+
+ /**
+ * Fetch content from an external URI.
+ * @param string $uri
+ * @return bool|string
+ * @throws HttpFetchException
+ */
+ public function fetch(string $uri)
+ {
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $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
<?php namespace BookStack\Uploads;
use BookStack\Auth\User;
+use BookStack\Exceptions\HttpFetchException;
use BookStack\Exceptions\ImageUploadException;
use DB;
use Exception;
protected $cache;
protected $storageUrl;
protected $image;
+ protected $http;
/**
* ImageService constructor.
* @param ImageManager $imageTool
* @param FileSystem $fileSystem
* @param Cache $cache
+ * @param HttpFetcher $http
*/
- public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
+ public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
{
$this->image = $image;
$this->imageTool = $imageTool;
$this->cache = $cache;
+ $this->http = $http;
parent::__construct($fileSystem);
}
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);
*/
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;
}
/**
}
} 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) {
}
}
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*",
+ "ext-curl": "*",
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
'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),
<env name="MAIL_DRIVER" value="log"/>
<env name="AUTH_METHOD" value="standard"/>
<env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
+ <env name="AVATAR_URL" value=""/>
<env name="LDAP_VERSION" value="3"/>
<env name="STORAGE_TYPE" value="local"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
--- /dev/null
+<?php namespace Tests\Uploads;
+
+use BookStack\Auth\User;
+use BookStack\Uploads\HttpFetcher;
+use Tests\TestCase;
+
+class AvatarTest extends TestCase
+{
+ use UsesImages;
+
+
+ protected function createUserRequest($user)
+ {
+ $resp = $this->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);
+ }
+
+}
-<?php namespace Tests;
+<?php namespace Tests\Uploads;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
use BookStack\Uploads\ImageService;
+use Tests\TestCase;
class ImageTest extends TestCase
{
- /**
- * Get the path to our basic test image.
- * @return string
- */
- protected function getTestImageFilePath()
- {
- return base_path('tests/test-data/test-image.png');
- }
-
- /**
- * Get a test image that can be uploaded
- * @param $fileName
- * @return \Illuminate\Http\UploadedFile
- */
- protected function getTestImage($fileName)
- {
- return new \Illuminate\Http\UploadedFile($this->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()
{
--- /dev/null
+<?php namespace Tests\Uploads;
+
+
+trait UsesImages
+{
+ /**
+ * Get the path to our basic test image.
+ * @return string
+ */
+ protected function getTestImageFilePath()
+ {
+ return base_path('tests/test-data/test-image.png');
+ }
+
+ /**
+ * Get a test image that can be uploaded
+ * @param $fileName
+ * @return \Illuminate\Http\UploadedFile
+ */
+ protected function getTestImage($fileName)
+ {
+ return new \Illuminate\Http\UploadedFile($this->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