diff --git a/.env.example b/.env.example index 6d9189b5d..91e59f966 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,9 @@ GOOGLE_APP_SECRET=false # URL used for social login redirects, NO TRAILING SLASH APP_URL=http://bookstack.dev +# External services +USE_GRAVATAR=true + # Mail settings MAIL_DRIVER=smtp MAIL_HOST=localhost diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 23f5446d6..146dd0c05 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -33,7 +33,7 @@ class ImageController extends Controller /** - * Get all gallery images, Paginated + * Get all images for a specific type, Paginated * @param int $page * @return \Illuminate\Http\JsonResponse */ @@ -43,6 +43,17 @@ class ImageController extends Controller return response()->json($imgData); } + /** + * Get all images for a user. + * @param int $page + * @return \Illuminate\Http\JsonResponse + */ + public function getAllForUserType($page = 0) + { + $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id); + return response()->json($imgData); + } + /** * Handles image uploads for use on pages. diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index d1c328b86..c3f08a2f8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -62,7 +62,7 @@ class UserController extends Controller $this->checkPermission('user-create'); $this->validate($request, [ 'name' => 'required', - 'email' => 'required|email', + 'email' => 'required|email|unique:users,email', 'password' => 'required|min:5', 'password-confirm' => 'required|same:password', 'role' => 'required|exists:roles,id' diff --git a/app/Http/routes.php b/app/Http/routes.php index 274fccbff..23d4c33ab 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -57,6 +57,9 @@ Route::group(['middleware' => 'auth'], function () { // Image routes Route::group(['prefix' => 'images'], function() { + // Get for user images + Route::get('/user/all', 'ImageController@getAllForUserType'); + Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); // Standard get, update and deletion for all types Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); Route::put('/update/{imageId}', 'ImageController@update'); diff --git a/app/Image.php b/app/Image.php index 66d54ba30..651c618e6 100644 --- a/app/Image.php +++ b/app/Image.php @@ -3,9 +3,10 @@ namespace BookStack; +use Illuminate\Database\Eloquent\Model; use Images; -class Image +class Image extends Model { use Ownable; @@ -16,9 +17,10 @@ class Image * @param int $width * @param int $height * @param bool|false $hardCrop + * @return string */ public function getThumb($width, $height, $hardCrop = false) { - Images::getThumbnail($this, $width, $height, $hardCrop); + return Images::getThumbnail($this, $width, $height, $hardCrop); } } diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 56b0ba98d..d41909ac5 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -17,7 +17,7 @@ class ImageRepo * @param Image $image * @param ImageService $imageService */ - public function __construct(Image $image,ImageService $imageService) + public function __construct(Image $image, ImageService $imageService) { $this->image = $image; $this->imageService = $imageService; @@ -40,12 +40,18 @@ class ImageRepo * @param string $type * @param int $page * @param int $pageSize + * @param bool|int $userFilter * @return array */ - public function getPaginatedByType($type, $page = 0, $pageSize = 24) + public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false) { - $images = $this->image->where('type', '=', strtolower($type)) - ->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); + $images = $this->image->where('type', '=', strtolower($type)); + + if ($userFilter !== false) { + $images = $images->where('created_by', '=', $userFilter); + } + + $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); $hasMore = count($images) > $pageSize; $returnImages = $images->take(24); @@ -54,7 +60,7 @@ class ImageRepo }); return [ - 'images' => $returnImages, + 'images' => $returnImages, 'hasMore' => $hasMore ]; } @@ -67,7 +73,7 @@ class ImageRepo */ public function saveNew(UploadedFile $uploadFile, $type) { - $image = $this->imageService->saveNew($this->image, $uploadFile, $type); + $image = $this->imageService->saveNewFromUpload($uploadFile, $type); $this->loadThumbs($image); return $image; } diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index e6ee4cf0b..57293209c 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -1,6 +1,7 @@ cache = $cache; } - public function saveNew(Image $image, UploadedFile $uploadedFile, $type) + /** + * Saves a new image from an upload. + * @param UploadedFile $uploadedFile + * @param string $type + * @return mixed + */ + public function saveNewFromUpload(UploadedFile $uploadedFile, $type) + { + $imageName = $uploadedFile->getClientOriginalName(); + $imageData = file_get_contents($uploadedFile->getRealPath()); + return $this->saveNew($imageName, $imageData, $type); + } + + + /** + * Gets an image from url and saves it to the database. + * @param $url + * @param string $type + * @param bool|string $imageName + * @return mixed + * @throws \Exception + */ + private function saveNewFromUrl($url, $type, $imageName = false) + { + $imageName = $imageName ? $imageName : basename($url); + $imageData = file_get_contents($url); + if($imageData === false) throw new \Exception('Cannot get image from ' . $url); + return $this->saveNew($imageName, $imageData, $type); + } + + /** + * Saves a new image + * @param string $imageName + * @param string $imageData + * @param string $type + * @return Image + */ + private function saveNew($imageName, $imageData, $type) { $storage = $this->getStorage(); $secureUploads = Setting::get('app-secure-images'); - $imageName = str_replace(' ', '-', $uploadedFile->getClientOriginalName()); + $imageName = str_replace(' ', '-', $imageName); if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; @@ -48,14 +86,14 @@ class ImageService } $fullPath = $imagePath . $imageName; - $storage->put($fullPath, file_get_contents($uploadedFile->getRealPath())); + $storage->put($fullPath, $imageData); $userId = auth()->user()->id; - $image = $image->forceCreate([ - 'name' => $imageName, - 'path' => $fullPath, - 'url' => $this->getPublicUrl($fullPath), - 'type' => $type, + $image = Image::forceCreate([ + 'name' => $imageName, + 'path' => $fullPath, + 'url' => $this->getPublicUrl($fullPath), + 'type' => $type, 'created_by' => $userId, 'updated_by' => $userId ]); @@ -137,6 +175,26 @@ class ImageService return true; } + /** + * Save a gravatar image and set a the profile image for a user. + * @param User $user + * @param int $size + * @return mixed + */ + public function saveUserGravatar(User $user, $size = 500) + { + if (!env('USE_GRAVATAR', false)) return false; + $emailHash = md5(strtolower(trim($user->email))); + $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; + $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); + $image = $this->saveNewFromUrl($url, 'user', $imageName); + $image->created_by = $user->id; + $image->save(); + $user->avatar()->associate($image); + $user->save(); + return $image; + } + /** * Get the storage that will be used for storing images. * @return FileSystemInstance diff --git a/app/User.php b/app/User.php index 570789f37..bf2b14ac4 100644 --- a/app/User.php +++ b/app/User.php @@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon * * @var array */ - protected $fillable = ['name', 'email', 'password']; + protected $fillable = ['name', 'email', 'password', 'image_id']; /** * The attributes excluded from the model's JSON form. @@ -145,8 +145,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ public function getAvatar($size = 50) { - $emailHash = md5(strtolower(trim($this->email))); - return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; + if ($this->image_id === 0 || $this->image_id === null) return '/user_avatar.png'; + return $this->avatar->getThumb($size, $size, true); + } + + /** + * Get the avatar for the user. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function avatar() + { + return $this->belongsTo('BookStack\Image', 'image_id'); } /** diff --git a/database/migrations/2015_12_09_195748_add_user_avatars.php b/database/migrations/2015_12_09_195748_add_user_avatars.php new file mode 100644 index 000000000..47cb027fa --- /dev/null +++ b/database/migrations/2015_12_09_195748_add_user_avatars.php @@ -0,0 +1,31 @@ +integer('image_id')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('image_id'); + }); + } +} diff --git a/public/user_avatar.png b/public/user_avatar.png new file mode 100644 index 000000000..26440a9cc Binary files /dev/null and b/public/user_avatar.png differ diff --git a/resources/assets/js/components/image-manager.vue b/resources/assets/js/components/image-manager.vue index c47fee184..dd4a77d57 100644 --- a/resources/assets/js/components/image-manager.vue +++ b/resources/assets/js/components/image-manager.vue @@ -80,15 +80,6 @@ imageType: { type: String, required: true - }, - resizeWidth: { - type: String - }, - resizeHeight: { - type: String - }, - resizeCrop: { - type: Boolean } }, @@ -137,21 +128,7 @@ }, returnCallback: function (image) { - var _this = this; - var isResized = _this.resizeWidth && _this.resizeHeight; - - if (!isResized) { - _this.callback(image); - return; - } - - var cropped = _this.resizeCrop ? 'true' : 'false'; - var requestString = '/images/thumb/' + image.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped; - _this.$http.get(requestString, function(data) { - image.thumbs.custom = data.url; - _this.callback(image); - }); - + this.callback(image); }, imageClick: function (image) { diff --git a/resources/assets/js/components/image-picker.vue b/resources/assets/js/components/image-picker.vue index 4318ea1f0..7be976caf 100644 --- a/resources/assets/js/components/image-picker.vue +++ b/resources/assets/js/components/image-picker.vue @@ -7,31 +7,89 @@
- | - + | +