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