# 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
/**
- * Get all gallery images, Paginated
+ * Get all images for a specific type, Paginated
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
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.
$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'
// 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');
namespace BookStack;
+use Illuminate\Database\Eloquent\Model;
use Images;
-class Image
+class Image extends Model
{
use Ownable;
* @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);
}
}
* @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;
* @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);
});
return [
- 'images' => $returnImages,
+ 'images' => $returnImages,
'hasMore' => $hasMore
];
}
*/
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;
}
<?php namespace BookStack\Services;
use BookStack\Image;
+use BookStack\User;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
$this->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;
}
$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
]);
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
*
* @var array
*/
- protected $fillable = ['name', 'email', 'password'];
+ protected $fillable = ['name', 'email', 'password', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
*/
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');
}
/**
--- /dev/null
+<?php
+
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddUserAvatars extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->integer('image_id')->default(0);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('image_id');
+ });
+ }
+}
imageType: {
type: String,
required: true
- },
- resizeWidth: {
- type: String
- },
- resizeHeight: {
- type: String
- },
- resizeCrop: {
- type: Boolean
}
},
},
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) {
</div>
<button class="button" type="button" @click="showImageManager">Select Image</button>
<br>
- <button class="text-button" @click="reset" type="button">Reset</button> <span class="sep">|</span> <button class="text-button neg" v-on:click="remove" type="button">Remove</button>
- <input type="hidden" :name="name" :id="name" v-model="image">
+ <button class="text-button" @click="reset" type="button">Reset</button> <span v-show="showRemove" class="sep">|</span> <button v-show="showRemove" class="text-button neg" @click="remove" type="button">Remove</button>
+ <input type="hidden" :name="name" :id="name" v-model="value">
</div>
</template>
<script>
module.exports = {
- props: ['currentImage', 'name', 'imageClass', 'defaultImage'],
+ props: {
+ currentImage: {
+ required: true,
+ type: String
+ },
+ currentId: {
+ required: false,
+ default: 'false',
+ type: String
+ },
+ name: {
+ required: true,
+ type: String
+ },
+ defaultImage: {
+ required: true,
+ type: String
+ },
+ imageClass: {
+ required: true,
+ type: String
+ },
+ resizeWidth: {
+ type: String
+ },
+ resizeHeight: {
+ type: String
+ },
+ resizeCrop: {
+ type: Boolean
+ },
+ showRemove: {
+ type: Boolean,
+ default: 'true'
+ }
+ },
data: function() {
return {
- image: this.currentImage
+ image: this.currentImage,
+ value: false
}
},
+ compiled: function() {
+ this.value = this.currentId === 'false' ? this.currentImage : this.currentId;
+ },
methods: {
+ setCurrentValue: function(imageModel, imageUrl) {
+ this.image = imageUrl;
+ this.value = this.currentId === 'false' ? imageUrl : imageModel.id;
+ },
showImageManager: function(e) {
var _this = this;
ImageManager.show(function(image) {
- _this.image = image.thumbs.custom || image.url;
+ _this.updateImageFromModel(image);
});
},
reset: function() {
- this.image = '';
+ this.setCurrentValue({id: 0}, this.defaultImage);
},
remove: function() {
this.image = 'none';
+ },
+ updateImageFromModel: function(model) {
+ var _this = this;
+ var isResized = _this.resizeWidth && _this.resizeHeight;
+
+ if (!isResized) {
+ _this.setCurrentValue(model, model.url);
+ return;
+ }
+
+ var cropped = _this.resizeCrop ? 'true' : 'false';
+ var requestString = '/images/thumb/' + model.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped;
+ _this.$http.get(requestString, function(data) {
+ _this.setCurrentValue(model, data.url);
+ });
}
}
};
width: 40px;
height: 40px;
}
+ &.large {
+ width: 80px;
+ height: 80px;
+ }
}
// System wide notifications
<div class="form-group" id="logo-control">
<label for="setting-app-logo">Application Logo</label>
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
- <image-picker current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
+ <image-picker resize-height="43" resize-width="200" current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
</div>
</div>
</div>
</div>
-<image-manager image-type="system" resize-height="43" resize-width="200"></image-manager>
+<image-manager image-type="system"></image-manager>
@stop
<div class="container small">
-
+ <form action="/users/{{$user->id}}" method="post">
<div class="row">
<div class="col-md-6">
<h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
- <form action="/users/{{$user->id}}" method="post">
- {!! csrf_field() !!}
- <input type="hidden" name="_method" value="put">
- @include('users/form', ['model' => $user])
- </form>
+ {!! csrf_field() !!}
+ <input type="hidden" name="_method" value="put">
+ @include('users/form', ['model' => $user])
+
</div>
<div class="col-md-6">
<h1> </h1>
- <div class="shaded padded margin-top">
- <p>
- <img class="avatar" src="{{ $user->getAvatar(80) }}" alt="{{ $user->name }}">
- </p>
- <p class="text-muted">You can change your profile picture at <a href="http://en.gravatar.com/">Gravatar</a>.</p>
+ <div class="form-group" id="logo-control">
+ <label for="user-avatar">User Avatar</label>
+ <p class="small">This image should be approx 256px square.</p>
+ <image-picker resize-height="512" resize-width="512" current-image="{{ $user->getAvatar(80) }}" current-id="{{ $user->image_id }}" default-image="/user_avatar.png" name="image_id" show-remove="false" image-class="avatar large"></image-picker>
</div>
</div>
</div>
+ </form>
<hr class="margin-top large">
</div>
<p class="margin-top large"><br></p>
-
+ <image-manager image-type="user"></image-manager>
@stop
<div class="form-group">
<a href="/users" class="button muted">Cancel</a>
<button class="button pos" type="submit">Save</button>
-</div>
\ No newline at end of file
+</div>
+