Showing
60 changed files
with
1163 additions
and
273 deletions
| ... | @@ -14,14 +14,28 @@ CACHE_DRIVER=file | ... | @@ -14,14 +14,28 @@ CACHE_DRIVER=file |
| 14 | SESSION_DRIVER=file | 14 | SESSION_DRIVER=file |
| 15 | QUEUE_DRIVER=sync | 15 | QUEUE_DRIVER=sync |
| 16 | 16 | ||
| 17 | +# Storage | ||
| 18 | +STORAGE_TYPE=local | ||
| 19 | +# Amazon S3 Config | ||
| 20 | +STORAGE_S3_KEY=false | ||
| 21 | +STORAGE_S3_SECRET=false | ||
| 22 | +STORAGE_S3_REGION=false | ||
| 23 | +STORAGE_S3_BUCKET=false | ||
| 24 | +# Storage URL | ||
| 25 | +# Used to prefix image urls for when using custom domains/cdns | ||
| 26 | +STORAGE_URL=false | ||
| 27 | + | ||
| 17 | # Social Authentication information. Defaults as off. | 28 | # Social Authentication information. Defaults as off. |
| 18 | GITHUB_APP_ID=false | 29 | GITHUB_APP_ID=false |
| 19 | GITHUB_APP_SECRET=false | 30 | GITHUB_APP_SECRET=false |
| 20 | GOOGLE_APP_ID=false | 31 | GOOGLE_APP_ID=false |
| 21 | GOOGLE_APP_SECRET=false | 32 | GOOGLE_APP_SECRET=false |
| 22 | -# URL for social login redirects, NO TRAILING SLASH | 33 | +# URL used for social login redirects, NO TRAILING SLASH |
| 23 | APP_URL=http://bookstack.dev | 34 | APP_URL=http://bookstack.dev |
| 24 | 35 | ||
| 36 | +# External services | ||
| 37 | +USE_GRAVATAR=true | ||
| 38 | + | ||
| 25 | # Mail settings | 39 | # Mail settings |
| 26 | MAIL_DRIVER=smtp | 40 | MAIL_DRIVER=smtp |
| 27 | MAIL_HOST=localhost | 41 | MAIL_HOST=localhost | ... | ... |
| ... | @@ -7,23 +7,7 @@ use Illuminate\Database\Eloquent\Model; | ... | @@ -7,23 +7,7 @@ use Illuminate\Database\Eloquent\Model; |
| 7 | abstract class Entity extends Model | 7 | abstract class Entity extends Model |
| 8 | { | 8 | { |
| 9 | 9 | ||
| 10 | - /** | 10 | + use Ownable; |
| 11 | - * Relation for the user that created this entity. | ||
| 12 | - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 13 | - */ | ||
| 14 | - public function createdBy() | ||
| 15 | - { | ||
| 16 | - return $this->belongsTo('BookStack\User', 'created_by'); | ||
| 17 | - } | ||
| 18 | - | ||
| 19 | - /** | ||
| 20 | - * Relation for the user that updated this entity. | ||
| 21 | - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 22 | - */ | ||
| 23 | - public function updatedBy() | ||
| 24 | - { | ||
| 25 | - return $this->belongsTo('BookStack\User', 'updated_by'); | ||
| 26 | - } | ||
| 27 | 11 | ||
| 28 | /** | 12 | /** |
| 29 | * Compares this entity to another given entity. | 13 | * Compares this entity to another given entity. |
| ... | @@ -97,19 +81,30 @@ abstract class Entity extends Model | ... | @@ -97,19 +81,30 @@ abstract class Entity extends Model |
| 97 | */ | 81 | */ |
| 98 | public static function isA($type) | 82 | public static function isA($type) |
| 99 | { | 83 | { |
| 100 | - return static::getName() === strtolower($type); | 84 | + return static::getClassName() === strtolower($type); |
| 101 | } | 85 | } |
| 102 | 86 | ||
| 103 | /** | 87 | /** |
| 104 | * Gets the class name. | 88 | * Gets the class name. |
| 105 | * @return string | 89 | * @return string |
| 106 | */ | 90 | */ |
| 107 | - public static function getName() | 91 | + public static function getClassName() |
| 108 | { | 92 | { |
| 109 | return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]); | 93 | return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]); |
| 110 | } | 94 | } |
| 111 | 95 | ||
| 112 | /** | 96 | /** |
| 97 | + *Gets a limited-length version of the entities name. | ||
| 98 | + * @param int $length | ||
| 99 | + * @return string | ||
| 100 | + */ | ||
| 101 | + public function getShortName($length = 25) | ||
| 102 | + { | ||
| 103 | + if(strlen($this->name) <= $length) return $this->name; | ||
| 104 | + return substr($this->name, 0, $length-3) . '...'; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + /** | ||
| 113 | * Perform a full-text search on this entity. | 108 | * Perform a full-text search on this entity. |
| 114 | * @param string[] $fieldsToSearch | 109 | * @param string[] $fieldsToSearch |
| 115 | * @param string[] $terms | 110 | * @param string[] $terms |
| ... | @@ -123,20 +118,20 @@ abstract class Entity extends Model | ... | @@ -123,20 +118,20 @@ abstract class Entity extends Model |
| 123 | $termString .= $term . '* '; | 118 | $termString .= $term . '* '; |
| 124 | } | 119 | } |
| 125 | $fields = implode(',', $fieldsToSearch); | 120 | $fields = implode(',', $fieldsToSearch); |
| 126 | - $search = static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); | 121 | + $termStringEscaped = \DB::connection()->getPdo()->quote($termString); |
| 122 | + $search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance')); | ||
| 123 | + $search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]); | ||
| 124 | + | ||
| 125 | + // Add additional where terms | ||
| 127 | foreach ($wheres as $whereTerm) { | 126 | foreach ($wheres as $whereTerm) { |
| 128 | $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); | 127 | $search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]); |
| 129 | } | 128 | } |
| 130 | 129 | ||
| 131 | - if (!static::isA('book')) { | 130 | + // Load in relations |
| 132 | - $search = $search->with('book'); | 131 | + if (!static::isA('book')) $search = $search->with('book'); |
| 133 | - } | 132 | + if (static::isA('page')) $search = $search->with('chapter'); |
| 134 | - | ||
| 135 | - if (static::isA('page')) { | ||
| 136 | - $search = $search->with('chapter'); | ||
| 137 | - } | ||
| 138 | 133 | ||
| 139 | - return $search->get(); | 134 | + return $search->orderBy('title_relevance', 'desc')->get(); |
| 140 | } | 135 | } |
| 141 | 136 | ||
| 142 | /** | 137 | /** | ... | ... |
| ... | @@ -42,8 +42,10 @@ class BookController extends Controller | ... | @@ -42,8 +42,10 @@ class BookController extends Controller |
| 42 | public function index() | 42 | public function index() |
| 43 | { | 43 | { |
| 44 | $books = $this->bookRepo->getAllPaginated(10); | 44 | $books = $this->bookRepo->getAllPaginated(10); |
| 45 | - $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(10, 0) : false; | 45 | + $recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(4, 0) : false; |
| 46 | - return view('books/index', ['books' => $books, 'recents' => $recents]); | 46 | + $popular = $this->bookRepo->getPopular(4, 0); |
| 47 | + $this->setPageTitle('Books'); | ||
| 48 | + return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]); | ||
| 47 | } | 49 | } |
| 48 | 50 | ||
| 49 | /** | 51 | /** |
| ... | @@ -54,6 +56,7 @@ class BookController extends Controller | ... | @@ -54,6 +56,7 @@ class BookController extends Controller |
| 54 | public function create() | 56 | public function create() |
| 55 | { | 57 | { |
| 56 | $this->checkPermission('book-create'); | 58 | $this->checkPermission('book-create'); |
| 59 | + $this->setPageTitle('Create New Book'); | ||
| 57 | return view('books/create'); | 60 | return view('books/create'); |
| 58 | } | 61 | } |
| 59 | 62 | ||
| ... | @@ -88,8 +91,9 @@ class BookController extends Controller | ... | @@ -88,8 +91,9 @@ class BookController extends Controller |
| 88 | public function show($slug) | 91 | public function show($slug) |
| 89 | { | 92 | { |
| 90 | $book = $this->bookRepo->getBySlug($slug); | 93 | $book = $this->bookRepo->getBySlug($slug); |
| 91 | - Views::add($book); | ||
| 92 | $bookChildren = $this->bookRepo->getChildren($book); | 94 | $bookChildren = $this->bookRepo->getChildren($book); |
| 95 | + Views::add($book); | ||
| 96 | + $this->setPageTitle($book->getShortName()); | ||
| 93 | return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); | 97 | return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]); |
| 94 | } | 98 | } |
| 95 | 99 | ||
| ... | @@ -103,6 +107,7 @@ class BookController extends Controller | ... | @@ -103,6 +107,7 @@ class BookController extends Controller |
| 103 | { | 107 | { |
| 104 | $this->checkPermission('book-update'); | 108 | $this->checkPermission('book-update'); |
| 105 | $book = $this->bookRepo->getBySlug($slug); | 109 | $book = $this->bookRepo->getBySlug($slug); |
| 110 | + $this->setPageTitle('Edit Book ' . $book->getShortName()); | ||
| 106 | return view('books/edit', ['book' => $book, 'current' => $book]); | 111 | return view('books/edit', ['book' => $book, 'current' => $book]); |
| 107 | } | 112 | } |
| 108 | 113 | ||
| ... | @@ -138,6 +143,7 @@ class BookController extends Controller | ... | @@ -138,6 +143,7 @@ class BookController extends Controller |
| 138 | { | 143 | { |
| 139 | $this->checkPermission('book-delete'); | 144 | $this->checkPermission('book-delete'); |
| 140 | $book = $this->bookRepo->getBySlug($bookSlug); | 145 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 146 | + $this->setPageTitle('Delete Book ' . $book->getShortName()); | ||
| 141 | return view('books/delete', ['book' => $book, 'current' => $book]); | 147 | return view('books/delete', ['book' => $book, 'current' => $book]); |
| 142 | } | 148 | } |
| 143 | 149 | ||
| ... | @@ -152,9 +158,16 @@ class BookController extends Controller | ... | @@ -152,9 +158,16 @@ class BookController extends Controller |
| 152 | $book = $this->bookRepo->getBySlug($bookSlug); | 158 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 153 | $bookChildren = $this->bookRepo->getChildren($book); | 159 | $bookChildren = $this->bookRepo->getChildren($book); |
| 154 | $books = $this->bookRepo->getAll(); | 160 | $books = $this->bookRepo->getAll(); |
| 161 | + $this->setPageTitle('Sort Book ' . $book->getShortName()); | ||
| 155 | return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); | 162 | return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]); |
| 156 | } | 163 | } |
| 157 | 164 | ||
| 165 | + /** | ||
| 166 | + * Shows the sort box for a single book. | ||
| 167 | + * Used via AJAX when loading in extra books to a sort. | ||
| 168 | + * @param $bookSlug | ||
| 169 | + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View | ||
| 170 | + */ | ||
| 158 | public function getSortItem($bookSlug) | 171 | public function getSortItem($bookSlug) |
| 159 | { | 172 | { |
| 160 | $book = $this->bookRepo->getBySlug($bookSlug); | 173 | $book = $this->bookRepo->getBySlug($bookSlug); | ... | ... |
| ... | @@ -40,6 +40,7 @@ class ChapterController extends Controller | ... | @@ -40,6 +40,7 @@ class ChapterController extends Controller |
| 40 | { | 40 | { |
| 41 | $this->checkPermission('chapter-create'); | 41 | $this->checkPermission('chapter-create'); |
| 42 | $book = $this->bookRepo->getBySlug($bookSlug); | 42 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 43 | + $this->setPageTitle('Create New Chapter'); | ||
| 43 | return view('chapters/create', ['book' => $book, 'current' => $book]); | 44 | return view('chapters/create', ['book' => $book, 'current' => $book]); |
| 44 | } | 45 | } |
| 45 | 46 | ||
| ... | @@ -79,6 +80,7 @@ class ChapterController extends Controller | ... | @@ -79,6 +80,7 @@ class ChapterController extends Controller |
| 79 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | 80 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); |
| 80 | $sidebarTree = $this->bookRepo->getChildren($book); | 81 | $sidebarTree = $this->bookRepo->getChildren($book); |
| 81 | Views::add($chapter); | 82 | Views::add($chapter); |
| 83 | + $this->setPageTitle($chapter->getShortName()); | ||
| 82 | return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]); | 84 | return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]); |
| 83 | } | 85 | } |
| 84 | 86 | ||
| ... | @@ -93,6 +95,7 @@ class ChapterController extends Controller | ... | @@ -93,6 +95,7 @@ class ChapterController extends Controller |
| 93 | $this->checkPermission('chapter-update'); | 95 | $this->checkPermission('chapter-update'); |
| 94 | $book = $this->bookRepo->getBySlug($bookSlug); | 96 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 95 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | 97 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); |
| 98 | + $this->setPageTitle('Edit Chapter' . $chapter->getShortName()); | ||
| 96 | return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); | 99 | return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); |
| 97 | } | 100 | } |
| 98 | 101 | ||
| ... | @@ -127,6 +130,7 @@ class ChapterController extends Controller | ... | @@ -127,6 +130,7 @@ class ChapterController extends Controller |
| 127 | $this->checkPermission('chapter-delete'); | 130 | $this->checkPermission('chapter-delete'); |
| 128 | $book = $this->bookRepo->getBySlug($bookSlug); | 131 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 129 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); | 132 | $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); |
| 133 | + $this->setPageTitle('Delete Chapter' . $chapter->getShortName()); | ||
| 130 | return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); | 134 | return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]); |
| 131 | } | 135 | } |
| 132 | 136 | ... | ... |
| ... | @@ -43,6 +43,15 @@ abstract class Controller extends BaseController | ... | @@ -43,6 +43,15 @@ abstract class Controller extends BaseController |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | /** | 45 | /** |
| 46 | + * Adds the page title into the view. | ||
| 47 | + * @param $title | ||
| 48 | + */ | ||
| 49 | + public function setPageTitle($title) | ||
| 50 | + { | ||
| 51 | + view()->share('pageTitle', $title); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + /** | ||
| 46 | * Checks for a permission. | 55 | * Checks for a permission. |
| 47 | * | 56 | * |
| 48 | * @param $permissionName | 57 | * @param $permissionName | ... | ... |
| ... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
| 2 | 2 | ||
| 3 | namespace BookStack\Http\Controllers; | 3 | namespace BookStack\Http\Controllers; |
| 4 | 4 | ||
| 5 | +use BookStack\Repos\ImageRepo; | ||
| 5 | use Illuminate\Filesystem\Filesystem as File; | 6 | use Illuminate\Filesystem\Filesystem as File; |
| 6 | use Illuminate\Http\Request; | 7 | use Illuminate\Http\Request; |
| 7 | use Illuminate\Support\Facades\Auth; | 8 | use Illuminate\Support\Facades\Auth; |
| ... | @@ -14,125 +15,78 @@ class ImageController extends Controller | ... | @@ -14,125 +15,78 @@ class ImageController extends Controller |
| 14 | { | 15 | { |
| 15 | protected $image; | 16 | protected $image; |
| 16 | protected $file; | 17 | protected $file; |
| 18 | + protected $imageRepo; | ||
| 17 | 19 | ||
| 18 | /** | 20 | /** |
| 19 | * ImageController constructor. | 21 | * ImageController constructor. |
| 20 | - * @param Image $image | 22 | + * @param Image $image |
| 21 | - * @param File $file | 23 | + * @param File $file |
| 24 | + * @param ImageRepo $imageRepo | ||
| 22 | */ | 25 | */ |
| 23 | - public function __construct(Image $image, File $file) | 26 | + public function __construct(Image $image, File $file, ImageRepo $imageRepo) |
| 24 | { | 27 | { |
| 25 | $this->image = $image; | 28 | $this->image = $image; |
| 26 | $this->file = $file; | 29 | $this->file = $file; |
| 30 | + $this->imageRepo = $imageRepo; | ||
| 27 | parent::__construct(); | 31 | parent::__construct(); |
| 28 | } | 32 | } |
| 29 | 33 | ||
| 30 | 34 | ||
| 31 | /** | 35 | /** |
| 32 | - * Get all images, Paginated | 36 | + * Get all images for a specific type, Paginated |
| 33 | * @param int $page | 37 | * @param int $page |
| 34 | * @return \Illuminate\Http\JsonResponse | 38 | * @return \Illuminate\Http\JsonResponse |
| 35 | */ | 39 | */ |
| 36 | - public function getAll($page = 0) | 40 | + public function getAllByType($type, $page = 0) |
| 37 | { | 41 | { |
| 38 | - $pageSize = 30; | 42 | + $imgData = $this->imageRepo->getPaginatedByType($type, $page); |
| 39 | - $images = $this->image->orderBy('created_at', 'desc') | 43 | + return response()->json($imgData); |
| 40 | - ->skip($page * $pageSize)->take($pageSize)->get(); | ||
| 41 | - foreach ($images as $image) { | ||
| 42 | - $this->loadSizes($image); | ||
| 43 | - } | ||
| 44 | - $hasMore = $this->image->orderBy('created_at', 'desc') | ||
| 45 | - ->skip(($page + 1) * $pageSize)->take($pageSize)->count() > 0; | ||
| 46 | - return response()->json([ | ||
| 47 | - 'images' => $images, | ||
| 48 | - 'hasMore' => $hasMore | ||
| 49 | - ]); | ||
| 50 | } | 44 | } |
| 51 | 45 | ||
| 52 | /** | 46 | /** |
| 53 | - * Loads the standard thumbnail sizes for an image. | 47 | + * Get all images for a user. |
| 54 | - * @param Image $image | 48 | + * @param int $page |
| 49 | + * @return \Illuminate\Http\JsonResponse | ||
| 55 | */ | 50 | */ |
| 56 | - private function loadSizes(Image $image) | 51 | + public function getAllForUserType($page = 0) |
| 57 | { | 52 | { |
| 58 | - $image->thumbnail = $this->getThumbnail($image, 150, 150); | 53 | + $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id); |
| 59 | - $image->display = $this->getThumbnail($image, 840, 0, true); | 54 | + return response()->json($imgData); |
| 60 | } | 55 | } |
| 61 | 56 | ||
| 62 | - /** | ||
| 63 | - * Get the thumbnail for an image. | ||
| 64 | - * If $keepRatio is true only the width will be used. | ||
| 65 | - * @param $image | ||
| 66 | - * @param int $width | ||
| 67 | - * @param int $height | ||
| 68 | - * @param bool $keepRatio | ||
| 69 | - * @return string | ||
| 70 | - */ | ||
| 71 | - public function getThumbnail($image, $width = 220, $height = 220, $keepRatio = false) | ||
| 72 | - { | ||
| 73 | - $explodedPath = explode('/', $image->url); | ||
| 74 | - $dirPrefix = $keepRatio ? 'scaled-' : 'thumbs-'; | ||
| 75 | - array_splice($explodedPath, 4, 0, [$dirPrefix . $width . '-' . $height]); | ||
| 76 | - $thumbPath = implode('/', $explodedPath); | ||
| 77 | - $thumbFilePath = public_path() . $thumbPath; | ||
| 78 | - | ||
| 79 | - // Return the thumbnail url path if already exists | ||
| 80 | - if (file_exists($thumbFilePath)) { | ||
| 81 | - return $thumbPath; | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | - // Otherwise create the thumbnail | ||
| 85 | - $thumb = ImageTool::make(public_path() . $image->url); | ||
| 86 | - if($keepRatio) { | ||
| 87 | - $thumb->resize($width, null, function ($constraint) { | ||
| 88 | - $constraint->aspectRatio(); | ||
| 89 | - $constraint->upsize(); | ||
| 90 | - }); | ||
| 91 | - } else { | ||
| 92 | - $thumb->fit($width, $height); | ||
| 93 | - } | ||
| 94 | - | ||
| 95 | - // Create thumbnail folder if it does not exist | ||
| 96 | - if (!file_exists(dirname($thumbFilePath))) { | ||
| 97 | - mkdir(dirname($thumbFilePath), 0775, true); | ||
| 98 | - } | ||
| 99 | - | ||
| 100 | - //Save Thumbnail | ||
| 101 | - $thumb->save($thumbFilePath); | ||
| 102 | - return $thumbPath; | ||
| 103 | - } | ||
| 104 | 57 | ||
| 105 | /** | 58 | /** |
| 106 | * Handles image uploads for use on pages. | 59 | * Handles image uploads for use on pages. |
| 60 | + * @param string $type | ||
| 107 | * @param Request $request | 61 | * @param Request $request |
| 108 | * @return \Illuminate\Http\JsonResponse | 62 | * @return \Illuminate\Http\JsonResponse |
| 109 | */ | 63 | */ |
| 110 | - public function upload(Request $request) | 64 | + public function uploadByType($type, Request $request) |
| 111 | { | 65 | { |
| 112 | $this->checkPermission('image-create'); | 66 | $this->checkPermission('image-create'); |
| 113 | $this->validate($request, [ | 67 | $this->validate($request, [ |
| 114 | 'file' => 'image|mimes:jpeg,gif,png' | 68 | 'file' => 'image|mimes:jpeg,gif,png' |
| 115 | ]); | 69 | ]); |
| 70 | + | ||
| 116 | $imageUpload = $request->file('file'); | 71 | $imageUpload = $request->file('file'); |
| 72 | + $image = $this->imageRepo->saveNew($imageUpload, $type); | ||
| 73 | + return response()->json($image); | ||
| 74 | + } | ||
| 117 | 75 | ||
| 118 | - $name = str_replace(' ', '-', $imageUpload->getClientOriginalName()); | 76 | + /** |
| 119 | - $storageName = substr(sha1(time()), 0, 10) . '-' . $name; | 77 | + * Generate a sized thumbnail for an image. |
| 120 | - $imagePath = '/uploads/images/' . Date('Y-m-M') . '/'; | 78 | + * @param $id |
| 121 | - $storagePath = public_path() . $imagePath; | 79 | + * @param $width |
| 122 | - $fullPath = $storagePath . $storageName; | 80 | + * @param $height |
| 123 | - while (file_exists($fullPath)) { | 81 | + * @param $crop |
| 124 | - $storageName = substr(sha1(rand()), 0, 3) . $storageName; | 82 | + * @return \Illuminate\Http\JsonResponse |
| 125 | - $fullPath = $storagePath . $storageName; | 83 | + */ |
| 126 | - } | 84 | + public function getThumbnail($id, $width, $height, $crop) |
| 127 | - $imageUpload->move($storagePath, $storageName); | 85 | + { |
| 128 | - // Create and save image object | 86 | + $this->checkPermission('image-create'); |
| 129 | - $this->image->name = $name; | 87 | + $image = $this->imageRepo->getById($id); |
| 130 | - $this->image->url = $imagePath . $storageName; | 88 | + $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); |
| 131 | - $this->image->created_by = auth()->user()->id; | 89 | + return response()->json(['url' => $thumbnailUrl]); |
| 132 | - $this->image->updated_by = auth()->user()->id; | ||
| 133 | - $this->image->save(); | ||
| 134 | - $this->loadSizes($this->image); | ||
| 135 | - return response()->json($this->image); | ||
| 136 | } | 90 | } |
| 137 | 91 | ||
| 138 | /** | 92 | /** |
| ... | @@ -147,13 +101,12 @@ class ImageController extends Controller | ... | @@ -147,13 +101,12 @@ class ImageController extends Controller |
| 147 | $this->validate($request, [ | 101 | $this->validate($request, [ |
| 148 | 'name' => 'required|min:2|string' | 102 | 'name' => 'required|min:2|string' |
| 149 | ]); | 103 | ]); |
| 150 | - $image = $this->image->findOrFail($imageId); | 104 | + $image = $this->imageRepo->getById($imageId); |
| 151 | - $image->fill($request->all()); | 105 | + $image = $this->imageRepo->updateImageDetails($image, $request->all()); |
| 152 | - $image->save(); | 106 | + return response()->json($image); |
| 153 | - $this->loadSizes($image); | ||
| 154 | - return response()->json($this->image); | ||
| 155 | } | 107 | } |
| 156 | 108 | ||
| 109 | + | ||
| 157 | /** | 110 | /** |
| 158 | * Deletes an image and all thumbnail/image files | 111 | * Deletes an image and all thumbnail/image files |
| 159 | * @param PageRepo $pageRepo | 112 | * @param PageRepo $pageRepo |
| ... | @@ -164,41 +117,18 @@ class ImageController extends Controller | ... | @@ -164,41 +117,18 @@ class ImageController extends Controller |
| 164 | public function destroy(PageRepo $pageRepo, Request $request, $id) | 117 | public function destroy(PageRepo $pageRepo, Request $request, $id) |
| 165 | { | 118 | { |
| 166 | $this->checkPermission('image-delete'); | 119 | $this->checkPermission('image-delete'); |
| 167 | - $image = $this->image->findOrFail($id); | 120 | + $image = $this->imageRepo->getById($id); |
| 168 | 121 | ||
| 169 | // Check if this image is used on any pages | 122 | // Check if this image is used on any pages |
| 170 | - $pageSearch = $pageRepo->searchForImage($image->url); | ||
| 171 | $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); | 123 | $isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true); |
| 172 | - if ($pageSearch !== false && !$isForced) { | 124 | + if (!$isForced) { |
| 173 | - return response()->json($pageSearch, 400); | 125 | + $pageSearch = $pageRepo->searchForImage($image->url); |
| 174 | - } | 126 | + if ($pageSearch !== false) { |
| 175 | - | 127 | + return response()->json($pageSearch, 400); |
| 176 | - // Delete files | ||
| 177 | - $folder = public_path() . dirname($image->url); | ||
| 178 | - $fileName = basename($image->url); | ||
| 179 | - | ||
| 180 | - // Delete thumbnails | ||
| 181 | - foreach (glob($folder . '/*') as $file) { | ||
| 182 | - if (is_dir($file)) { | ||
| 183 | - $thumbName = $file . '/' . $fileName; | ||
| 184 | - if (file_exists($file)) { | ||
| 185 | - unlink($thumbName); | ||
| 186 | - } | ||
| 187 | - // Remove thumb folder if empty | ||
| 188 | - if (count(glob($file . '/*')) === 0) { | ||
| 189 | - rmdir($file); | ||
| 190 | - } | ||
| 191 | } | 128 | } |
| 192 | } | 129 | } |
| 193 | 130 | ||
| 194 | - // Delete file and database entry | 131 | + $this->imageRepo->destroyImage($image); |
| 195 | - unlink($folder . '/' . $fileName); | ||
| 196 | - $image->delete(); | ||
| 197 | - | ||
| 198 | - // Delete parent folder if empty | ||
| 199 | - if (count(glob($folder . '/*')) === 0) { | ||
| 200 | - rmdir($folder); | ||
| 201 | - } | ||
| 202 | return response()->json('Image Deleted'); | 132 | return response()->json('Image Deleted'); |
| 203 | } | 133 | } |
| 204 | 134 | ... | ... |
| ... | @@ -46,6 +46,7 @@ class PageController extends Controller | ... | @@ -46,6 +46,7 @@ class PageController extends Controller |
| 46 | $this->checkPermission('page-create'); | 46 | $this->checkPermission('page-create'); |
| 47 | $book = $this->bookRepo->getBySlug($bookSlug); | 47 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 48 | $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; | 48 | $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false; |
| 49 | + $this->setPageTitle('Create New Page'); | ||
| 49 | return view('pages/create', ['book' => $book, 'chapter' => $chapter]); | 50 | return view('pages/create', ['book' => $book, 'chapter' => $chapter]); |
| 50 | } | 51 | } |
| 51 | 52 | ||
| ... | @@ -89,6 +90,7 @@ class PageController extends Controller | ... | @@ -89,6 +90,7 @@ class PageController extends Controller |
| 89 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | 90 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); |
| 90 | $sidebarTree = $this->bookRepo->getChildren($book); | 91 | $sidebarTree = $this->bookRepo->getChildren($book); |
| 91 | Views::add($page); | 92 | Views::add($page); |
| 93 | + $this->setPageTitle($page->getShortName()); | ||
| 92 | return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]); | 94 | return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]); |
| 93 | } | 95 | } |
| 94 | 96 | ||
| ... | @@ -104,6 +106,7 @@ class PageController extends Controller | ... | @@ -104,6 +106,7 @@ class PageController extends Controller |
| 104 | $this->checkPermission('page-update'); | 106 | $this->checkPermission('page-update'); |
| 105 | $book = $this->bookRepo->getBySlug($bookSlug); | 107 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 106 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | 108 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); |
| 109 | + $this->setPageTitle('Editing Page ' . $page->getShortName()); | ||
| 107 | return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); | 110 | return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); |
| 108 | } | 111 | } |
| 109 | 112 | ||
| ... | @@ -148,6 +151,7 @@ class PageController extends Controller | ... | @@ -148,6 +151,7 @@ class PageController extends Controller |
| 148 | $this->checkPermission('page-delete'); | 151 | $this->checkPermission('page-delete'); |
| 149 | $book = $this->bookRepo->getBySlug($bookSlug); | 152 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 150 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | 153 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); |
| 154 | + $this->setPageTitle('Delete Page ' . $page->getShortName()); | ||
| 151 | return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); | 155 | return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]); |
| 152 | } | 156 | } |
| 153 | 157 | ||
| ... | @@ -179,6 +183,7 @@ class PageController extends Controller | ... | @@ -179,6 +183,7 @@ class PageController extends Controller |
| 179 | { | 183 | { |
| 180 | $book = $this->bookRepo->getBySlug($bookSlug); | 184 | $book = $this->bookRepo->getBySlug($bookSlug); |
| 181 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | 185 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); |
| 186 | + $this->setPageTitle('Revisions For ' . $page->getShortName()); | ||
| 182 | return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]); | 187 | return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]); |
| 183 | } | 188 | } |
| 184 | 189 | ||
| ... | @@ -195,6 +200,7 @@ class PageController extends Controller | ... | @@ -195,6 +200,7 @@ class PageController extends Controller |
| 195 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); | 200 | $page = $this->pageRepo->getBySlug($pageSlug, $book->id); |
| 196 | $revision = $this->pageRepo->getRevisionById($revisionId); | 201 | $revision = $this->pageRepo->getRevisionById($revisionId); |
| 197 | $page->fill($revision->toArray()); | 202 | $page->fill($revision->toArray()); |
| 203 | + $this->setPageTitle('Page Revision For ' . $page->getShortName()); | ||
| 198 | return view('pages/revision', ['page' => $page, 'book' => $book]); | 204 | return view('pages/revision', ['page' => $page, 'book' => $book]); |
| 199 | } | 205 | } |
| 200 | 206 | ... | ... |
| ... | @@ -45,6 +45,7 @@ class SearchController extends Controller | ... | @@ -45,6 +45,7 @@ class SearchController extends Controller |
| 45 | $pages = $this->pageRepo->getBySearch($searchTerm); | 45 | $pages = $this->pageRepo->getBySearch($searchTerm); |
| 46 | $books = $this->bookRepo->getBySearch($searchTerm); | 46 | $books = $this->bookRepo->getBySearch($searchTerm); |
| 47 | $chapters = $this->chapterRepo->getBySearch($searchTerm); | 47 | $chapters = $this->chapterRepo->getBySearch($searchTerm); |
| 48 | + $this->setPageTitle('Search For ' . $searchTerm); | ||
| 48 | return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); | 49 | return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]); |
| 49 | } | 50 | } |
| 50 | 51 | ... | ... |
| ... | @@ -18,6 +18,7 @@ class SettingController extends Controller | ... | @@ -18,6 +18,7 @@ class SettingController extends Controller |
| 18 | public function index() | 18 | public function index() |
| 19 | { | 19 | { |
| 20 | $this->checkPermission('settings-update'); | 20 | $this->checkPermission('settings-update'); |
| 21 | + $this->setPageTitle('Settings'); | ||
| 21 | return view('settings/index'); | 22 | return view('settings/index'); |
| 22 | } | 23 | } |
| 23 | 24 | ... | ... |
| ... | @@ -4,7 +4,7 @@ namespace BookStack\Http\Controllers; | ... | @@ -4,7 +4,7 @@ namespace BookStack\Http\Controllers; |
| 4 | 4 | ||
| 5 | use Illuminate\Http\Request; | 5 | use Illuminate\Http\Request; |
| 6 | 6 | ||
| 7 | -use Illuminate\Support\Facades\Hash; | 7 | +use Illuminate\Http\Response; |
| 8 | use BookStack\Http\Requests; | 8 | use BookStack\Http\Requests; |
| 9 | use BookStack\Repos\UserRepo; | 9 | use BookStack\Repos\UserRepo; |
| 10 | use BookStack\Services\SocialAuthService; | 10 | use BookStack\Services\SocialAuthService; |
| ... | @@ -18,7 +18,8 @@ class UserController extends Controller | ... | @@ -18,7 +18,8 @@ class UserController extends Controller |
| 18 | 18 | ||
| 19 | /** | 19 | /** |
| 20 | * UserController constructor. | 20 | * UserController constructor. |
| 21 | - * @param $user | 21 | + * @param User $user |
| 22 | + * @param UserRepo $userRepo | ||
| 22 | */ | 23 | */ |
| 23 | public function __construct(User $user, UserRepo $userRepo) | 24 | public function __construct(User $user, UserRepo $userRepo) |
| 24 | { | 25 | { |
| ... | @@ -29,18 +30,17 @@ class UserController extends Controller | ... | @@ -29,18 +30,17 @@ class UserController extends Controller |
| 29 | 30 | ||
| 30 | /** | 31 | /** |
| 31 | * Display a listing of the users. | 32 | * Display a listing of the users. |
| 32 | - * | ||
| 33 | * @return Response | 33 | * @return Response |
| 34 | */ | 34 | */ |
| 35 | public function index() | 35 | public function index() |
| 36 | { | 36 | { |
| 37 | $users = $this->user->all(); | 37 | $users = $this->user->all(); |
| 38 | + $this->setPageTitle('Users'); | ||
| 38 | return view('users/index', ['users' => $users]); | 39 | return view('users/index', ['users' => $users]); |
| 39 | } | 40 | } |
| 40 | 41 | ||
| 41 | /** | 42 | /** |
| 42 | * Show the form for creating a new user. | 43 | * Show the form for creating a new user. |
| 43 | - * | ||
| 44 | * @return Response | 44 | * @return Response |
| 45 | */ | 45 | */ |
| 46 | public function create() | 46 | public function create() |
| ... | @@ -51,7 +51,6 @@ class UserController extends Controller | ... | @@ -51,7 +51,6 @@ class UserController extends Controller |
| 51 | 51 | ||
| 52 | /** | 52 | /** |
| 53 | * Store a newly created user in storage. | 53 | * Store a newly created user in storage. |
| 54 | - * | ||
| 55 | * @param Request $request | 54 | * @param Request $request |
| 56 | * @return Response | 55 | * @return Response |
| 57 | */ | 56 | */ |
| ... | @@ -60,7 +59,7 @@ class UserController extends Controller | ... | @@ -60,7 +59,7 @@ class UserController extends Controller |
| 60 | $this->checkPermission('user-create'); | 59 | $this->checkPermission('user-create'); |
| 61 | $this->validate($request, [ | 60 | $this->validate($request, [ |
| 62 | 'name' => 'required', | 61 | 'name' => 'required', |
| 63 | - 'email' => 'required|email', | 62 | + 'email' => 'required|email|unique:users,email', |
| 64 | 'password' => 'required|min:5', | 63 | 'password' => 'required|min:5', |
| 65 | 'password-confirm' => 'required|same:password', | 64 | 'password-confirm' => 'required|same:password', |
| 66 | 'role' => 'required|exists:roles,id' | 65 | 'role' => 'required|exists:roles,id' |
| ... | @@ -71,13 +70,20 @@ class UserController extends Controller | ... | @@ -71,13 +70,20 @@ class UserController extends Controller |
| 71 | $user->save(); | 70 | $user->save(); |
| 72 | 71 | ||
| 73 | $user->attachRoleId($request->get('role')); | 72 | $user->attachRoleId($request->get('role')); |
| 73 | + | ||
| 74 | + // Get avatar from gravatar and save | ||
| 75 | + if (!env('DISABLE_EXTERNAL_SERVICES', false)) { | ||
| 76 | + $avatar = \Images::saveUserGravatar($user); | ||
| 77 | + $user->avatar()->associate($avatar); | ||
| 78 | + $user->save(); | ||
| 79 | + } | ||
| 80 | + | ||
| 74 | return redirect('/users'); | 81 | return redirect('/users'); |
| 75 | } | 82 | } |
| 76 | 83 | ||
| 77 | 84 | ||
| 78 | /** | 85 | /** |
| 79 | * Show the form for editing the specified user. | 86 | * Show the form for editing the specified user. |
| 80 | - * | ||
| 81 | * @param int $id | 87 | * @param int $id |
| 82 | * @param SocialAuthService $socialAuthService | 88 | * @param SocialAuthService $socialAuthService |
| 83 | * @return Response | 89 | * @return Response |
| ... | @@ -90,12 +96,12 @@ class UserController extends Controller | ... | @@ -90,12 +96,12 @@ class UserController extends Controller |
| 90 | 96 | ||
| 91 | $user = $this->user->findOrFail($id); | 97 | $user = $this->user->findOrFail($id); |
| 92 | $activeSocialDrivers = $socialAuthService->getActiveDrivers(); | 98 | $activeSocialDrivers = $socialAuthService->getActiveDrivers(); |
| 99 | + $this->setPageTitle('User Profile'); | ||
| 93 | return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]); | 100 | return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]); |
| 94 | } | 101 | } |
| 95 | 102 | ||
| 96 | /** | 103 | /** |
| 97 | * Update the specified user in storage. | 104 | * Update the specified user in storage. |
| 98 | - * | ||
| 99 | * @param Request $request | 105 | * @param Request $request |
| 100 | * @param int $id | 106 | * @param int $id |
| 101 | * @return Response | 107 | * @return Response |
| ... | @@ -139,12 +145,12 @@ class UserController extends Controller | ... | @@ -139,12 +145,12 @@ class UserController extends Controller |
| 139 | return $this->currentUser->id == $id; | 145 | return $this->currentUser->id == $id; |
| 140 | }); | 146 | }); |
| 141 | $user = $this->user->findOrFail($id); | 147 | $user = $this->user->findOrFail($id); |
| 148 | + $this->setPageTitle('Delete User ' . $user->name); | ||
| 142 | return view('users/delete', ['user' => $user]); | 149 | return view('users/delete', ['user' => $user]); |
| 143 | } | 150 | } |
| 144 | 151 | ||
| 145 | /** | 152 | /** |
| 146 | * Remove the specified user from storage. | 153 | * Remove the specified user from storage. |
| 147 | - * | ||
| 148 | * @param int $id | 154 | * @param int $id |
| 149 | * @return Response | 155 | * @return Response |
| 150 | */ | 156 | */ |
| ... | @@ -153,14 +159,14 @@ class UserController extends Controller | ... | @@ -153,14 +159,14 @@ class UserController extends Controller |
| 153 | $this->checkPermissionOr('user-delete', function () use ($id) { | 159 | $this->checkPermissionOr('user-delete', function () use ($id) { |
| 154 | return $this->currentUser->id == $id; | 160 | return $this->currentUser->id == $id; |
| 155 | }); | 161 | }); |
| 162 | + | ||
| 156 | $user = $this->userRepo->getById($id); | 163 | $user = $this->userRepo->getById($id); |
| 157 | - // Delete social accounts | 164 | + if ($this->userRepo->isOnlyAdmin($user)) { |
| 158 | - if($this->userRepo->isOnlyAdmin($user)) { | ||
| 159 | session()->flash('error', 'You cannot delete the only admin'); | 165 | session()->flash('error', 'You cannot delete the only admin'); |
| 160 | return redirect($user->getEditUrl()); | 166 | return redirect($user->getEditUrl()); |
| 161 | } | 167 | } |
| 162 | - $user->socialAccounts()->delete(); | 168 | + $this->userRepo->destroy($user); |
| 163 | - $user->delete(); | 169 | + |
| 164 | return redirect('/users'); | 170 | return redirect('/users'); |
| 165 | } | 171 | } |
| 166 | } | 172 | } | ... | ... |
| ... | @@ -45,8 +45,6 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -45,8 +45,6 @@ Route::group(['middleware' => 'auth'], function () { |
| 45 | 45 | ||
| 46 | }); | 46 | }); |
| 47 | 47 | ||
| 48 | - // Uploads | ||
| 49 | - Route::post('/upload/image', 'ImageController@upload'); | ||
| 50 | 48 | ||
| 51 | // Users | 49 | // Users |
| 52 | Route::get('/users', 'UserController@index'); | 50 | Route::get('/users', 'UserController@index'); |
| ... | @@ -58,10 +56,18 @@ Route::group(['middleware' => 'auth'], function () { | ... | @@ -58,10 +56,18 @@ Route::group(['middleware' => 'auth'], function () { |
| 58 | Route::delete('/users/{id}', 'UserController@destroy'); | 56 | Route::delete('/users/{id}', 'UserController@destroy'); |
| 59 | 57 | ||
| 60 | // Image routes | 58 | // Image routes |
| 61 | - Route::get('/images/all', 'ImageController@getAll'); | 59 | + Route::group(['prefix' => 'images'], function() { |
| 62 | - Route::put('/images/update/{imageId}', 'ImageController@update'); | 60 | + // Get for user images |
| 63 | - Route::delete('/images/{imageId}', 'ImageController@destroy'); | 61 | + Route::get('/user/all', 'ImageController@getAllForUserType'); |
| 64 | - Route::get('/images/all/{page}', 'ImageController@getAll'); | 62 | + Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); |
| 63 | + // Standard get, update and deletion for all types | ||
| 64 | + Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); | ||
| 65 | + Route::put('/update/{imageId}', 'ImageController@update'); | ||
| 66 | + Route::post('/{type}/upload', 'ImageController@uploadByType'); | ||
| 67 | + Route::get('/{type}/all', 'ImageController@getAllByType'); | ||
| 68 | + Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); | ||
| 69 | + Route::delete('/{imageId}', 'ImageController@destroy'); | ||
| 70 | + }); | ||
| 65 | 71 | ||
| 66 | // Links | 72 | // Links |
| 67 | Route::get('/link/{id}', 'PageController@redirectFromLink'); | 73 | Route::get('/link/{id}', 'PageController@redirectFromLink'); | ... | ... |
| ... | @@ -3,22 +3,24 @@ | ... | @@ -3,22 +3,24 @@ |
| 3 | namespace BookStack; | 3 | namespace BookStack; |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | -class Image extends Entity | 6 | +use Illuminate\Database\Eloquent\Model; |
| 7 | +use Images; | ||
| 8 | + | ||
| 9 | +class Image extends Model | ||
| 7 | { | 10 | { |
| 11 | + use Ownable; | ||
| 8 | 12 | ||
| 9 | protected $fillable = ['name']; | 13 | protected $fillable = ['name']; |
| 10 | 14 | ||
| 11 | - public function getFilePath() | ||
| 12 | - { | ||
| 13 | - return storage_path() . $this->url; | ||
| 14 | - } | ||
| 15 | - | ||
| 16 | /** | 15 | /** |
| 17 | - * Get the url for this item. | 16 | + * Get a thumbnail for this image. |
| 17 | + * @param int $width | ||
| 18 | + * @param int $height | ||
| 19 | + * @param bool|false $keepRatio | ||
| 18 | * @return string | 20 | * @return string |
| 19 | */ | 21 | */ |
| 20 | - public function getUrl() | 22 | + public function getThumb($width, $height, $keepRatio = false) |
| 21 | { | 23 | { |
| 22 | - return public_path() . $this->url; | 24 | + return Images::getThumbnail($this, $width, $height, $keepRatio); |
| 23 | } | 25 | } |
| 24 | } | 26 | } | ... | ... |
app/Ownable.php
0 → 100644
| 1 | +<?php namespace BookStack; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +trait Ownable | ||
| 5 | +{ | ||
| 6 | + /** | ||
| 7 | + * Relation for the user that created this entity. | ||
| 8 | + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 9 | + */ | ||
| 10 | + public function createdBy() | ||
| 11 | + { | ||
| 12 | + return $this->belongsTo('BookStack\User', 'created_by'); | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * Relation for the user that updated this entity. | ||
| 17 | + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 18 | + */ | ||
| 19 | + public function updatedBy() | ||
| 20 | + { | ||
| 21 | + return $this->belongsTo('BookStack\User', 'updated_by'); | ||
| 22 | + } | ||
| 23 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -32,7 +32,6 @@ class Page extends Entity | ... | @@ -32,7 +32,6 @@ class Page extends Entity |
| 32 | return $this->chapter()->count() > 0; | 32 | return $this->chapter()->count() > 0; |
| 33 | } | 33 | } |
| 34 | 34 | ||
| 35 | - | ||
| 36 | public function revisions() | 35 | public function revisions() |
| 37 | { | 36 | { |
| 38 | return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc'); | 37 | return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc'); |
| ... | @@ -40,7 +39,6 @@ class Page extends Entity | ... | @@ -40,7 +39,6 @@ class Page extends Entity |
| 40 | 39 | ||
| 41 | public function getUrl() | 40 | public function getUrl() |
| 42 | { | 41 | { |
| 43 | - // TODO - Extract this and share with chapters | ||
| 44 | $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; | 42 | $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; |
| 45 | return '/books/' . $bookSlug . '/page/' . $this->slug; | 43 | return '/books/' . $bookSlug . '/page/' . $this->slug; |
| 46 | } | 44 | } | ... | ... |
| ... | @@ -2,6 +2,7 @@ | ... | @@ -2,6 +2,7 @@ |
| 2 | 2 | ||
| 3 | namespace BookStack\Providers; | 3 | namespace BookStack\Providers; |
| 4 | 4 | ||
| 5 | +use BookStack\Services\ImageService; | ||
| 5 | use BookStack\Services\ViewService; | 6 | use BookStack\Services\ViewService; |
| 6 | use Illuminate\Support\ServiceProvider; | 7 | use Illuminate\Support\ServiceProvider; |
| 7 | use BookStack\Services\ActivityService; | 8 | use BookStack\Services\ActivityService; |
| ... | @@ -40,5 +41,12 @@ class CustomFacadeProvider extends ServiceProvider | ... | @@ -40,5 +41,12 @@ class CustomFacadeProvider extends ServiceProvider |
| 40 | $this->app->make('Illuminate\Contracts\Cache\Repository') | 41 | $this->app->make('Illuminate\Contracts\Cache\Repository') |
| 41 | ); | 42 | ); |
| 42 | }); | 43 | }); |
| 44 | + $this->app->bind('images', function() { | ||
| 45 | + return new ImageService( | ||
| 46 | + $this->app->make('Intervention\Image\ImageManager'), | ||
| 47 | + $this->app->make('Illuminate\Contracts\Filesystem\Factory'), | ||
| 48 | + $this->app->make('Illuminate\Contracts\Cache\Repository') | ||
| 49 | + ); | ||
| 50 | + }); | ||
| 43 | } | 51 | } |
| 44 | } | 52 | } | ... | ... |
| ... | @@ -78,6 +78,17 @@ class BookRepo | ... | @@ -78,6 +78,17 @@ class BookRepo |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | /** | 80 | /** |
| 81 | + * Gets the most viewed books. | ||
| 82 | + * @param int $count | ||
| 83 | + * @param int $page | ||
| 84 | + * @return mixed | ||
| 85 | + */ | ||
| 86 | + public function getPopular($count = 10, $page = 0) | ||
| 87 | + { | ||
| 88 | + return Views::getPopular($count, $page, $this->book); | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + /** | ||
| 81 | * Get a book by slug | 92 | * Get a book by slug |
| 82 | * @param $slug | 93 | * @param $slug |
| 83 | * @return mixed | 94 | * @return mixed | ... | ... |
app/Repos/ImageRepo.php
0 → 100644
| 1 | +<?php namespace BookStack\Repos; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +use BookStack\Image; | ||
| 5 | +use BookStack\Services\ImageService; | ||
| 6 | +use Setting; | ||
| 7 | +use Symfony\Component\HttpFoundation\File\UploadedFile; | ||
| 8 | + | ||
| 9 | +class ImageRepo | ||
| 10 | +{ | ||
| 11 | + | ||
| 12 | + protected $image; | ||
| 13 | + protected $imageService; | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * ImageRepo constructor. | ||
| 17 | + * @param Image $image | ||
| 18 | + * @param ImageService $imageService | ||
| 19 | + */ | ||
| 20 | + public function __construct(Image $image, ImageService $imageService) | ||
| 21 | + { | ||
| 22 | + $this->image = $image; | ||
| 23 | + $this->imageService = $imageService; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * Get an image with the given id. | ||
| 29 | + * @param $id | ||
| 30 | + * @return mixed | ||
| 31 | + */ | ||
| 32 | + public function getById($id) | ||
| 33 | + { | ||
| 34 | + return $this->image->findOrFail($id); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * Gets a load images paginated, filtered by image type. | ||
| 40 | + * @param string $type | ||
| 41 | + * @param int $page | ||
| 42 | + * @param int $pageSize | ||
| 43 | + * @param bool|int $userFilter | ||
| 44 | + * @return array | ||
| 45 | + */ | ||
| 46 | + public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false) | ||
| 47 | + { | ||
| 48 | + $images = $this->image->where('type', '=', strtolower($type)); | ||
| 49 | + | ||
| 50 | + if ($userFilter !== false) { | ||
| 51 | + $images = $images->where('created_by', '=', $userFilter); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); | ||
| 55 | + $hasMore = count($images) > $pageSize; | ||
| 56 | + | ||
| 57 | + $returnImages = $images->take(24); | ||
| 58 | + $returnImages->each(function ($image) { | ||
| 59 | + $this->loadThumbs($image); | ||
| 60 | + }); | ||
| 61 | + | ||
| 62 | + return [ | ||
| 63 | + 'images' => $returnImages, | ||
| 64 | + 'hasMore' => $hasMore | ||
| 65 | + ]; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * Save a new image into storage and return the new image. | ||
| 70 | + * @param UploadedFile $uploadFile | ||
| 71 | + * @param string $type | ||
| 72 | + * @return Image | ||
| 73 | + */ | ||
| 74 | + public function saveNew(UploadedFile $uploadFile, $type) | ||
| 75 | + { | ||
| 76 | + $image = $this->imageService->saveNewFromUpload($uploadFile, $type); | ||
| 77 | + $this->loadThumbs($image); | ||
| 78 | + return $image; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * Update the details of an image via an array of properties. | ||
| 83 | + * @param Image $image | ||
| 84 | + * @param array $updateDetails | ||
| 85 | + * @return Image | ||
| 86 | + */ | ||
| 87 | + public function updateImageDetails(Image $image, $updateDetails) | ||
| 88 | + { | ||
| 89 | + $image->fill($updateDetails); | ||
| 90 | + $image->save(); | ||
| 91 | + $this->loadThumbs($image); | ||
| 92 | + return $image; | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + | ||
| 96 | + /** | ||
| 97 | + * Destroys an Image object along with its files and thumbnails. | ||
| 98 | + * @param Image $image | ||
| 99 | + * @return bool | ||
| 100 | + */ | ||
| 101 | + public function destroyImage(Image $image) | ||
| 102 | + { | ||
| 103 | + $this->imageService->destroyImage($image); | ||
| 104 | + return true; | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + /** | ||
| 109 | + * Load thumbnails onto an image object. | ||
| 110 | + * @param Image $image | ||
| 111 | + */ | ||
| 112 | + private function loadThumbs(Image $image) | ||
| 113 | + { | ||
| 114 | + $image->thumbs = [ | ||
| 115 | + 'gallery' => $this->getThumbnail($image, 150, 150), | ||
| 116 | + 'display' => $this->getThumbnail($image, 840, 0, true) | ||
| 117 | + ]; | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * Get the thumbnail for an image. | ||
| 122 | + * If $keepRatio is true only the width will be used. | ||
| 123 | + * Checks the cache then storage to avoid creating / accessing the filesystem on every check. | ||
| 124 | + * | ||
| 125 | + * @param Image $image | ||
| 126 | + * @param int $width | ||
| 127 | + * @param int $height | ||
| 128 | + * @param bool $keepRatio | ||
| 129 | + * @return string | ||
| 130 | + */ | ||
| 131 | + public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) | ||
| 132 | + { | ||
| 133 | + return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + | ||
| 137 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -269,7 +269,7 @@ class PageRepo | ... | @@ -269,7 +269,7 @@ class PageRepo |
| 269 | * @param Page $page | 269 | * @param Page $page |
| 270 | * @return $this | 270 | * @return $this |
| 271 | */ | 271 | */ |
| 272 | - private function saveRevision(Page $page) | 272 | + public function saveRevision(Page $page) |
| 273 | { | 273 | { |
| 274 | $revision = $this->pageRevision->fill($page->toArray()); | 274 | $revision = $this->pageRevision->fill($page->toArray()); |
| 275 | $revision->page_id = $page->id; | 275 | $revision->page_id = $page->id; | ... | ... |
| ... | @@ -46,14 +46,19 @@ class UserRepo | ... | @@ -46,14 +46,19 @@ class UserRepo |
| 46 | public function registerNew(array $data) | 46 | public function registerNew(array $data) |
| 47 | { | 47 | { |
| 48 | $user = $this->create($data); | 48 | $user = $this->create($data); |
| 49 | - $roleId = \Setting::get('registration-role'); | 49 | + $this->attachDefaultRole($user); |
| 50 | - | 50 | + return $user; |
| 51 | - if ($roleId === false) { | 51 | + } |
| 52 | - $roleId = $this->role->getDefault()->id; | ||
| 53 | - } | ||
| 54 | 52 | ||
| 53 | + /** | ||
| 54 | + * Give a user the default role. Used when creating a new user. | ||
| 55 | + * @param $user | ||
| 56 | + */ | ||
| 57 | + public function attachDefaultRole($user) | ||
| 58 | + { | ||
| 59 | + $roleId = \Setting::get('registration-role'); | ||
| 60 | + if ($roleId === false) $roleId = $this->role->getDefault()->id; | ||
| 55 | $user->attachRoleId($roleId); | 61 | $user->attachRoleId($roleId); |
| 56 | - return $user; | ||
| 57 | } | 62 | } |
| 58 | 63 | ||
| 59 | /** | 64 | /** |
| ... | @@ -88,4 +93,14 @@ class UserRepo | ... | @@ -88,4 +93,14 @@ class UserRepo |
| 88 | 'password' => bcrypt($data['password']) | 93 | 'password' => bcrypt($data['password']) |
| 89 | ]); | 94 | ]); |
| 90 | } | 95 | } |
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * Remove the given user from storage, Delete all related content. | ||
| 99 | + * @param User $user | ||
| 100 | + */ | ||
| 101 | + public function destroy(User $user) | ||
| 102 | + { | ||
| 103 | + $user->socialAccounts()->delete(); | ||
| 104 | + $user->delete(); | ||
| 105 | + } | ||
| 91 | } | 106 | } |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
app/Services/Facades/Images.php
0 → 100644
| 1 | +<?php namespace BookStack\Services\Facades; | ||
| 2 | + | ||
| 3 | + | ||
| 4 | +use Illuminate\Support\Facades\Facade; | ||
| 5 | + | ||
| 6 | +class Images extends Facade | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Get the registered name of the component. | ||
| 10 | + * | ||
| 11 | + * @return string | ||
| 12 | + */ | ||
| 13 | + protected static function getFacadeAccessor() { return 'images'; } | ||
| 14 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
app/Services/ImageService.php
0 → 100644
| 1 | +<?php namespace BookStack\Services; | ||
| 2 | + | ||
| 3 | +use BookStack\Image; | ||
| 4 | +use BookStack\User; | ||
| 5 | +use Intervention\Image\ImageManager; | ||
| 6 | +use Illuminate\Contracts\Filesystem\Factory as FileSystem; | ||
| 7 | +use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; | ||
| 8 | +use Illuminate\Contracts\Cache\Repository as Cache; | ||
| 9 | +use Setting; | ||
| 10 | +use Symfony\Component\HttpFoundation\File\UploadedFile; | ||
| 11 | + | ||
| 12 | +class ImageService | ||
| 13 | +{ | ||
| 14 | + | ||
| 15 | + protected $imageTool; | ||
| 16 | + protected $fileSystem; | ||
| 17 | + protected $cache; | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * @var FileSystemInstance | ||
| 21 | + */ | ||
| 22 | + protected $storageInstance; | ||
| 23 | + protected $storageUrl; | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * ImageService constructor. | ||
| 27 | + * @param $imageTool | ||
| 28 | + * @param $fileSystem | ||
| 29 | + * @param $cache | ||
| 30 | + */ | ||
| 31 | + public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) | ||
| 32 | + { | ||
| 33 | + $this->imageTool = $imageTool; | ||
| 34 | + $this->fileSystem = $fileSystem; | ||
| 35 | + $this->cache = $cache; | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * Saves a new image from an upload. | ||
| 40 | + * @param UploadedFile $uploadedFile | ||
| 41 | + * @param string $type | ||
| 42 | + * @return mixed | ||
| 43 | + */ | ||
| 44 | + public function saveNewFromUpload(UploadedFile $uploadedFile, $type) | ||
| 45 | + { | ||
| 46 | + $imageName = $uploadedFile->getClientOriginalName(); | ||
| 47 | + $imageData = file_get_contents($uploadedFile->getRealPath()); | ||
| 48 | + return $this->saveNew($imageName, $imageData, $type); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * Gets an image from url and saves it to the database. | ||
| 54 | + * @param $url | ||
| 55 | + * @param string $type | ||
| 56 | + * @param bool|string $imageName | ||
| 57 | + * @return mixed | ||
| 58 | + * @throws \Exception | ||
| 59 | + */ | ||
| 60 | + private function saveNewFromUrl($url, $type, $imageName = false) | ||
| 61 | + { | ||
| 62 | + $imageName = $imageName ? $imageName : basename($url); | ||
| 63 | + $imageData = file_get_contents($url); | ||
| 64 | + if($imageData === false) throw new \Exception('Cannot get image from ' . $url); | ||
| 65 | + return $this->saveNew($imageName, $imageData, $type); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * Saves a new image | ||
| 70 | + * @param string $imageName | ||
| 71 | + * @param string $imageData | ||
| 72 | + * @param string $type | ||
| 73 | + * @return Image | ||
| 74 | + */ | ||
| 75 | + private function saveNew($imageName, $imageData, $type) | ||
| 76 | + { | ||
| 77 | + $storage = $this->getStorage(); | ||
| 78 | + $secureUploads = Setting::get('app-secure-images'); | ||
| 79 | + $imageName = str_replace(' ', '-', $imageName); | ||
| 80 | + | ||
| 81 | + if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; | ||
| 82 | + | ||
| 83 | + $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; | ||
| 84 | + while ($storage->exists($imagePath . $imageName)) { | ||
| 85 | + $imageName = str_random(3) . $imageName; | ||
| 86 | + } | ||
| 87 | + $fullPath = $imagePath . $imageName; | ||
| 88 | + | ||
| 89 | + $storage->put($fullPath, $imageData); | ||
| 90 | + | ||
| 91 | + $userId = auth()->user()->id; | ||
| 92 | + $image = Image::forceCreate([ | ||
| 93 | + 'name' => $imageName, | ||
| 94 | + 'path' => $fullPath, | ||
| 95 | + 'url' => $this->getPublicUrl($fullPath), | ||
| 96 | + 'type' => $type, | ||
| 97 | + 'created_by' => $userId, | ||
| 98 | + 'updated_by' => $userId | ||
| 99 | + ]); | ||
| 100 | + | ||
| 101 | + return $image; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * Get the thumbnail for an image. | ||
| 106 | + * If $keepRatio is true only the width will be used. | ||
| 107 | + * Checks the cache then storage to avoid creating / accessing the filesystem on every check. | ||
| 108 | + * | ||
| 109 | + * @param Image $image | ||
| 110 | + * @param int $width | ||
| 111 | + * @param int $height | ||
| 112 | + * @param bool $keepRatio | ||
| 113 | + * @return string | ||
| 114 | + */ | ||
| 115 | + public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) | ||
| 116 | + { | ||
| 117 | + $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; | ||
| 118 | + $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); | ||
| 119 | + | ||
| 120 | + if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { | ||
| 121 | + return $this->getPublicUrl($thumbFilePath); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + $storage = $this->getStorage(); | ||
| 125 | + | ||
| 126 | + if ($storage->exists($thumbFilePath)) { | ||
| 127 | + return $this->getPublicUrl($thumbFilePath); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + // Otherwise create the thumbnail | ||
| 131 | + $thumb = $this->imageTool->make($storage->get($image->path)); | ||
| 132 | + if ($keepRatio) { | ||
| 133 | + $thumb->resize($width, null, function ($constraint) { | ||
| 134 | + $constraint->aspectRatio(); | ||
| 135 | + $constraint->upsize(); | ||
| 136 | + }); | ||
| 137 | + } else { | ||
| 138 | + $thumb->fit($width, $height); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + $thumbData = (string)$thumb->encode(); | ||
| 142 | + $storage->put($thumbFilePath, $thumbData); | ||
| 143 | + $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); | ||
| 144 | + | ||
| 145 | + return $this->getPublicUrl($thumbFilePath); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + /** | ||
| 149 | + * Destroys an Image object along with its files and thumbnails. | ||
| 150 | + * @param Image $image | ||
| 151 | + * @return bool | ||
| 152 | + */ | ||
| 153 | + public function destroyImage(Image $image) | ||
| 154 | + { | ||
| 155 | + $storage = $this->getStorage(); | ||
| 156 | + | ||
| 157 | + $imageFolder = dirname($image->path); | ||
| 158 | + $imageFileName = basename($image->path); | ||
| 159 | + $allImages = collect($storage->allFiles($imageFolder)); | ||
| 160 | + | ||
| 161 | + $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { | ||
| 162 | + $expectedIndex = strlen($imagePath) - strlen($imageFileName); | ||
| 163 | + return strpos($imagePath, $imageFileName) === $expectedIndex; | ||
| 164 | + }); | ||
| 165 | + | ||
| 166 | + $storage->delete($imagesToDelete->all()); | ||
| 167 | + | ||
| 168 | + // Cleanup of empty folders | ||
| 169 | + foreach ($storage->directories($imageFolder) as $directory) { | ||
| 170 | + if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory); | ||
| 171 | + } | ||
| 172 | + if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder); | ||
| 173 | + | ||
| 174 | + $image->delete(); | ||
| 175 | + return true; | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /** | ||
| 179 | + * Save a gravatar image and set a the profile image for a user. | ||
| 180 | + * @param User $user | ||
| 181 | + * @param int $size | ||
| 182 | + * @return mixed | ||
| 183 | + */ | ||
| 184 | + public function saveUserGravatar(User $user, $size = 500) | ||
| 185 | + { | ||
| 186 | + $emailHash = md5(strtolower(trim($user->email))); | ||
| 187 | + $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; | ||
| 188 | + $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); | ||
| 189 | + $image = $this->saveNewFromUrl($url, 'user', $imageName); | ||
| 190 | + $image->created_by = $user->id; | ||
| 191 | + $image->save(); | ||
| 192 | + return $image; | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + /** | ||
| 196 | + * Get the storage that will be used for storing images. | ||
| 197 | + * @return FileSystemInstance | ||
| 198 | + */ | ||
| 199 | + private function getStorage() | ||
| 200 | + { | ||
| 201 | + if ($this->storageInstance !== null) return $this->storageInstance; | ||
| 202 | + | ||
| 203 | + $storageType = env('STORAGE_TYPE'); | ||
| 204 | + $this->storageInstance = $this->fileSystem->disk($storageType); | ||
| 205 | + | ||
| 206 | + return $this->storageInstance; | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + /** | ||
| 210 | + * Check whether or not a folder is empty. | ||
| 211 | + * @param $path | ||
| 212 | + * @return int | ||
| 213 | + */ | ||
| 214 | + private function isFolderEmpty($path) | ||
| 215 | + { | ||
| 216 | + $files = $this->getStorage()->files($path); | ||
| 217 | + $folders = $this->getStorage()->directories($path); | ||
| 218 | + return count($files) === 0 && count($folders) === 0; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + /** | ||
| 222 | + * Gets a public facing url for an image by checking relevant environment variables. | ||
| 223 | + * @param $filePath | ||
| 224 | + * @return string | ||
| 225 | + */ | ||
| 226 | + private function getPublicUrl($filePath) | ||
| 227 | + { | ||
| 228 | + if ($this->storageUrl === null) { | ||
| 229 | + $storageUrl = env('STORAGE_URL'); | ||
| 230 | + | ||
| 231 | + // Get the standard public s3 url if s3 is set as storage type | ||
| 232 | + if ($storageUrl == false && env('STORAGE_TYPE') === 's3') { | ||
| 233 | + $storageDetails = config('filesystems.disks.s3'); | ||
| 234 | + $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + $this->storageUrl = $storageUrl; | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath; | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + | ||
| 244 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -44,6 +44,29 @@ class ViewService | ... | @@ -44,6 +44,29 @@ class ViewService |
| 44 | return 1; | 44 | return 1; |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * Get the entities with the most views. | ||
| 50 | + * @param int $count | ||
| 51 | + * @param int $page | ||
| 52 | + * @param bool|false $filterModel | ||
| 53 | + */ | ||
| 54 | + public function getPopular($count = 10, $page = 0, $filterModel = false) | ||
| 55 | + { | ||
| 56 | + $skipCount = $count * $page; | ||
| 57 | + $query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) | ||
| 58 | + ->groupBy('viewable_id', 'viewable_type') | ||
| 59 | + ->orderBy('view_count', 'desc'); | ||
| 60 | + | ||
| 61 | + if($filterModel) $query->where('viewable_type', '=', get_class($filterModel)); | ||
| 62 | + | ||
| 63 | + $views = $query->with('viewable')->skip($skipCount)->take($count)->get(); | ||
| 64 | + $viewedEntities = $views->map(function ($item) { | ||
| 65 | + return $item->viewable()->getResults(); | ||
| 66 | + }); | ||
| 67 | + return $viewedEntities; | ||
| 68 | + } | ||
| 69 | + | ||
| 47 | /** | 70 | /** |
| 48 | * Get all recently viewed entities for the current user. | 71 | * Get all recently viewed entities for the current user. |
| 49 | * @param int $count | 72 | * @param int $count | ... | ... |
| ... | @@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ... | @@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon |
| 24 | * | 24 | * |
| 25 | * @var array | 25 | * @var array |
| 26 | */ | 26 | */ |
| 27 | - protected $fillable = ['name', 'email', 'password']; | 27 | + protected $fillable = ['name', 'email', 'password', 'image_id']; |
| 28 | 28 | ||
| 29 | /** | 29 | /** |
| 30 | * The attributes excluded from the model's JSON form. | 30 | * The attributes excluded from the model's JSON form. |
| ... | @@ -145,8 +145,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ... | @@ -145,8 +145,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon |
| 145 | */ | 145 | */ |
| 146 | public function getAvatar($size = 50) | 146 | public function getAvatar($size = 50) |
| 147 | { | 147 | { |
| 148 | - $emailHash = md5(strtolower(trim($this->email))); | 148 | + if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png'; |
| 149 | - return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; | 149 | + return $this->avatar->getThumb($size, $size, false); |
| 150 | + } | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * Get the avatar for the user. | ||
| 154 | + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||
| 155 | + */ | ||
| 156 | + public function avatar() | ||
| 157 | + { | ||
| 158 | + return $this->belongsTo('BookStack\Image', 'image_id'); | ||
| 150 | } | 159 | } |
| 151 | 160 | ||
| 152 | /** | 161 | /** | ... | ... |
app/helpers.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +if (! function_exists('versioned_asset')) { | ||
| 4 | + /** | ||
| 5 | + * Get the path to a versioned file. | ||
| 6 | + * | ||
| 7 | + * @param string $file | ||
| 8 | + * @return string | ||
| 9 | + * | ||
| 10 | + * @throws \InvalidArgumentException | ||
| 11 | + */ | ||
| 12 | + function versioned_asset($file) | ||
| 13 | + { | ||
| 14 | + static $manifest = null; | ||
| 15 | + | ||
| 16 | + if (is_null($manifest)) { | ||
| 17 | + $manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + if (isset($manifest[$file])) { | ||
| 21 | + return '/' . $manifest[$file]; | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + if (file_exists(public_path($file))) { | ||
| 25 | + return '/' . $file; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + throw new InvalidArgumentException("File {$file} not defined in asset manifest."); | ||
| 29 | + } | ||
| 30 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -8,15 +8,16 @@ | ... | @@ -8,15 +8,16 @@ |
| 8 | "php": ">=5.5.9", | 8 | "php": ">=5.5.9", |
| 9 | "laravel/framework": "5.1.*", | 9 | "laravel/framework": "5.1.*", |
| 10 | "intervention/image": "^2.3", | 10 | "intervention/image": "^2.3", |
| 11 | - "laravel/socialite": "^2.0" | 11 | + "laravel/socialite": "^2.0", |
| 12 | + "barryvdh/laravel-ide-helper": "^2.1", | ||
| 13 | + "barryvdh/laravel-debugbar": "^2.0", | ||
| 14 | + "league/flysystem-aws-s3-v3": "^1.0" | ||
| 12 | }, | 15 | }, |
| 13 | "require-dev": { | 16 | "require-dev": { |
| 14 | "fzaninotto/faker": "~1.4", | 17 | "fzaninotto/faker": "~1.4", |
| 15 | "mockery/mockery": "0.9.*", | 18 | "mockery/mockery": "0.9.*", |
| 16 | "phpunit/phpunit": "~4.0", | 19 | "phpunit/phpunit": "~4.0", |
| 17 | - "phpspec/phpspec": "~2.1", | 20 | + "phpspec/phpspec": "~2.1" |
| 18 | - "barryvdh/laravel-ide-helper": "^2.1", | ||
| 19 | - "barryvdh/laravel-debugbar": "^2.0" | ||
| 20 | }, | 21 | }, |
| 21 | "autoload": { | 22 | "autoload": { |
| 22 | "classmap": [ | 23 | "classmap": [ |
| ... | @@ -24,7 +25,10 @@ | ... | @@ -24,7 +25,10 @@ |
| 24 | ], | 25 | ], |
| 25 | "psr-4": { | 26 | "psr-4": { |
| 26 | "BookStack\\": "app/" | 27 | "BookStack\\": "app/" |
| 27 | - } | 28 | + }, |
| 29 | + "files": [ | ||
| 30 | + "app/helpers.php" | ||
| 31 | + ] | ||
| 28 | }, | 32 | }, |
| 29 | "autoload-dev": { | 33 | "autoload-dev": { |
| 30 | "classmap": [ | 34 | "classmap": [ | ... | ... |
This diff is collapsed.
Click to expand it.
| ... | @@ -13,7 +13,7 @@ return [ | ... | @@ -13,7 +13,7 @@ return [ |
| 13 | | | 13 | | |
| 14 | */ | 14 | */ |
| 15 | 15 | ||
| 16 | - 'debug' => env('APP_DEBUG', false), | 16 | + 'debug' => env('APP_DEBUG', false), |
| 17 | 17 | ||
| 18 | /* | 18 | /* |
| 19 | |-------------------------------------------------------------------------- | 19 | |-------------------------------------------------------------------------- |
| ... | @@ -26,7 +26,7 @@ return [ | ... | @@ -26,7 +26,7 @@ return [ |
| 26 | | | 26 | | |
| 27 | */ | 27 | */ |
| 28 | 28 | ||
| 29 | - 'url' => env('APP_URL', 'http://localhost'), | 29 | + 'url' => env('APP_URL', 'http://localhost'), |
| 30 | 30 | ||
| 31 | /* | 31 | /* |
| 32 | |-------------------------------------------------------------------------- | 32 | |-------------------------------------------------------------------------- |
| ... | @@ -39,7 +39,7 @@ return [ | ... | @@ -39,7 +39,7 @@ return [ |
| 39 | | | 39 | | |
| 40 | */ | 40 | */ |
| 41 | 41 | ||
| 42 | - 'timezone' => 'UTC', | 42 | + 'timezone' => 'UTC', |
| 43 | 43 | ||
| 44 | /* | 44 | /* |
| 45 | |-------------------------------------------------------------------------- | 45 | |-------------------------------------------------------------------------- |
| ... | @@ -52,7 +52,7 @@ return [ | ... | @@ -52,7 +52,7 @@ return [ |
| 52 | | | 52 | | |
| 53 | */ | 53 | */ |
| 54 | 54 | ||
| 55 | - 'locale' => 'en', | 55 | + 'locale' => 'en', |
| 56 | 56 | ||
| 57 | /* | 57 | /* |
| 58 | |-------------------------------------------------------------------------- | 58 | |-------------------------------------------------------------------------- |
| ... | @@ -78,9 +78,9 @@ return [ | ... | @@ -78,9 +78,9 @@ return [ |
| 78 | | | 78 | | |
| 79 | */ | 79 | */ |
| 80 | 80 | ||
| 81 | - 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), | 81 | + 'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'), |
| 82 | 82 | ||
| 83 | - 'cipher' => 'AES-256-CBC', | 83 | + 'cipher' => 'AES-256-CBC', |
| 84 | 84 | ||
| 85 | /* | 85 | /* |
| 86 | |-------------------------------------------------------------------------- | 86 | |-------------------------------------------------------------------------- |
| ... | @@ -95,7 +95,7 @@ return [ | ... | @@ -95,7 +95,7 @@ return [ |
| 95 | | | 95 | | |
| 96 | */ | 96 | */ |
| 97 | 97 | ||
| 98 | - 'log' => 'single', | 98 | + 'log' => 'single', |
| 99 | 99 | ||
| 100 | /* | 100 | /* |
| 101 | |-------------------------------------------------------------------------- | 101 | |-------------------------------------------------------------------------- |
| ... | @@ -108,7 +108,7 @@ return [ | ... | @@ -108,7 +108,7 @@ return [ |
| 108 | | | 108 | | |
| 109 | */ | 109 | */ |
| 110 | 110 | ||
| 111 | - 'providers' => [ | 111 | + 'providers' => [ |
| 112 | 112 | ||
| 113 | /* | 113 | /* |
| 114 | * Laravel Framework Service Providers... | 114 | * Laravel Framework Service Providers... |
| ... | @@ -167,7 +167,7 @@ return [ | ... | @@ -167,7 +167,7 @@ return [ |
| 167 | | | 167 | | |
| 168 | */ | 168 | */ |
| 169 | 169 | ||
| 170 | - 'aliases' => [ | 170 | + 'aliases' => [ |
| 171 | 171 | ||
| 172 | 'App' => Illuminate\Support\Facades\App::class, | 172 | 'App' => Illuminate\Support\Facades\App::class, |
| 173 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, | 173 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, |
| ... | @@ -208,15 +208,16 @@ return [ | ... | @@ -208,15 +208,16 @@ return [ |
| 208 | */ | 208 | */ |
| 209 | 209 | ||
| 210 | 'ImageTool' => Intervention\Image\Facades\Image::class, | 210 | 'ImageTool' => Intervention\Image\Facades\Image::class, |
| 211 | - 'Debugbar' => Barryvdh\Debugbar\Facade::class, | 211 | + 'Debugbar' => Barryvdh\Debugbar\Facade::class, |
| 212 | 212 | ||
| 213 | /** | 213 | /** |
| 214 | * Custom | 214 | * Custom |
| 215 | */ | 215 | */ |
| 216 | 216 | ||
| 217 | - 'Activity' => BookStack\Services\Facades\Activity::class, | 217 | + 'Activity' => BookStack\Services\Facades\Activity::class, |
| 218 | - 'Setting' => BookStack\Services\Facades\Setting::class, | 218 | + 'Setting' => BookStack\Services\Facades\Setting::class, |
| 219 | - 'Views' => BookStack\Services\Facades\Views::class, | 219 | + 'Views' => BookStack\Services\Facades\Views::class, |
| 220 | + 'Images' => \BookStack\Services\Facades\Images::class, | ||
| 220 | 221 | ||
| 221 | ], | 222 | ], |
| 222 | 223 | ... | ... |
| ... | @@ -45,7 +45,7 @@ return [ | ... | @@ -45,7 +45,7 @@ return [ |
| 45 | 45 | ||
| 46 | 'local' => [ | 46 | 'local' => [ |
| 47 | 'driver' => 'local', | 47 | 'driver' => 'local', |
| 48 | - 'root' => storage_path('app'), | 48 | + 'root' => public_path(), |
| 49 | ], | 49 | ], |
| 50 | 50 | ||
| 51 | 'ftp' => [ | 51 | 'ftp' => [ |
| ... | @@ -64,10 +64,10 @@ return [ | ... | @@ -64,10 +64,10 @@ return [ |
| 64 | 64 | ||
| 65 | 's3' => [ | 65 | 's3' => [ |
| 66 | 'driver' => 's3', | 66 | 'driver' => 's3', |
| 67 | - 'key' => 'your-key', | 67 | + 'key' => env('STORAGE_S3_KEY', 'your-key'), |
| 68 | - 'secret' => 'your-secret', | 68 | + 'secret' => env('STORAGE_S3_SECRET', 'your-secret'), |
| 69 | - 'region' => 'your-region', | 69 | + 'region' => env('STORAGE_S3_REGION', 'your-region'), |
| 70 | - 'bucket' => 'your-bucket', | 70 | + 'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'), |
| 71 | ], | 71 | ], |
| 72 | 72 | ||
| 73 | 'rackspace' => [ | 73 | 'rackspace' => [ | ... | ... |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Database\Schema\Blueprint; | ||
| 4 | +use Illuminate\Database\Migrations\Migration; | ||
| 5 | + | ||
| 6 | +class FulltextWeighting extends Migration | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Run the migrations. | ||
| 10 | + * | ||
| 11 | + * @return void | ||
| 12 | + */ | ||
| 13 | + public function up() | ||
| 14 | + { | ||
| 15 | + DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)'); | ||
| 16 | + DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)'); | ||
| 17 | + DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)'); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * Reverse the migrations. | ||
| 22 | + * | ||
| 23 | + * @return void | ||
| 24 | + */ | ||
| 25 | + public function down() | ||
| 26 | + { | ||
| 27 | + Schema::table('pages', function(Blueprint $table) { | ||
| 28 | + $table->dropIndex('name_search'); | ||
| 29 | + }); | ||
| 30 | + Schema::table('books', function(Blueprint $table) { | ||
| 31 | + $table->dropIndex('name_search'); | ||
| 32 | + }); | ||
| 33 | + Schema::table('chapters', function(Blueprint $table) { | ||
| 34 | + $table->dropIndex('name_search'); | ||
| 35 | + }); | ||
| 36 | + } | ||
| 37 | +} |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use BookStack\Image; | ||
| 4 | +use Illuminate\Database\Schema\Blueprint; | ||
| 5 | +use Illuminate\Database\Migrations\Migration; | ||
| 6 | + | ||
| 7 | +class AddImageUploadTypes extends Migration | ||
| 8 | +{ | ||
| 9 | + /** | ||
| 10 | + * Run the migrations. | ||
| 11 | + * | ||
| 12 | + * @return void | ||
| 13 | + */ | ||
| 14 | + public function up() | ||
| 15 | + { | ||
| 16 | + Schema::table('images', function (Blueprint $table) { | ||
| 17 | + $table->string('path', 400); | ||
| 18 | + $table->string('type')->index(); | ||
| 19 | + }); | ||
| 20 | + | ||
| 21 | + Image::all()->each(function($image) { | ||
| 22 | + $image->path = $image->url; | ||
| 23 | + $image->type = 'gallery'; | ||
| 24 | + $image->save(); | ||
| 25 | + }); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * Reverse the migrations. | ||
| 30 | + * | ||
| 31 | + * @return void | ||
| 32 | + */ | ||
| 33 | + public function down() | ||
| 34 | + { | ||
| 35 | + Schema::table('images', function (Blueprint $table) { | ||
| 36 | + $table->dropColumn('type'); | ||
| 37 | + $table->dropColumn('path'); | ||
| 38 | + }); | ||
| 39 | + | ||
| 40 | + } | ||
| 41 | +} |
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Database\Schema\Blueprint; | ||
| 4 | +use Illuminate\Database\Migrations\Migration; | ||
| 5 | + | ||
| 6 | +class AddUserAvatars extends Migration | ||
| 7 | +{ | ||
| 8 | + /** | ||
| 9 | + * Run the migrations. | ||
| 10 | + * | ||
| 11 | + * @return void | ||
| 12 | + */ | ||
| 13 | + public function up() | ||
| 14 | + { | ||
| 15 | + Schema::table('users', function (Blueprint $table) { | ||
| 16 | + $table->integer('image_id')->default(0); | ||
| 17 | + }); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * Reverse the migrations. | ||
| 22 | + * | ||
| 23 | + * @return void | ||
| 24 | + */ | ||
| 25 | + public function down() | ||
| 26 | + { | ||
| 27 | + Schema::table('users', function (Blueprint $table) { | ||
| 28 | + $table->dropColumn('image_id'); | ||
| 29 | + }); | ||
| 30 | + } | ||
| 31 | +} |
| ... | @@ -23,7 +23,9 @@ class DummyContentSeeder extends Seeder | ... | @@ -23,7 +23,9 @@ class DummyContentSeeder extends Seeder |
| 23 | $pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); | 23 | $pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); |
| 24 | $chapter->pages()->saveMany($pages); | 24 | $chapter->pages()->saveMany($pages); |
| 25 | }); | 25 | }); |
| 26 | + $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); | ||
| 26 | $book->chapters()->saveMany($chapters); | 27 | $book->chapters()->saveMany($chapters); |
| 28 | + $book->pages()->saveMany($pages); | ||
| 27 | }); | 29 | }); |
| 28 | } | 30 | } |
| 29 | } | 31 | } | ... | ... |
| 1 | var elixir = require('laravel-elixir'); | 1 | var elixir = require('laravel-elixir'); |
| 2 | 2 | ||
| 3 | +// Custom extensions | ||
| 4 | +var gulp = require('gulp'); | ||
| 5 | +var Task = elixir.Task; | ||
| 6 | +var fs = require('fs'); | ||
| 7 | + | ||
| 8 | +elixir.extend('queryVersion', function(inputFiles) { | ||
| 9 | + new Task('queryVersion', function() { | ||
| 10 | + var manifestObject = {}; | ||
| 11 | + var uidString = Date.now().toString(16).slice(4); | ||
| 12 | + for (var i = 0; i < inputFiles.length; i++) { | ||
| 13 | + var file = inputFiles[i]; | ||
| 14 | + manifestObject[file] = file + '?version=' + uidString; | ||
| 15 | + } | ||
| 16 | + var fileContents = JSON.stringify(manifestObject, null, 1); | ||
| 17 | + fs.writeFileSync('public/build/manifest.json', fileContents); | ||
| 18 | + }).watch(['./public/css/*.css', './public/js/*.js']); | ||
| 19 | +}); | ||
| 20 | + | ||
| 3 | elixir(function(mix) { | 21 | elixir(function(mix) { |
| 4 | mix.sass('styles.scss') | 22 | mix.sass('styles.scss') |
| 5 | .sass('print-styles.scss') | 23 | .sass('print-styles.scss') |
| 6 | .browserify(['jquery-extensions.js', 'global.js'], 'public/js/common.js') | 24 | .browserify(['jquery-extensions.js', 'global.js'], 'public/js/common.js') |
| 7 | - .version(['css/styles.css', 'css/print-styles.css', 'js/common.js']); | 25 | + .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); |
| 8 | }); | 26 | }); | ... | ... |
| ... | @@ -26,5 +26,6 @@ | ... | @@ -26,5 +26,6 @@ |
| 26 | <env name="QUEUE_DRIVER" value="sync"/> | 26 | <env name="QUEUE_DRIVER" value="sync"/> |
| 27 | <env name="DB_CONNECTION" value="mysql_testing"/> | 27 | <env name="DB_CONNECTION" value="mysql_testing"/> |
| 28 | <env name="MAIL_PRETEND" value="true"/> | 28 | <env name="MAIL_PRETEND" value="true"/> |
| 29 | + <env name="DISABLE_EXTERNAL_SERVICES" value="true"/> | ||
| 29 | </php> | 30 | </php> |
| 30 | </phpunit> | 31 | </phpunit> | ... | ... |
public/user_avatar.png
0 → 100644
7.23 KB
| ... | @@ -5,7 +5,7 @@ A platform to create documentation/wiki content. General information about BookS | ... | @@ -5,7 +5,7 @@ A platform to create documentation/wiki content. General information about BookS |
| 5 | 5 | ||
| 6 | ## Requirements | 6 | ## Requirements |
| 7 | 7 | ||
| 8 | -BookStack has the similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. | 8 | +BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. |
| 9 | 9 | ||
| 10 | * PHP >= 5.5.9 | 10 | * PHP >= 5.5.9 |
| 11 | * OpenSSL PHP Extension | 11 | * OpenSSL PHP Extension |
| ... | @@ -25,11 +25,11 @@ Ensure the requirements are met before installing. | ... | @@ -25,11 +25,11 @@ Ensure the requirements are met before installing. |
| 25 | 25 | ||
| 26 | This project currently uses the `release` branch of this repository as a stable channel for providing updates. | 26 | This project currently uses the `release` branch of this repository as a stable channel for providing updates. |
| 27 | 27 | ||
| 28 | -The installation is currently somewhat complicated. Some PHP/Laravel experience will benefit. | 28 | +The installation is currently somewhat complicated and will be made simpler in future releases. Some PHP/Laravel experience will currently benefit. |
| 29 | 29 | ||
| 30 | 1. Clone the release branch of this repository into a folder. | 30 | 1. Clone the release branch of this repository into a folder. |
| 31 | 31 | ||
| 32 | -``` | 32 | +``` |
| 33 | git clone https://github.com/ssddanbrown/BookStack.git --branch release --single-branch | 33 | git clone https://github.com/ssddanbrown/BookStack.git --branch release --single-branch |
| 34 | ``` | 34 | ``` |
| 35 | 35 | ||
| ... | @@ -37,7 +37,7 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single | ... | @@ -37,7 +37,7 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single |
| 37 | 3. Copy the `.env.example` file to `.env` and fill with your own database and mail details. | 37 | 3. Copy the `.env.example` file to `.env` and fill with your own database and mail details. |
| 38 | 4. Ensure the `storage` & `bootstrap/cache` folders are writable by the web server. | 38 | 4. Ensure the `storage` & `bootstrap/cache` folders are writable by the web server. |
| 39 | 5. In the application root, Run `php artisan key:generate` to generate a unique application key. | 39 | 5. In the application root, Run `php artisan key:generate` to generate a unique application key. |
| 40 | -6. If not using apache or `.htaccess` files are disable you will have to create some URL rewrite rules as shown below. | 40 | +6. If not using apache or if `.htaccess` files are disabled you will have to create some URL rewrite rules as shown below. |
| 41 | 7. Run `php migrate` to update the database. | 41 | 7. Run `php migrate` to update the database. |
| 42 | 8. Done! You can now login using the default admin details `admin@admin.com` with a password of `password`. It is recommended to change these details directly after first logging in. | 42 | 8. Done! You can now login using the default admin details `admin@admin.com` with a password of `password`. It is recommended to change these details directly after first logging in. |
| 43 | 43 | ||
| ... | @@ -76,3 +76,17 @@ Once done you can run `phpunit` in the application root directory to run all tes | ... | @@ -76,3 +76,17 @@ Once done you can run `phpunit` in the application root directory to run all tes |
| 76 | ## License | 76 | ## License |
| 77 | 77 | ||
| 78 | BookStack is provided under the MIT License. | 78 | BookStack is provided under the MIT License. |
| 79 | + | ||
| 80 | +## Attribution | ||
| 81 | + | ||
| 82 | +These are the great projects used to help build BookStack: | ||
| 83 | + | ||
| 84 | +* [Laravel](http://laravel.com/) | ||
| 85 | +* [VueJS](http://vuejs.org/) | ||
| 86 | +* [jQuery](https://jquery.com/) | ||
| 87 | +* [TinyMCE](https://www.tinymce.com/) | ||
| 88 | +* [highlight.js](https://highlightjs.org/) | ||
| 89 | +* [jQuery Sortable](https://johnny.github.io/jquery-sortable/) | ||
| 90 | +* [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html) | ||
| 91 | +* [Dropzone.js](http://www.dropzonejs.com/) | ||
| 92 | +* [ZeroClipboard](http://zeroclipboard.org/) | ... | ... |
| ... | @@ -7,7 +7,7 @@ | ... | @@ -7,7 +7,7 @@ |
| 7 | <div v-for="image in images"> | 7 | <div v-for="image in images"> |
| 8 | <img class="anim fadeIn" | 8 | <img class="anim fadeIn" |
| 9 | :class="{selected: (image==selectedImage)}" | 9 | :class="{selected: (image==selectedImage)}" |
| 10 | - :src="image.thumbnail" :alt="image.title" :title="image.name" | 10 | + :src="image.thumbs.gallery" :alt="image.title" :title="image.name" |
| 11 | @click="imageClick(image)" | 11 | @click="imageClick(image)" |
| 12 | :style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"> | 12 | :style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"> |
| 13 | </div> | 13 | </div> |
| ... | @@ -76,6 +76,13 @@ | ... | @@ -76,6 +76,13 @@ |
| 76 | } | 76 | } |
| 77 | }, | 77 | }, |
| 78 | 78 | ||
| 79 | + props: { | ||
| 80 | + imageType: { | ||
| 81 | + type: String, | ||
| 82 | + required: true | ||
| 83 | + } | ||
| 84 | + }, | ||
| 85 | + | ||
| 79 | created: function () { | 86 | created: function () { |
| 80 | window.ImageManager = this; | 87 | window.ImageManager = this; |
| 81 | }, | 88 | }, |
| ... | @@ -88,7 +95,7 @@ | ... | @@ -88,7 +95,7 @@ |
| 88 | methods: { | 95 | methods: { |
| 89 | fetchData: function () { | 96 | fetchData: function () { |
| 90 | var _this = this; | 97 | var _this = this; |
| 91 | - this.$http.get('/images/all/' + _this.page, function (data) { | 98 | + this.$http.get('/images/' + _this.imageType + '/all/' + _this.page, function (data) { |
| 92 | _this.images = _this.images.concat(data.images); | 99 | _this.images = _this.images.concat(data.images); |
| 93 | _this.hasMore = data.hasMore; | 100 | _this.hasMore = data.hasMore; |
| 94 | _this.page++; | 101 | _this.page++; |
| ... | @@ -98,7 +105,7 @@ | ... | @@ -98,7 +105,7 @@ |
| 98 | setupDropZone: function () { | 105 | setupDropZone: function () { |
| 99 | var _this = this; | 106 | var _this = this; |
| 100 | var dropZone = new Dropzone(_this.$els.dropZone, { | 107 | var dropZone = new Dropzone(_this.$els.dropZone, { |
| 101 | - url: '/upload/image', | 108 | + url: '/images/' + _this.imageType + '/upload', |
| 102 | init: function () { | 109 | init: function () { |
| 103 | var dz = this; | 110 | var dz = this; |
| 104 | this.on("sending", function (file, xhr, data) { | 111 | this.on("sending", function (file, xhr, data) { |
| ... | @@ -110,8 +117,8 @@ | ... | @@ -110,8 +117,8 @@ |
| 110 | dz.removeFile(file); | 117 | dz.removeFile(file); |
| 111 | }); | 118 | }); |
| 112 | }); | 119 | }); |
| 113 | - this.on('error', function(file, errorMessage, xhr) { | 120 | + this.on('error', function (file, errorMessage, xhr) { |
| 114 | - if(errorMessage.file) { | 121 | + if (errorMessage.file) { |
| 115 | $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]); | 122 | $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]); |
| 116 | } | 123 | } |
| 117 | console.log(errorMessage); | 124 | console.log(errorMessage); |
| ... | @@ -120,6 +127,10 @@ | ... | @@ -120,6 +127,10 @@ |
| 120 | }); | 127 | }); |
| 121 | }, | 128 | }, |
| 122 | 129 | ||
| 130 | + returnCallback: function (image) { | ||
| 131 | + this.callback(image); | ||
| 132 | + }, | ||
| 133 | + | ||
| 123 | imageClick: function (image) { | 134 | imageClick: function (image) { |
| 124 | var dblClickTime = 380; | 135 | var dblClickTime = 380; |
| 125 | var cTime = (new Date()).getTime(); | 136 | var cTime = (new Date()).getTime(); |
| ... | @@ -127,7 +138,7 @@ | ... | @@ -127,7 +138,7 @@ |
| 127 | if (this.cClickTime !== 0 && timeDiff < dblClickTime && this.selectedImage === image) { | 138 | if (this.cClickTime !== 0 && timeDiff < dblClickTime && this.selectedImage === image) { |
| 128 | // DoubleClick | 139 | // DoubleClick |
| 129 | if (this.callback) { | 140 | if (this.callback) { |
| 130 | - this.callback(image); | 141 | + this.returnCallback(image); |
| 131 | } | 142 | } |
| 132 | this.hide(); | 143 | this.hide(); |
| 133 | } else { | 144 | } else { |
| ... | @@ -139,7 +150,7 @@ | ... | @@ -139,7 +150,7 @@ |
| 139 | 150 | ||
| 140 | selectButtonClick: function () { | 151 | selectButtonClick: function () { |
| 141 | if (this.callback) { | 152 | if (this.callback) { |
| 142 | - this.callback(this.selectedImage); | 153 | + this.returnCallback(this.selectedImage); |
| 143 | } | 154 | } |
| 144 | this.hide(); | 155 | this.hide(); |
| 145 | }, | 156 | }, | ... | ... |
| ... | @@ -7,31 +7,89 @@ | ... | @@ -7,31 +7,89 @@ |
| 7 | </div> | 7 | </div> |
| 8 | <button class="button" type="button" @click="showImageManager">Select Image</button> | 8 | <button class="button" type="button" @click="showImageManager">Select Image</button> |
| 9 | <br> | 9 | <br> |
| 10 | - <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> | 10 | + <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> |
| 11 | - <input type="hidden" :name="name" :id="name" v-model="image"> | 11 | + <input type="hidden" :name="name" :id="name" v-model="value"> |
| 12 | </div> | 12 | </div> |
| 13 | </template> | 13 | </template> |
| 14 | 14 | ||
| 15 | <script> | 15 | <script> |
| 16 | module.exports = { | 16 | module.exports = { |
| 17 | - props: ['currentImage', 'name', 'imageClass', 'defaultImage'], | 17 | + props: { |
| 18 | + currentImage: { | ||
| 19 | + required: true, | ||
| 20 | + type: String | ||
| 21 | + }, | ||
| 22 | + currentId: { | ||
| 23 | + required: false, | ||
| 24 | + default: 'false', | ||
| 25 | + type: String | ||
| 26 | + }, | ||
| 27 | + name: { | ||
| 28 | + required: true, | ||
| 29 | + type: String | ||
| 30 | + }, | ||
| 31 | + defaultImage: { | ||
| 32 | + required: true, | ||
| 33 | + type: String | ||
| 34 | + }, | ||
| 35 | + imageClass: { | ||
| 36 | + required: true, | ||
| 37 | + type: String | ||
| 38 | + }, | ||
| 39 | + resizeWidth: { | ||
| 40 | + type: String | ||
| 41 | + }, | ||
| 42 | + resizeHeight: { | ||
| 43 | + type: String | ||
| 44 | + }, | ||
| 45 | + resizeCrop: { | ||
| 46 | + type: Boolean | ||
| 47 | + }, | ||
| 48 | + showRemove: { | ||
| 49 | + type: Boolean, | ||
| 50 | + default: 'true' | ||
| 51 | + } | ||
| 52 | + }, | ||
| 18 | data: function() { | 53 | data: function() { |
| 19 | return { | 54 | return { |
| 20 | - image: this.currentImage | 55 | + image: this.currentImage, |
| 56 | + value: false | ||
| 21 | } | 57 | } |
| 22 | }, | 58 | }, |
| 59 | + compiled: function() { | ||
| 60 | + this.value = this.currentId === 'false' ? this.currentImage : this.currentId; | ||
| 61 | + }, | ||
| 23 | methods: { | 62 | methods: { |
| 63 | + setCurrentValue: function(imageModel, imageUrl) { | ||
| 64 | + this.image = imageUrl; | ||
| 65 | + this.value = this.currentId === 'false' ? imageUrl : imageModel.id; | ||
| 66 | + }, | ||
| 24 | showImageManager: function(e) { | 67 | showImageManager: function(e) { |
| 25 | var _this = this; | 68 | var _this = this; |
| 26 | ImageManager.show(function(image) { | 69 | ImageManager.show(function(image) { |
| 27 | - _this.image = image.url; | 70 | + _this.updateImageFromModel(image); |
| 28 | }); | 71 | }); |
| 29 | }, | 72 | }, |
| 30 | reset: function() { | 73 | reset: function() { |
| 31 | - this.image = ''; | 74 | + this.setCurrentValue({id: 0}, this.defaultImage); |
| 32 | }, | 75 | }, |
| 33 | remove: function() { | 76 | remove: function() { |
| 34 | this.image = 'none'; | 77 | this.image = 'none'; |
| 78 | + }, | ||
| 79 | + updateImageFromModel: function(model) { | ||
| 80 | + var _this = this; | ||
| 81 | + var isResized = _this.resizeWidth && _this.resizeHeight; | ||
| 82 | + | ||
| 83 | + if (!isResized) { | ||
| 84 | + _this.setCurrentValue(model, model.url); | ||
| 85 | + return; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + var cropped = _this.resizeCrop ? 'true' : 'false'; | ||
| 89 | + var requestString = '/images/thumb/' + model.id + '/' + _this.resizeWidth + '/' + _this.resizeHeight + '/' + cropped; | ||
| 90 | + _this.$http.get(requestString, function(data) { | ||
| 91 | + _this.setCurrentValue(model, data.url); | ||
| 92 | + }); | ||
| 35 | } | 93 | } |
| 36 | } | 94 | } |
| 37 | }; | 95 | }; | ... | ... |
| ... | @@ -100,7 +100,7 @@ module.exports = { | ... | @@ -100,7 +100,7 @@ module.exports = { |
| 100 | onclick: function() { | 100 | onclick: function() { |
| 101 | ImageManager.show(function(image) { | 101 | ImageManager.show(function(image) { |
| 102 | var html = '<a href="'+image.url+'" target="_blank">'; | 102 | var html = '<a href="'+image.url+'" target="_blank">'; |
| 103 | - html += '<img src="'+image.display+'" alt="'+image.name+'">'; | 103 | + html += '<img src="'+image.thumbs.display+'" alt="'+image.name+'">'; |
| 104 | html += '</a>'; | 104 | html += '</a>'; |
| 105 | editor.execCommand('mceInsertContent', false, html); | 105 | editor.execCommand('mceInsertContent', false, html); |
| 106 | }); | 106 | }); | ... | ... |
| ... | @@ -118,6 +118,9 @@ | ... | @@ -118,6 +118,9 @@ |
| 118 | text-decoration: none; | 118 | text-decoration: none; |
| 119 | } | 119 | } |
| 120 | } | 120 | } |
| 121 | + li a i { | ||
| 122 | + padding-right: $-xs + 2px; | ||
| 123 | + } | ||
| 121 | li, a { | 124 | li, a { |
| 122 | display: block; | 125 | display: block; |
| 123 | } | 126 | } |
| ... | @@ -150,11 +153,11 @@ | ... | @@ -150,11 +153,11 @@ |
| 150 | } | 153 | } |
| 151 | .list-item-page { | 154 | .list-item-page { |
| 152 | border-bottom: none; | 155 | border-bottom: none; |
| 156 | + border-left: 5px solid $color-page; | ||
| 157 | + margin: 10px 10px; | ||
| 153 | } | 158 | } |
| 154 | .page { | 159 | .page { |
| 155 | color: $color-page !important; | 160 | color: $color-page !important; |
| 156 | - border-left: 5px solid $color-page; | ||
| 157 | - margin: 10px 10px; | ||
| 158 | border-bottom: none; | 161 | border-bottom: none; |
| 159 | &.selected { | 162 | &.selected { |
| 160 | background-color: rgba($color-page, 0.1); | 163 | background-color: rgba($color-page, 0.1); | ... | ... |
| ... | @@ -32,10 +32,16 @@ body.dragging, body.dragging * { | ... | @@ -32,10 +32,16 @@ body.dragging, body.dragging * { |
| 32 | .avatar { | 32 | .avatar { |
| 33 | border-radius: 100%; | 33 | border-radius: 100%; |
| 34 | background-color: #EEE; | 34 | background-color: #EEE; |
| 35 | + width: 30px; | ||
| 36 | + height: 30px; | ||
| 35 | &.med { | 37 | &.med { |
| 36 | width: 40px; | 38 | width: 40px; |
| 37 | height: 40px; | 39 | height: 40px; |
| 38 | } | 40 | } |
| 41 | + &.large { | ||
| 42 | + width: 80px; | ||
| 43 | + height: 80px; | ||
| 44 | + } | ||
| 39 | } | 45 | } |
| 40 | 46 | ||
| 41 | // System wide notifications | 47 | // System wide notifications | ... | ... |
| 1 | <!DOCTYPE html> | 1 | <!DOCTYPE html> |
| 2 | <html> | 2 | <html> |
| 3 | <head> | 3 | <head> |
| 4 | - <title>BookStack</title> | 4 | + <title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ Setting::get('app-name', 'BookStack') }}</title> |
| 5 | 5 | ||
| 6 | <!-- Meta --> | 6 | <!-- Meta --> |
| 7 | <meta name="viewport" content="width=device-width"> | 7 | <meta name="viewport" content="width=device-width"> |
| ... | @@ -9,8 +9,8 @@ | ... | @@ -9,8 +9,8 @@ |
| 9 | <meta charset="utf-8"> | 9 | <meta charset="utf-8"> |
| 10 | 10 | ||
| 11 | <!-- Styles and Fonts --> | 11 | <!-- Styles and Fonts --> |
| 12 | - <link rel="stylesheet" href="{{ elixir('css/styles.css') }}"> | 12 | + <link rel="stylesheet" href="{{ versioned_asset('css/styles.css') }}"> |
| 13 | - <link rel="stylesheet" media="print" href="{{ elixir('css/print-styles.css') }}"> | 13 | + <link rel="stylesheet" media="print" href="{{ versioned_asset('css/print-styles.css') }}"> |
| 14 | <link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'> | 14 | <link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'> |
| 15 | <link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css"> | 15 | <link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css"> |
| 16 | 16 | ||
| ... | @@ -79,6 +79,6 @@ | ... | @@ -79,6 +79,6 @@ |
| 79 | </section> | 79 | </section> |
| 80 | 80 | ||
| 81 | @yield('bottom') | 81 | @yield('bottom') |
| 82 | -<script src="{{ elixir('js/common.js') }}"></script> | 82 | +<script src="{{ versioned_asset('js/common.js') }}"></script> |
| 83 | </body> | 83 | </body> |
| 84 | </html> | 84 | </html> | ... | ... |
| ... | @@ -34,11 +34,22 @@ | ... | @@ -34,11 +34,22 @@ |
| 34 | @endif | 34 | @endif |
| 35 | </div> | 35 | </div> |
| 36 | <div class="col-sm-4 col-sm-offset-1"> | 36 | <div class="col-sm-4 col-sm-offset-1"> |
| 37 | + <div id="recents"> | ||
| 38 | + @if($recents) | ||
| 39 | + <div class="margin-top large"> </div> | ||
| 40 | + <h3>Recently Viewed</h3> | ||
| 41 | + @include('partials/entity-list', ['entities' => $recents]) | ||
| 42 | + @endif | ||
| 43 | + </div> | ||
| 37 | <div class="margin-top large"> </div> | 44 | <div class="margin-top large"> </div> |
| 38 | - @if($recents) | 45 | + <div id="popular"> |
| 39 | - <h3>Recently Viewed</h3> | 46 | + <h3>Popular Books</h3> |
| 40 | - @include('partials/entity-list', ['entities' => $recents]) | 47 | + @if(count($popular) > 0) |
| 41 | - @endif | 48 | + @include('partials/entity-list', ['entities' => $popular]) |
| 49 | + @else | ||
| 50 | + <p class="text-muted">The most popular books will appear here.</p> | ||
| 51 | + @endif | ||
| 52 | + </div> | ||
| 42 | </div> | 53 | </div> |
| 43 | </div> | 54 | </div> |
| 44 | </div> | 55 | </div> | ... | ... |
| ... | @@ -58,7 +58,7 @@ | ... | @@ -58,7 +58,7 @@ |
| 58 | <p class="text-muted small"> | 58 | <p class="text-muted small"> |
| 59 | Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif | 59 | Created {{$book->created_at->diffForHumans()}} @if($book->createdBy) by {{$book->createdBy->name}} @endif |
| 60 | <br> | 60 | <br> |
| 61 | - Last Updated {{$book->updated_at->diffForHumans()}} @if($book->createdBy) by {{$book->updatedBy->name}} @endif | 61 | + Last Updated {{$book->updated_at->diffForHumans()}} @if($book->updatedBy) by {{$book->updatedBy->name}} @endif |
| 62 | </p> | 62 | </p> |
| 63 | </div> | 63 | </div> |
| 64 | </div> | 64 | </div> | ... | ... |
| ... | @@ -2,7 +2,7 @@ | ... | @@ -2,7 +2,7 @@ |
| 2 | <h3 class="text-book"><i class="zmdi zmdi-book"></i>{{ $book->name }}</h3> | 2 | <h3 class="text-book"><i class="zmdi zmdi-book"></i>{{ $book->name }}</h3> |
| 3 | <ul class="sortable-page-list sort-list"> | 3 | <ul class="sortable-page-list sort-list"> |
| 4 | @foreach($bookChildren as $bookChild) | 4 | @foreach($bookChildren as $bookChild) |
| 5 | - <li data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getName() }}" class="text-{{ $bookChild->getName() }}"> | 5 | + <li data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getClassName() }}" class="text-{{ $bookChild->getClassName() }}"> |
| 6 | <i class="zmdi {{ $bookChild->isA('chapter') ? 'zmdi-collection-bookmark':'zmdi-file-text'}}"></i>{{ $bookChild->name }} | 6 | <i class="zmdi {{ $bookChild->isA('chapter') ? 'zmdi-collection-bookmark':'zmdi-file-text'}}"></i>{{ $bookChild->name }} |
| 7 | @if($bookChild->isA('chapter')) | 7 | @if($bookChild->isA('chapter')) |
| 8 | <ul> | 8 | <ul> | ... | ... |
| ... | @@ -56,7 +56,7 @@ | ... | @@ -56,7 +56,7 @@ |
| 56 | <p class="text-muted small"> | 56 | <p class="text-muted small"> |
| 57 | Created {{$chapter->created_at->diffForHumans()}} @if($chapter->createdBy) by {{$chapter->createdBy->name}} @endif | 57 | Created {{$chapter->created_at->diffForHumans()}} @if($chapter->createdBy) by {{$chapter->createdBy->name}} @endif |
| 58 | <br> | 58 | <br> |
| 59 | - Last Updated {{$chapter->updated_at->diffForHumans()}} @if($chapter->createdBy) by {{$chapter->updatedBy->name}} @endif | 59 | + Last Updated {{$chapter->updated_at->diffForHumans()}} @if($chapter->updatedBy) by {{$chapter->updatedBy->name}} @endif |
| 60 | </p> | 60 | </p> |
| 61 | </div> | 61 | </div> |
| 62 | <div class="col-md-3 col-md-offset-1"> | 62 | <div class="col-md-3 col-md-offset-1"> | ... | ... |
| ... | @@ -14,6 +14,6 @@ | ... | @@ -14,6 +14,6 @@ |
| 14 | @include('pages/form', ['model' => $page]) | 14 | @include('pages/form', ['model' => $page]) |
| 15 | </form> | 15 | </form> |
| 16 | </div> | 16 | </div> |
| 17 | - <image-manager></image-manager> | 17 | + <image-manager image-type="gallery"></image-manager> |
| 18 | 18 | ||
| 19 | @stop | 19 | @stop |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
| ... | @@ -32,8 +32,12 @@ | ... | @@ -32,8 +32,12 @@ |
| 32 | @foreach($page->revisions as $revision) | 32 | @foreach($page->revisions as $revision) |
| 33 | <tr> | 33 | <tr> |
| 34 | <td>{{$revision->name}}</td> | 34 | <td>{{$revision->name}}</td> |
| 35 | - <td style="line-height: 0;"><img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}"></td> | 35 | + <td style="line-height: 0;"> |
| 36 | - <td> {{$revision->createdBy->name}}</td> | 36 | + @if($revision->createdBy) |
| 37 | + <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{$revision->createdBy->name}}"> | ||
| 38 | + @endif | ||
| 39 | + </td> | ||
| 40 | + <td> @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif</td> | ||
| 37 | <td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} ({{$revision->created_at->diffForHumans()}})</small></td> | 41 | <td><small>{{$revision->created_at->format('jS F, Y H:i:s')}} ({{$revision->created_at->diffForHumans()}})</small></td> |
| 38 | <td> | 42 | <td> |
| 39 | <a href="{{$revision->getUrl()}}" target="_blank">Preview</a> | 43 | <a href="{{$revision->getUrl()}}" target="_blank">Preview</a> | ... | ... |
| ... | @@ -7,12 +7,12 @@ | ... | @@ -7,12 +7,12 @@ |
| 7 | <div class="row"> | 7 | <div class="row"> |
| 8 | <div class="col-sm-6 faded"> | 8 | <div class="col-sm-6 faded"> |
| 9 | <div class="breadcrumbs"> | 9 | <div class="breadcrumbs"> |
| 10 | - <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->name }}</a> | 10 | + <a href="{{$book->getUrl()}}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $book->getShortName() }}</a> |
| 11 | @if($page->hasChapter()) | 11 | @if($page->hasChapter()) |
| 12 | <span class="sep">»</span> | 12 | <span class="sep">»</span> |
| 13 | <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button"> | 13 | <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button"> |
| 14 | <i class="zmdi zmdi-collection-bookmark"></i> | 14 | <i class="zmdi zmdi-collection-bookmark"></i> |
| 15 | - {{$page->chapter->name}} | 15 | + {{$page->chapter->getShortName()}} |
| 16 | </a> | 16 | </a> |
| 17 | @endif | 17 | @endif |
| 18 | </div> | 18 | </div> |
| ... | @@ -53,7 +53,7 @@ | ... | @@ -53,7 +53,7 @@ |
| 53 | <p class="text-muted small"> | 53 | <p class="text-muted small"> |
| 54 | Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif | 54 | Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif |
| 55 | <br> | 55 | <br> |
| 56 | - Last Updated {{$page->updated_at->diffForHumans()}} @if($page->createdBy) by {{$page->updatedBy->name}} @endif | 56 | + Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif |
| 57 | </p> | 57 | </p> |
| 58 | 58 | ||
| 59 | </div> | 59 | </div> | ... | ... |
| ... | @@ -6,8 +6,8 @@ | ... | @@ -6,8 +6,8 @@ |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | @foreach($sidebarTree as $bookChild) | 8 | @foreach($sidebarTree as $bookChild) |
| 9 | - <li class="list-item-{{ $bookChild->getName() }} {{ $bookChild->getName() }}"> | 9 | + <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }}"> |
| 10 | - <a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getName() }} {{ $current->matches($bookChild)? 'selected' : '' }}"> | 10 | + <a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getClassName() }} {{ $current->matches($bookChild)? 'selected' : '' }}"> |
| 11 | @if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }} | 11 | @if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }} |
| 12 | </a> | 12 | </a> |
| 13 | 13 | ... | ... |
| ... | @@ -10,6 +10,8 @@ | ... | @@ -10,6 +10,8 @@ |
| 10 | <div class="right"> | 10 | <div class="right"> |
| 11 | @if($activity->user) | 11 | @if($activity->user) |
| 12 | {{$activity->user->name}} | 12 | {{$activity->user->name}} |
| 13 | + @else | ||
| 14 | + A deleted user | ||
| 13 | @endif | 15 | @endif |
| 14 | 16 | ||
| 15 | {{ $activity->getText() }} | 17 | {{ $activity->getText() }} | ... | ... |
| 1 | 1 | ||
| 2 | @if(count($entities) > 0) | 2 | @if(count($entities) > 0) |
| 3 | - @foreach($entities as $entity) | 3 | + @foreach($entities as $index => $entity) |
| 4 | @if($entity->isA('page')) | 4 | @if($entity->isA('page')) |
| 5 | @include('pages/list-item', ['page' => $entity]) | 5 | @include('pages/list-item', ['page' => $entity]) |
| 6 | @elseif($entity->isA('book')) | 6 | @elseif($entity->isA('book')) |
| ... | @@ -8,7 +8,11 @@ | ... | @@ -8,7 +8,11 @@ |
| 8 | @elseif($entity->isA('chapter')) | 8 | @elseif($entity->isA('chapter')) |
| 9 | @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) | 9 | @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) |
| 10 | @endif | 10 | @endif |
| 11 | - <hr> | 11 | + |
| 12 | + @if($index !== count($entities) - 1) | ||
| 13 | + <hr> | ||
| 14 | + @endif | ||
| 15 | + | ||
| 12 | @endforeach | 16 | @endforeach |
| 13 | @else | 17 | @else |
| 14 | <p class="text-muted"> | 18 | <p class="text-muted"> | ... | ... |
| ... | @@ -23,12 +23,17 @@ | ... | @@ -23,12 +23,17 @@ |
| 23 | <label>Allow public viewing?</label> | 23 | <label>Allow public viewing?</label> |
| 24 | <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch> | 24 | <toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch> |
| 25 | </div> | 25 | </div> |
| 26 | + <div class="form-group"> | ||
| 27 | + <label>Enable higher security image uploads?</label> | ||
| 28 | + <p class="small">For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.</p> | ||
| 29 | + <toggle-switch name="setting-app-secure-images" value="{{ Setting::get('app-secure-images') }}"></toggle-switch> | ||
| 30 | + </div> | ||
| 26 | </div> | 31 | </div> |
| 27 | <div class="col-md-6"> | 32 | <div class="col-md-6"> |
| 28 | <div class="form-group" id="logo-control"> | 33 | <div class="form-group" id="logo-control"> |
| 29 | <label for="setting-app-logo">Application Logo</label> | 34 | <label for="setting-app-logo">Application Logo</label> |
| 30 | - <p class="small">This image should be 43px in height. </p> | 35 | + <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p> |
| 31 | - <image-picker current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker> | 36 | + <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> |
| 32 | </div> | 37 | </div> |
| 33 | </div> | 38 | </div> |
| 34 | </div> | 39 | </div> |
| ... | @@ -57,7 +62,7 @@ | ... | @@ -57,7 +62,7 @@ |
| 57 | </select> | 62 | </select> |
| 58 | </div> | 63 | </div> |
| 59 | <div class="form-group"> | 64 | <div class="form-group"> |
| 60 | - <label for="setting-registration-confirmation">Require Email Confirmation?</label> | 65 | + <label for="setting-registration-confirmation">Require email confirmation?</label> |
| 61 | <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p> | 66 | <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p> |
| 62 | <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch> | 67 | <toggle-switch name="setting-registration-confirmation" value="{{ Setting::get('registration-confirmation') }}"></toggle-switch> |
| 63 | </div> | 68 | </div> |
| ... | @@ -81,6 +86,6 @@ | ... | @@ -81,6 +86,6 @@ |
| 81 | 86 | ||
| 82 | </div> | 87 | </div> |
| 83 | 88 | ||
| 84 | -<image-manager></image-manager> | 89 | +<image-manager image-type="system"></image-manager> |
| 85 | 90 | ||
| 86 | @stop | 91 | @stop | ... | ... |
| ... | @@ -19,26 +19,25 @@ | ... | @@ -19,26 +19,25 @@ |
| 19 | 19 | ||
| 20 | 20 | ||
| 21 | <div class="container small"> | 21 | <div class="container small"> |
| 22 | - | 22 | + <form action="/users/{{$user->id}}" method="post"> |
| 23 | <div class="row"> | 23 | <div class="row"> |
| 24 | <div class="col-md-6"> | 24 | <div class="col-md-6"> |
| 25 | <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1> | 25 | <h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1> |
| 26 | - <form action="/users/{{$user->id}}" method="post"> | 26 | + {!! csrf_field() !!} |
| 27 | - {!! csrf_field() !!} | 27 | + <input type="hidden" name="_method" value="put"> |
| 28 | - <input type="hidden" name="_method" value="put"> | 28 | + @include('users/form', ['model' => $user]) |
| 29 | - @include('users/form', ['model' => $user]) | 29 | + |
| 30 | - </form> | ||
| 31 | </div> | 30 | </div> |
| 32 | <div class="col-md-6"> | 31 | <div class="col-md-6"> |
| 33 | <h1> </h1> | 32 | <h1> </h1> |
| 34 | - <div class="shaded padded margin-top"> | 33 | + <div class="form-group" id="logo-control"> |
| 35 | - <p> | 34 | + <label for="user-avatar">User Avatar</label> |
| 36 | - <img class="avatar" src="{{ $user->getAvatar(80) }}" alt="{{ $user->name }}"> | 35 | + <p class="small">This image should be approx 256px square.</p> |
| 37 | - </p> | 36 | + <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> |
| 38 | - <p class="text-muted">You can change your profile picture at <a href="http://en.gravatar.com/">Gravatar</a>.</p> | ||
| 39 | </div> | 37 | </div> |
| 40 | </div> | 38 | </div> |
| 41 | </div> | 39 | </div> |
| 40 | + </form> | ||
| 42 | 41 | ||
| 43 | <hr class="margin-top large"> | 42 | <hr class="margin-top large"> |
| 44 | 43 | ||
| ... | @@ -80,5 +79,5 @@ | ... | @@ -80,5 +79,5 @@ |
| 80 | </div> | 79 | </div> |
| 81 | 80 | ||
| 82 | <p class="margin-top large"><br></p> | 81 | <p class="margin-top large"><br></p> |
| 83 | - | 82 | + <image-manager image-type="user"></image-manager> |
| 84 | @stop | 83 | @stop | ... | ... |
| ... | @@ -36,4 +36,5 @@ | ... | @@ -36,4 +36,5 @@ |
| 36 | <div class="form-group"> | 36 | <div class="form-group"> |
| 37 | <a href="/users" class="button muted">Cancel</a> | 37 | <a href="/users" class="button muted">Cancel</a> |
| 38 | <button class="button pos" type="submit">Save</button> | 38 | <button class="button pos" type="submit">Save</button> |
| 39 | -</div> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 39 | +</div> | ||
| 40 | + | ... | ... |
tests/ActivityTrackingTest.php
0 → 100644
| 1 | +<?php | ||
| 2 | + | ||
| 3 | +use Illuminate\Foundation\Testing\WithoutMiddleware; | ||
| 4 | +use Illuminate\Foundation\Testing\DatabaseMigrations; | ||
| 5 | +use Illuminate\Foundation\Testing\DatabaseTransactions; | ||
| 6 | + | ||
| 7 | +class ActivityTrackingTest extends TestCase | ||
| 8 | +{ | ||
| 9 | + | ||
| 10 | + public function testRecentlyViewedBooks() | ||
| 11 | + { | ||
| 12 | + $books = \BookStack\Book::all()->take(10); | ||
| 13 | + | ||
| 14 | + $this->asAdmin()->visit('/books') | ||
| 15 | + ->dontSeeInElement('#recents', $books[0]->name) | ||
| 16 | + ->dontSeeInElement('#recents', $books[1]->name) | ||
| 17 | + ->visit($books[0]->getUrl()) | ||
| 18 | + ->visit($books[1]->getUrl()) | ||
| 19 | + ->visit('/books') | ||
| 20 | + ->seeInElement('#recents', $books[0]->name) | ||
| 21 | + ->seeInElement('#recents', $books[1]->name); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + public function testPopularBooks() | ||
| 25 | + { | ||
| 26 | + $books = \BookStack\Book::all()->take(10); | ||
| 27 | + | ||
| 28 | + $this->asAdmin()->visit('/books') | ||
| 29 | + ->dontSeeInElement('#popular', $books[0]->name) | ||
| 30 | + ->dontSeeInElement('#popular', $books[1]->name) | ||
| 31 | + ->visit($books[0]->getUrl()) | ||
| 32 | + ->visit($books[1]->getUrl()) | ||
| 33 | + ->visit($books[0]->getUrl()) | ||
| 34 | + ->visit('/books') | ||
| 35 | + ->seeInNthElement('#popular .book', 0, $books[0]->name) | ||
| 36 | + ->seeInNthElement('#popular .book', 1, $books[1]->name); | ||
| 37 | + } | ||
| 38 | +} |
| ... | @@ -171,4 +171,43 @@ class EntityTest extends TestCase | ... | @@ -171,4 +171,43 @@ class EntityTest extends TestCase |
| 171 | } | 171 | } |
| 172 | 172 | ||
| 173 | 173 | ||
| 174 | + public function testEntitiesViewableAfterCreatorDeletion() | ||
| 175 | + { | ||
| 176 | + // Create required assets and revisions | ||
| 177 | + $creator = $this->getNewUser(); | ||
| 178 | + $updater = $this->getNewUser(); | ||
| 179 | + $entities = $this->createEntityChainBelongingToUser($creator, $updater); | ||
| 180 | + $this->actingAs($creator); | ||
| 181 | + app('BookStack\Repos\UserRepo')->destroy($creator); | ||
| 182 | + app('BookStack\Repos\PageRepo')->saveRevision($entities['page']); | ||
| 183 | + | ||
| 184 | + $this->checkEntitiesViewable($entities); | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + public function testEntitiesViewableAfterUpdaterDeletion() | ||
| 188 | + { | ||
| 189 | + // Create required assets and revisions | ||
| 190 | + $creator = $this->getNewUser(); | ||
| 191 | + $updater = $this->getNewUser(); | ||
| 192 | + $entities = $this->createEntityChainBelongingToUser($creator, $updater); | ||
| 193 | + $this->actingAs($updater); | ||
| 194 | + app('BookStack\Repos\UserRepo')->destroy($updater); | ||
| 195 | + app('BookStack\Repos\PageRepo')->saveRevision($entities['page']); | ||
| 196 | + | ||
| 197 | + $this->checkEntitiesViewable($entities); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + private function checkEntitiesViewable($entities) | ||
| 201 | + { | ||
| 202 | + // Check pages and books are visible. | ||
| 203 | + $this->asAdmin(); | ||
| 204 | + $this->visit($entities['book']->getUrl())->seeStatusCode(200) | ||
| 205 | + ->visit($entities['chapter']->getUrl())->seeStatusCode(200) | ||
| 206 | + ->visit($entities['page']->getUrl())->seeStatusCode(200); | ||
| 207 | + // Check revision listing shows no errors. | ||
| 208 | + $this->visit($entities['page']->getUrl()) | ||
| 209 | + ->click('Revisions')->seeStatusCode(200); | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + | ||
| 174 | } | 213 | } | ... | ... |
| ... | @@ -48,4 +48,65 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase | ... | @@ -48,4 +48,65 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase |
| 48 | $settings->put($key, $value); | 48 | $settings->put($key, $value); |
| 49 | } | 49 | } |
| 50 | } | 50 | } |
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * Create a group of entities that belong to a specific user. | ||
| 54 | + * @param $creatorUser | ||
| 55 | + * @param $updaterUser | ||
| 56 | + * @return array | ||
| 57 | + */ | ||
| 58 | + protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false) | ||
| 59 | + { | ||
| 60 | + if ($updaterUser === false) $updaterUser = $creatorUser; | ||
| 61 | + $book = factory(BookStack\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]); | ||
| 62 | + $chapter = factory(BookStack\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]); | ||
| 63 | + $page = factory(BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]); | ||
| 64 | + $book->chapters()->saveMany([$chapter]); | ||
| 65 | + $chapter->pages()->saveMany([$page]); | ||
| 66 | + return [ | ||
| 67 | + 'book' => $book, | ||
| 68 | + 'chapter' => $chapter, | ||
| 69 | + 'page' => $page | ||
| 70 | + ]; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * Quick way to create a new user | ||
| 75 | + * @param array $attributes | ||
| 76 | + * @return mixed | ||
| 77 | + */ | ||
| 78 | + protected function getNewUser($attributes = []) | ||
| 79 | + { | ||
| 80 | + $user = factory(\BookStack\User::class)->create($attributes); | ||
| 81 | + $userRepo = app('BookStack\Repos\UserRepo'); | ||
| 82 | + $userRepo->attachDefaultRole($user); | ||
| 83 | + return $user; | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * Assert that a given string is seen inside an element. | ||
| 88 | + * | ||
| 89 | + * @param bool|string|null $element | ||
| 90 | + * @param integer $position | ||
| 91 | + * @param string $text | ||
| 92 | + * @param bool $negate | ||
| 93 | + * @return $this | ||
| 94 | + */ | ||
| 95 | + protected function seeInNthElement($element, $position, $text, $negate = false) | ||
| 96 | + { | ||
| 97 | + $method = $negate ? 'assertNotRegExp' : 'assertRegExp'; | ||
| 98 | + | ||
| 99 | + $rawPattern = preg_quote($text, '/'); | ||
| 100 | + | ||
| 101 | + $escapedPattern = preg_quote(e($text), '/'); | ||
| 102 | + | ||
| 103 | + $content = $this->crawler->filter($element)->eq($position)->html(); | ||
| 104 | + | ||
| 105 | + $pattern = $rawPattern == $escapedPattern | ||
| 106 | + ? $rawPattern : "({$rawPattern}|{$escapedPattern})"; | ||
| 107 | + | ||
| 108 | + $this->$method("/$pattern/i", $content); | ||
| 109 | + | ||
| 110 | + return $this; | ||
| 111 | + } | ||
| 51 | } | 112 | } | ... | ... |
-
Please register or sign in to post a comment