From 8f7c642f320f87e5c6b11b11c64089d56a51c1b9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 9 Dec 2015 22:30:55 +0000 Subject: [PATCH] Added custom user avatars --- .env.example | 3 + app/Http/Controllers/ImageController.php | 13 ++- app/Http/Controllers/UserController.php | 2 +- app/Http/routes.php | 3 + app/Image.php | 6 +- app/Repos/ImageRepo.php | 18 +++-- app/Services/ImageService.php | 74 ++++++++++++++++-- app/User.php | 15 +++- .../2015_12_09_195748_add_user_avatars.php | 31 ++++++++ public/user_avatar.png | Bin 0 -> 7405 bytes .../assets/js/components/image-manager.vue | 25 +----- .../assets/js/components/image-picker.vue | 74 ++++++++++++++++-- resources/assets/sass/styles.scss | 4 + resources/views/settings/index.blade.php | 4 +- resources/views/users/edit.blade.php | 23 +++--- resources/views/users/form.blade.php | 3 +- 16 files changed, 230 insertions(+), 68 deletions(-) create mode 100644 database/migrations/2015_12_09_195748_add_user_avatars.php create mode 100644 public/user_avatar.png 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 0000000000000000000000000000000000000000..26440a9cc7a91301b59732edcf22d81211d701e6 GIT binary patch literal 7405 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
- | - + | +