Merge branch 'master' into release
This commit is contained in:
commit
09f478bd74
|
@ -0,0 +1,4 @@
|
||||||
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
class AuthException extends PrettyException {}
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\AuthException;
|
||||||
|
use BookStack\Exceptions\PrettyException;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use BookStack\Exceptions\SocialSignInException;
|
use BookStack\Exceptions\SocialSignInException;
|
||||||
|
@ -115,6 +117,7 @@ class AuthController extends Controller
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param Authenticatable $user
|
* @param Authenticatable $user
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
* @throws AuthException
|
||||||
*/
|
*/
|
||||||
protected function authenticated(Request $request, Authenticatable $user)
|
protected function authenticated(Request $request, Authenticatable $user)
|
||||||
{
|
{
|
||||||
|
@ -132,6 +135,13 @@ class AuthController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$user->exists) {
|
if (!$user->exists) {
|
||||||
|
|
||||||
|
// Check for users with same email already
|
||||||
|
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||||
|
if ($alreadyUser) {
|
||||||
|
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
|
||||||
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
$this->userRepo->attachDefaultRole($user);
|
$this->userRepo->attachDefaultRole($user);
|
||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
|
@ -184,14 +194,11 @@ class AuthController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||||
$newUser->email_confirmed = false;
|
|
||||||
$newUser->save();
|
$newUser->save();
|
||||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||||
return redirect('/register/confirm');
|
return redirect('/register/confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
$newUser->email_confirmed = true;
|
|
||||||
|
|
||||||
auth()->login($newUser);
|
auth()->login($newUser);
|
||||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||||
return redirect($this->redirectPath());
|
return redirect($this->redirectPath());
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
<?php
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Repos\UserRepo;
|
use BookStack\Repos\UserRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use BookStack\Http\Requests;
|
use BookStack\Http\Requests;
|
||||||
use BookStack\Repos\BookRepo;
|
use BookStack\Repos\BookRepo;
|
||||||
use BookStack\Repos\ChapterRepo;
|
use BookStack\Repos\ChapterRepo;
|
||||||
|
@ -40,7 +36,6 @@ class BookController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the book.
|
* Display a listing of the book.
|
||||||
*
|
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
|
@ -54,7 +49,6 @@ class BookController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new book.
|
* Show the form for creating a new book.
|
||||||
*
|
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function create()
|
public function create()
|
||||||
|
@ -88,7 +82,6 @@ class BookController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the specified book.
|
* Display the specified book.
|
||||||
*
|
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
|
@ -103,7 +96,6 @@ class BookController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for editing the specified book.
|
* Show the form for editing the specified book.
|
||||||
*
|
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
|
@ -117,7 +109,6 @@ class BookController extends Controller
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified book in storage.
|
* Update the specified book in storage.
|
||||||
*
|
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
|
@ -267,7 +258,7 @@ class BookController extends Controller
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
$this->bookRepo->updateRestrictionsFromRequest($request, $book);
|
||||||
session()->flash('success', 'Page Restrictions Updated');
|
session()->flash('success', 'Book Restrictions Updated');
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,7 +187,7 @@ class ChapterController extends Controller
|
||||||
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
$this->chapterRepo->updateRestrictionsFromRequest($request, $chapter);
|
||||||
session()->flash('success', 'Page Restrictions Updated');
|
session()->flash('success', 'Chapter Restrictions Updated');
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
<?php
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
use BookStack\Repos\ImageRepo;
|
use BookStack\Repos\ImageRepo;
|
||||||
use Illuminate\Filesystem\Filesystem as File;
|
use Illuminate\Filesystem\Filesystem as File;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Intervention\Image\Facades\Image as ImageTool;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use BookStack\Image;
|
use BookStack\Image;
|
||||||
use BookStack\Repos\PageRepo;
|
use BookStack\Repos\PageRepo;
|
||||||
|
|
||||||
|
@ -44,6 +39,24 @@ class ImageController extends Controller
|
||||||
return response()->json($imgData);
|
return response()->json($imgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search through images within a particular type.
|
||||||
|
* @param $type
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function searchByType($type, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'term' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$searchTerm = $request->get('term');
|
||||||
|
$imgData = $this->imageRepo->searchPaginatedByType($type, $page,24, $searchTerm);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all images for a user.
|
* Get all images for a user.
|
||||||
* @param int $page
|
* @param int $page
|
||||||
|
@ -55,6 +68,27 @@ class ImageController extends Controller
|
||||||
return response()->json($imgData);
|
return response()->json($imgData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get gallery images with a specific filter such as book or page
|
||||||
|
* @param $filter
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
*/
|
||||||
|
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'page_id' => 'required|integer'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validFilters = collect(['page', 'book']);
|
||||||
|
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||||
|
|
||||||
|
$pageId = $request->get('page_id');
|
||||||
|
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||||
|
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles image uploads for use on pages.
|
* Handles image uploads for use on pages.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
|
|
|
@ -4,6 +4,7 @@ use Activity;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Repos\UserRepo;
|
use BookStack\Repos\UserRepo;
|
||||||
use BookStack\Services\ExportService;
|
use BookStack\Services\ExportService;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use BookStack\Http\Requests;
|
use BookStack\Http\Requests;
|
||||||
use BookStack\Repos\BookRepo;
|
use BookStack\Repos\BookRepo;
|
||||||
|
@ -88,7 +89,6 @@ class PageController extends Controller
|
||||||
|
|
||||||
$input = $request->all();
|
$input = $request->all();
|
||||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||||
$input['priority'] = $this->bookRepo->getNewPriority($book);
|
|
||||||
|
|
||||||
$draftPage = $this->pageRepo->getById($pageId, true);
|
$draftPage = $this->pageRepo->getById($pageId, true);
|
||||||
|
|
||||||
|
@ -96,6 +96,12 @@ class PageController extends Controller
|
||||||
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
|
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
|
if ($parent->isA('chapter')) {
|
||||||
|
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
|
||||||
|
} else {
|
||||||
|
$input['priority'] = $this->bookRepo->getNewPriority($parent);
|
||||||
|
}
|
||||||
|
|
||||||
$page = $this->pageRepo->publishDraft($draftPage, $input);
|
$page = $this->pageRepo->publishDraft($draftPage, $input);
|
||||||
|
|
||||||
Activity::add($page, 'page_create', $book->id);
|
Activity::add($page, 'page_create', $book->id);
|
||||||
|
@ -164,6 +170,7 @@ class PageController extends Controller
|
||||||
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||||
$page->name = $draft->name;
|
$page->name = $draft->name;
|
||||||
$page->html = $draft->html;
|
$page->html = $draft->html;
|
||||||
|
$page->markdown = $draft->markdown;
|
||||||
$page->isDraft = true;
|
$page->isDraft = true;
|
||||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
|
||||||
}
|
}
|
||||||
|
@ -204,12 +211,18 @@ class PageController extends Controller
|
||||||
$page = $this->pageRepo->getById($pageId, true);
|
$page = $this->pageRepo->getById($pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
if ($page->draft) {
|
if ($page->draft) {
|
||||||
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html']));
|
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
|
||||||
} else {
|
} else {
|
||||||
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html']));
|
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||||
}
|
}
|
||||||
$updateTime = $draft->updated_at->format('H:i');
|
|
||||||
return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]);
|
$updateTime = $draft->updated_at->timestamp;
|
||||||
|
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||||
|
return response()->json([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'Draft saved at ',
|
||||||
|
'timestamp' => $utcUpdateTimestamp
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,14 +11,12 @@ class Authenticate
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The Guard implementation.
|
* The Guard implementation.
|
||||||
*
|
|
||||||
* @var Guard
|
* @var Guard
|
||||||
*/
|
*/
|
||||||
protected $auth;
|
protected $auth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new filter instance.
|
* Create a new filter instance.
|
||||||
*
|
|
||||||
* @param Guard $auth
|
* @param Guard $auth
|
||||||
*/
|
*/
|
||||||
public function __construct(Guard $auth)
|
public function __construct(Guard $auth)
|
||||||
|
@ -28,14 +26,13 @@ class Authenticate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param \Closure $next
|
* @param \Closure $next
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
if(auth()->check() && auth()->user()->email_confirmed == false) {
|
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||||
return redirect()->guest('/register/confirm/awaiting');
|
return redirect()->guest('/register/confirm/awaiting');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,8 @@ Route::group(['middleware' => 'auth'], function () {
|
||||||
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
Route::post('/{type}/upload', 'ImageController@uploadByType');
|
||||||
Route::get('/{type}/all', 'ImageController@getAllByType');
|
Route::get('/{type}/all', 'ImageController@getAllByType');
|
||||||
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
|
||||||
|
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
|
||||||
|
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
|
||||||
Route::delete('/{imageId}', 'ImageController@destroy');
|
Route::delete('/{imageId}', 'ImageController@destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Page extends Entity
|
class Page extends Entity
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'priority'];
|
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||||
|
|
||||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class PageRevision extends Model
|
class PageRevision extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['name', 'html', 'text'];
|
protected $fillable = ['name', 'html', 'text', 'markdown'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user that created the page revision
|
* Get the user that created the page revision
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Permission extends Model
|
||||||
*/
|
*/
|
||||||
public function roles()
|
public function roles()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany('BookStack\Permissions');
|
return $this->belongsToMany('BookStack\Role');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -115,7 +115,7 @@ class LdapUserProvider implements UserProvider
|
||||||
$model->name = $userDetails['name'];
|
$model->name = $userDetails['name'];
|
||||||
$model->external_auth_id = $userDetails['uid'];
|
$model->external_auth_id = $userDetails['uid'];
|
||||||
$model->email = $userDetails['email'];
|
$model->email = $userDetails['email'];
|
||||||
$model->email_confirmed = true;
|
$model->email_confirmed = false;
|
||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,18 @@ class ChapterRepo extends EntityRepo
|
||||||
return $slug;
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a new priority value for a new page to be added
|
||||||
|
* to the given chapter.
|
||||||
|
* @param Chapter $chapter
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getNewPriority(Chapter $chapter)
|
||||||
|
{
|
||||||
|
$lastPage = $chapter->pages->last();
|
||||||
|
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get chapters by the given search term.
|
* Get chapters by the given search term.
|
||||||
* @param string $term
|
* @param string $term
|
||||||
|
|
|
@ -84,7 +84,7 @@ class EntityRepo
|
||||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||||
$additionalQuery($query);
|
$additionalQuery($query);
|
||||||
}
|
}
|
||||||
return $query->skip($page * $count)->take($count)->get();
|
return $query->with('book')->skip($page * $count)->take($count)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -114,7 +114,7 @@ class EntityRepo
|
||||||
{
|
{
|
||||||
return $this->restrictionService->enforcePageRestrictions($this->page)
|
return $this->restrictionService->enforcePageRestrictions($this->page)
|
||||||
->where('draft', '=', false)
|
->where('draft', '=', false)
|
||||||
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
|
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
|
|
||||||
use BookStack\Image;
|
use BookStack\Image;
|
||||||
|
use BookStack\Page;
|
||||||
use BookStack\Services\ImageService;
|
use BookStack\Services\ImageService;
|
||||||
use BookStack\Services\RestrictionService;
|
use BookStack\Services\RestrictionService;
|
||||||
use Setting;
|
use Setting;
|
||||||
|
@ -13,18 +14,21 @@ class ImageRepo
|
||||||
protected $image;
|
protected $image;
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
protected $restictionService;
|
protected $restictionService;
|
||||||
|
protected $page;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageRepo constructor.
|
* ImageRepo constructor.
|
||||||
* @param Image $image
|
* @param Image $image
|
||||||
* @param ImageService $imageService
|
* @param ImageService $imageService
|
||||||
* @param RestrictionService $restrictionService
|
* @param RestrictionService $restrictionService
|
||||||
|
* @param Page $page
|
||||||
*/
|
*/
|
||||||
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService)
|
public function __construct(Image $image, ImageService $imageService, RestrictionService $restrictionService, Page $page)
|
||||||
{
|
{
|
||||||
$this->image = $image;
|
$this->image = $image;
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
$this->restictionService = $restrictionService;
|
$this->restictionService = $restrictionService;
|
||||||
|
$this->page = $page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,6 +42,31 @@ class ImageRepo
|
||||||
return $this->image->findOrFail($id);
|
return $this->image->findOrFail($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a paginated query, returning in a standard format.
|
||||||
|
* Also runs the query through the restriction system.
|
||||||
|
* @param $query
|
||||||
|
* @param int $page
|
||||||
|
* @param int $pageSize
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function returnPaginated($query, $page = 0, $pageSize = 24)
|
||||||
|
{
|
||||||
|
$images = $this->restictionService->filterRelatedPages($query, 'images', 'uploaded_to');
|
||||||
|
$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
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a load images paginated, filtered by image type.
|
* Gets a load images paginated, filtered by image type.
|
||||||
* @param string $type
|
* @param string $type
|
||||||
|
@ -54,19 +83,46 @@ class ImageRepo
|
||||||
$images = $images->where('created_by', '=', $userFilter);
|
$images = $images->where('created_by', '=', $userFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
$images = $this->restictionService->filterRelatedPages($images, 'images', 'uploaded_to');
|
return $this->returnPaginated($images, $page, $pageSize);
|
||||||
$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) {
|
* Search for images by query, of a particular type.
|
||||||
$this->loadThumbs($image);
|
* @param string $type
|
||||||
});
|
* @param int $page
|
||||||
|
* @param int $pageSize
|
||||||
|
* @param string $searchTerm
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
|
||||||
|
{
|
||||||
|
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||||
|
return $this->returnPaginated($images, $page, $pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
/**
|
||||||
'images' => $returnImages,
|
* Get gallery images with a particular filter criteria such as
|
||||||
'hasMore' => $hasMore
|
* being within the current book or page.
|
||||||
];
|
* @param int $pagination
|
||||||
|
* @param int $pageSize
|
||||||
|
* @param $filter
|
||||||
|
* @param $pageId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
|
||||||
|
{
|
||||||
|
$images = $this->image->where('type', '=', 'gallery');
|
||||||
|
|
||||||
|
$page = $this->page->findOrFail($pageId);
|
||||||
|
|
||||||
|
if ($filter === 'page') {
|
||||||
|
$images = $images->where('uploaded_to', '=', $page->id);
|
||||||
|
} elseif ($filter === 'book') {
|
||||||
|
$validPageIds = $page->book->pages->pluck('id')->toArray();
|
||||||
|
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->returnPaginated($images, $pagination, $pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -312,6 +312,7 @@ class PageRepo extends EntityRepo
|
||||||
$page->fill($input);
|
$page->fill($input);
|
||||||
$page->html = $this->formatHtml($input['html']);
|
$page->html = $this->formatHtml($input['html']);
|
||||||
$page->text = strip_tags($page->html);
|
$page->text = strip_tags($page->html);
|
||||||
|
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||||
$page->updated_by = $userId;
|
$page->updated_by = $userId;
|
||||||
$page->save();
|
$page->save();
|
||||||
|
|
||||||
|
@ -348,6 +349,7 @@ class PageRepo extends EntityRepo
|
||||||
public function saveRevision(Page $page)
|
public function saveRevision(Page $page)
|
||||||
{
|
{
|
||||||
$revision = $this->pageRevision->fill($page->toArray());
|
$revision = $this->pageRevision->fill($page->toArray());
|
||||||
|
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||||
$revision->page_id = $page->id;
|
$revision->page_id = $page->id;
|
||||||
$revision->slug = $page->slug;
|
$revision->slug = $page->slug;
|
||||||
$revision->book_slug = $page->book->slug;
|
$revision->book_slug = $page->book->slug;
|
||||||
|
@ -386,6 +388,8 @@ class PageRepo extends EntityRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
$draft->fill($data);
|
$draft->fill($data);
|
||||||
|
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||||
|
|
||||||
$draft->save();
|
$draft->save();
|
||||||
return $draft;
|
return $draft;
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,7 +106,8 @@ class UserRepo
|
||||||
return $this->user->forceCreate([
|
return $this->user->forceCreate([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'email' => $data['email'],
|
'email' => $data['email'],
|
||||||
'password' => bcrypt($data['password'])
|
'password' => bcrypt($data['password']),
|
||||||
|
'email_confirmed' => false
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,15 @@ class Role extends Model
|
||||||
$this->permissions()->attach($permission->id);
|
$this->permissions()->attach($permission->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach a single permission from this role.
|
||||||
|
* @param Permission $permission
|
||||||
|
*/
|
||||||
|
public function detachPermission(Permission $permission)
|
||||||
|
{
|
||||||
|
$this->permissions()->detach($permission->id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the role object for the specified role.
|
* Get the role object for the specified role.
|
||||||
* @param $roleName
|
* @param $roleName
|
||||||
|
|
|
@ -44,28 +44,39 @@ class SettingService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a setting value from the cache or database.
|
* Gets a setting value from the cache or database.
|
||||||
|
* Looks at the system defaults if not cached or in database.
|
||||||
* @param $key
|
* @param $key
|
||||||
* @param $default
|
* @param $default
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
protected function getValueFromStore($key, $default)
|
protected function getValueFromStore($key, $default)
|
||||||
{
|
{
|
||||||
|
// Check for an overriding value
|
||||||
$overrideValue = $this->getOverrideValue($key);
|
$overrideValue = $this->getOverrideValue($key);
|
||||||
if ($overrideValue !== null) return $overrideValue;
|
if ($overrideValue !== null) return $overrideValue;
|
||||||
|
|
||||||
|
// Check the cache
|
||||||
$cacheKey = $this->cachePrefix . $key;
|
$cacheKey = $this->cachePrefix . $key;
|
||||||
if ($this->cache->has($cacheKey)) {
|
if ($this->cache->has($cacheKey)) {
|
||||||
return $this->cache->get($cacheKey);
|
return $this->cache->get($cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the database
|
||||||
$settingObject = $this->getSettingObjectByKey($key);
|
$settingObject = $this->getSettingObjectByKey($key);
|
||||||
|
|
||||||
if ($settingObject !== null) {
|
if ($settingObject !== null) {
|
||||||
$value = $settingObject->value;
|
$value = $settingObject->value;
|
||||||
$this->cache->forever($cacheKey, $value);
|
$this->cache->forever($cacheKey, $value);
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the defaults set in the app config.
|
||||||
|
$configPrefix = 'setting-defaults.' . $key;
|
||||||
|
if (config()->has($configPrefix)) {
|
||||||
|
$value = config($configPrefix);
|
||||||
|
$this->cache->forever($cacheKey, $value);
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?php namespace BookStack\Services;
|
<?php namespace BookStack\Services;
|
||||||
|
|
||||||
|
|
||||||
use BookStack\Entity;
|
use BookStack\Entity;
|
||||||
use BookStack\View;
|
use BookStack\View;
|
||||||
|
|
||||||
|
@ -47,7 +46,6 @@ class ViewService
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the entities with the most views.
|
* Get the entities with the most views.
|
||||||
* @param int $count
|
* @param int $count
|
||||||
|
@ -58,17 +56,13 @@ class ViewService
|
||||||
{
|
{
|
||||||
$skipCount = $count * $page;
|
$skipCount = $count * $page;
|
||||||
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
$query = $this->restrictionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||||
->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||||
->groupBy('viewable_id', 'viewable_type')
|
->groupBy('viewable_id', 'viewable_type')
|
||||||
->orderBy('view_count', 'desc');
|
->orderBy('view_count', 'desc');
|
||||||
|
|
||||||
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
|
||||||
|
|
||||||
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
|
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||||
$viewedEntities = $views->map(function ($item) {
|
|
||||||
return $item->viewable()->getResults();
|
|
||||||
});
|
|
||||||
return $viewedEntities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,21 +75,18 @@ class ViewService
|
||||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||||
{
|
{
|
||||||
if ($this->user === null) return collect();
|
if ($this->user === null) return collect();
|
||||||
$skipCount = $count * $page;
|
|
||||||
$query = $this->restrictionService
|
$query = $this->restrictionService
|
||||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||||
|
|
||||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||||
$query = $query->where('user_id', '=', auth()->user()->id);
|
$query = $query->where('user_id', '=', auth()->user()->id);
|
||||||
|
|
||||||
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
|
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||||
$viewedEntities = $views->map(function ($item) {
|
->skip($count * $page)->take($count)->get()->pluck('viewable');
|
||||||
return $item->viewable;
|
return $viewables;
|
||||||
});
|
|
||||||
return $viewedEntities;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all view counts by deleting all views.
|
* Reset all view counts by deleting all views.
|
||||||
*/
|
*/
|
||||||
|
@ -104,5 +95,4 @@ class ViewService
|
||||||
$this->view->truncate();
|
$this->view->truncate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -12,7 +12,8 @@
|
||||||
"barryvdh/laravel-ide-helper": "^2.1",
|
"barryvdh/laravel-ide-helper": "^2.1",
|
||||||
"barryvdh/laravel-debugbar": "^2.0",
|
"barryvdh/laravel-debugbar": "^2.0",
|
||||||
"league/flysystem-aws-s3-v3": "^1.0",
|
"league/flysystem-aws-s3-v3": "^1.0",
|
||||||
"barryvdh/laravel-dompdf": "0.6.*"
|
"barryvdh/laravel-dompdf": "0.6.*",
|
||||||
|
"predis/predis": "^1.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fzaninotto/faker": "~1.4",
|
"fzaninotto/faker": "~1.4",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,8 @@ return [
|
||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
'editor' => env('APP_EDITOR', 'html'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Debug Mode
|
| Application Debug Mode
|
||||||
|
|
|
@ -6,9 +6,8 @@ if (env('CACHE_DRIVER') === 'memcached') {
|
||||||
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
|
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
|
||||||
foreach ($memcachedServers as $index => $memcachedServer) {
|
foreach ($memcachedServers as $index => $memcachedServer) {
|
||||||
$memcachedServerDetails = explode(':', $memcachedServer);
|
$memcachedServerDetails = explode(':', $memcachedServer);
|
||||||
$components = count($memcachedServerDetails);
|
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
|
||||||
if ($components < 2) $memcachedServerDetails[] = '11211';
|
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
|
||||||
if ($components < 3) $memcachedServerDetails[] = '100';
|
|
||||||
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
|
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +82,6 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'prefix' => 'laravel',
|
'prefix' => env('CACHE_PREFIX', 'bookstack'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
// REDIS - Split out configuration into an array
|
||||||
|
if (env('REDIS_SERVERS', false)) {
|
||||||
|
$redisServerKeys = ['host', 'port', 'database'];
|
||||||
|
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
|
||||||
|
$redisConfig = [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', false)
|
||||||
|
];
|
||||||
|
foreach ($redisServers as $index => $redisServer) {
|
||||||
|
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
|
||||||
|
$redisServerDetails = explode(':', $redisServer);
|
||||||
|
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
|
||||||
|
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
|
||||||
|
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -123,16 +139,6 @@ return [
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'redis' => [
|
'redis' => $redisConfig,
|
||||||
|
|
||||||
'cluster' => false,
|
|
||||||
|
|
||||||
'default' => [
|
|
||||||
'host' => '127.0.0.1',
|
|
||||||
'port' => 6379,
|
|
||||||
'database' => 0,
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The defaults for the system settings that are saved in the database.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
|
||||||
|
'app-editor' => 'wysiwyg'
|
||||||
|
|
||||||
|
];
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddMarkdownSupport extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->longText('markdown')->default('');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('page_revisions', function (Blueprint $table) {
|
||||||
|
$table->longText('markdown')->default('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('pages', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('markdown');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('page_revisions', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('markdown');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@
|
||||||
"bootstrap-sass": "^3.0.0",
|
"bootstrap-sass": "^3.0.0",
|
||||||
"dropzone": "^4.0.1",
|
"dropzone": "^4.0.1",
|
||||||
"laravel-elixir": "^3.4.0",
|
"laravel-elixir": "^3.4.0",
|
||||||
|
"marked": "^0.3.5",
|
||||||
|
"moment": "^2.12.0",
|
||||||
"zeroclipboard": "^2.2.0"
|
"zeroclipboard": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
146
readme.md
146
readme.md
|
@ -1,149 +1,18 @@
|
||||||
# BookStack
|
# BookStack
|
||||||
|
|
||||||
A platform to create documentation/wiki content. General information about BookStack can be found at https://www.bookstackapp.com/
|
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
|
||||||
|
|
||||||
1. [Requirements](#requirements)
|
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
|
||||||
2. [Installation](#installation)
|
* [Documentation](https://www.bookstackapp.com/docs)
|
||||||
- [Server Rewrite Rules](#url-rewrite-rules)
|
* [Demo Instance](https://demo.bookstackapp.com) *(Login username: `admin@example.com`. Password: `password`)*
|
||||||
3. [Updating](#updating-bookstack)
|
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
||||||
4. [Social Authentication](#social-authentication)
|
|
||||||
- [Google](#google)
|
|
||||||
- [GitHub](#github)
|
|
||||||
5. [LDAP Authentication](#ldap-authentication)
|
|
||||||
6. [Testing](#testing)
|
|
||||||
7. [License](#license)
|
|
||||||
8. [Attribution](#attribution)
|
|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
BookStack has similar requirements to Laravel:
|
|
||||||
|
|
||||||
* PHP >= 5.5.9, Will need to be usable from the command line.
|
|
||||||
* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
|
|
||||||
* MySQL >= 5.6
|
|
||||||
* Git (Not strictly required but helps manage updates)
|
|
||||||
* [Composer](https://getcomposer.org/)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Ensure the above requirements are met before installing. Currently BookStack requires its own domain/subdomain and will not work in a site subdirectory.
|
|
||||||
|
|
||||||
This project currently uses the `release` branch of this repository as a stable channel for providing updates.
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
2. `cd` into the application folder and run `composer install`.
|
|
||||||
3. Copy the `.env.example` file to `.env` and fill with your own database and mail details.
|
|
||||||
4. Ensure the `storage`, `bootstrap/cache` & `public/uploads` 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 if `.htaccess` files are disabled you will have to create some URL rewrite rules as shown below.
|
|
||||||
7. Run `php artisan 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.
|
|
||||||
|
|
||||||
#### URL Rewrite rules
|
|
||||||
|
|
||||||
**Apache**
|
|
||||||
```
|
|
||||||
Options +FollowSymLinks
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteRule ^ index.php [L]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nginx**
|
|
||||||
```
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.php?$query_string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
## Updating BookStack
|
|
||||||
|
|
||||||
To update BookStack you can run the following command in the root directory of the application:
|
|
||||||
```
|
|
||||||
git pull origin release && composer install && php artisan migrate
|
|
||||||
```
|
|
||||||
This command will update the repository that was created in the installation, install the PHP dependencies using `composer` then run the database migrations.
|
|
||||||
|
|
||||||
## Social Authentication
|
|
||||||
|
|
||||||
BookStack currently supports login via both Google and GitHub. Once enabled options for these services will show up in the login, registration and user profile pages. By default these services are disabled. To enable them you will have to create an application on the external services to obtain the require application id's and secrets. Here are instructions to do this for the current supported services:
|
|
||||||
|
|
||||||
### Google
|
|
||||||
|
|
||||||
1. Open the [Google Developers Console](https://console.developers.google.com/).
|
|
||||||
2. Create a new project (May have to wait a short while for it to be created).
|
|
||||||
3. Select 'Enable and manage APIs'.
|
|
||||||
4. Enable the 'Google+ API'.
|
|
||||||
5. In 'Credentials' choose the 'OAuth consent screen' tab and enter a product name ('BookStack' or your custom set name).
|
|
||||||
6. Back in the 'Credentials' tab click 'New credentials' > 'OAuth client ID'.
|
|
||||||
7. Choose an application type of 'Web application' and enter the following urls under 'Authorized redirect URIs', changing `https://example.com` to your own domain where BookStack is hosted:
|
|
||||||
- `https://example.com/login/service/google/callback`
|
|
||||||
- `https://example.com/register/service/google/callback`
|
|
||||||
8. Click 'Create' and your app_id and secret will be displayed. Replace the false value on both the `GOOGLE_APP_ID` & `GOOGLE_APP_SECRET` variables in the '.env' file in the BookStack root directory with your own app_id and secret.
|
|
||||||
9. Set the 'APP_URL' environment variable to be the same domain as you entered in step 7. So, in this example, it will be `https://example.com`.
|
|
||||||
10. All done! Users should now be able to link to their social accounts in their account profile pages and also register/login using their Google accounts.
|
|
||||||
|
|
||||||
### Github
|
|
||||||
|
|
||||||
1. While logged in, open up your [GitHub developer applications](https://github.com/settings/developers).
|
|
||||||
2. Click 'Register new application'.
|
|
||||||
3. Enter an application name ('BookStack' or your custom set name), A link to your app instance under 'Homepage URL' and an 'Authorization callback URL' of the url that your BookStack instance is hosted on then click 'Register application'.
|
|
||||||
4. A 'Client ID' and a 'Client Secret' value will be shown. Add these two values to the to the `GITHUB_APP_ID` and `GITHUB_APP_SECRET` variables, replacing the default false value, in the '.env' file found in the BookStack root folder.
|
|
||||||
5. Set the 'APP_URL' environment variable to be the same domain as you entered in step 3.
|
|
||||||
6. All done! Users should now be able to link to their social accounts in their account profile pages and also register/login using their Github account.
|
|
||||||
|
|
||||||
## LDAP Authentication
|
|
||||||
|
|
||||||
BookStack can be configured to allow LDAP based user login. While LDAP login is enabled you cannot log in with the standard user/password login and new user registration is disabled. BookStack will only use the LDAP server for getting user details and for authentication. Data on the LDAP server is not currently editable through BookStack.
|
|
||||||
|
|
||||||
When a LDAP user logs into BookStack for the first time their BookStack profile will be created and they will be given the default role set under the 'Default user role after registration' option in the application settings.
|
|
||||||
|
|
||||||
To set up LDAP-based authentication add or modify the following variables in your `.env` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
# General auth
|
|
||||||
AUTH_METHOD=ldap
|
|
||||||
|
|
||||||
# The LDAP host, Adding a port is optional
|
|
||||||
LDAP_SERVER=ldap://example.com:389
|
|
||||||
|
|
||||||
# The base DN from where users will be searched within.
|
|
||||||
LDAP_BASE_DN=ou=People,dc=example,dc=com
|
|
||||||
|
|
||||||
# The full DN and password of the user used to search the server
|
|
||||||
# Can both be left as false to bind anonymously
|
|
||||||
LDAP_DN=false
|
|
||||||
LDAP_PASS=false
|
|
||||||
|
|
||||||
# A filter to use when searching for users
|
|
||||||
# The user-provided user-name used to replace any occurrences of '${user}'
|
|
||||||
LDAP_USER_FILTER=(&(uid=${user}))
|
|
||||||
|
|
||||||
# Set the LDAP version to use when connecting to the server.
|
|
||||||
LDAP_VERSION=false
|
|
||||||
```
|
|
||||||
|
|
||||||
You will also need to have the php-ldap extension installed on your system. It's recommended to change your `APP_DEBUG` variable to `true` while setting up LDAP to make any errors visible. Remember to change this back after LDAP is functioning.
|
|
||||||
|
|
||||||
A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user uid changes it can be updated in BookStack by an admin by changing the 'External Authentication ID' field on the user's profile.
|
|
||||||
|
|
||||||
You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID.
|
|
||||||
|
|
||||||
## Development & Testing
|
## Development & Testing
|
||||||
|
|
||||||
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
|
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
|
||||||
|
|
||||||
* [Node.js](https://nodejs.org/en/) **Development Only**
|
* [Node.js](https://nodejs.org/en/)
|
||||||
* [Gulp](http://gulpjs.com/) **Development Only**
|
* [Gulp](http://gulpjs.com/)
|
||||||
|
|
||||||
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
|
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
|
||||||
|
|
||||||
|
@ -176,3 +45,4 @@ These are the great projects used to help build BookStack:
|
||||||
* [Dropzone.js](http://www.dropzonejs.com/)
|
* [Dropzone.js](http://www.dropzonejs.com/)
|
||||||
* [ZeroClipboard](http://zeroclipboard.org/)
|
* [ZeroClipboard](http://zeroclipboard.org/)
|
||||||
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
|
||||||
|
* [Marked](https://github.com/chjj/marked)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var moment = require('moment');
|
||||||
|
|
||||||
module.exports = function (ngApp, events) {
|
module.exports = function (ngApp, events) {
|
||||||
|
|
||||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||||
|
@ -14,20 +16,40 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.imageUpdateSuccess = false;
|
$scope.imageUpdateSuccess = false;
|
||||||
$scope.imageDeleteSuccess = false;
|
$scope.imageDeleteSuccess = false;
|
||||||
$scope.uploadedTo = $attrs.uploadedTo;
|
$scope.uploadedTo = $attrs.uploadedTo;
|
||||||
|
$scope.view = 'all';
|
||||||
|
|
||||||
|
$scope.searching = false;
|
||||||
|
$scope.searchTerm = '';
|
||||||
|
|
||||||
var page = 0;
|
var page = 0;
|
||||||
var previousClickTime = 0;
|
var previousClickTime = 0;
|
||||||
|
var previousClickImage = 0;
|
||||||
var dataLoaded = false;
|
var dataLoaded = false;
|
||||||
var callback = false;
|
var callback = false;
|
||||||
|
|
||||||
|
var preSearchImages = [];
|
||||||
|
var preSearchHasMore = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple returns the appropriate upload url depending on the image type set.
|
* Used by dropzone to get the endpoint to upload to.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
$scope.getUploadUrl = function () {
|
$scope.getUploadUrl = function () {
|
||||||
return '/images/' + $scope.imageType + '/upload';
|
return '/images/' + $scope.imageType + '/upload';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the current search operation.
|
||||||
|
*/
|
||||||
|
function cancelSearch() {
|
||||||
|
$scope.searching = false;
|
||||||
|
$scope.searchTerm = '';
|
||||||
|
$scope.images = preSearchImages;
|
||||||
|
$scope.hasMore = preSearchHasMore;
|
||||||
|
}
|
||||||
|
$scope.cancelSearch = cancelSearch;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs on image upload, Adds an image to local list of images
|
* Runs on image upload, Adds an image to local list of images
|
||||||
* and shows a success message to the user.
|
* and shows a success message to the user.
|
||||||
|
@ -59,7 +81,7 @@ module.exports = function (ngApp, events) {
|
||||||
var currentTime = Date.now();
|
var currentTime = Date.now();
|
||||||
var timeDiff = currentTime - previousClickTime;
|
var timeDiff = currentTime - previousClickTime;
|
||||||
|
|
||||||
if (timeDiff < dblClickTime) {
|
if (timeDiff < dblClickTime && image.id === previousClickImage) {
|
||||||
// If double click
|
// If double click
|
||||||
callbackAndHide(image);
|
callbackAndHide(image);
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,6 +90,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.dependantPages = false;
|
$scope.dependantPages = false;
|
||||||
}
|
}
|
||||||
previousClickTime = currentTime;
|
previousClickTime = currentTime;
|
||||||
|
previousClickImage = image.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,20 +133,69 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.showing = false;
|
$scope.showing = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var baseUrl = '/images/' + $scope.imageType + '/all/'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the list image data from the server.
|
* Fetch the list image data from the server.
|
||||||
*/
|
*/
|
||||||
function fetchData() {
|
function fetchData() {
|
||||||
var url = '/images/' + $scope.imageType + '/all/' + page;
|
var url = baseUrl + page + '?';
|
||||||
|
var components = {};
|
||||||
|
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
|
||||||
|
if ($scope.searching) components['term'] = $scope.searchTerm;
|
||||||
|
|
||||||
|
|
||||||
|
var urlQueryString = Object.keys(components).map((key) => {
|
||||||
|
return key + '=' + encodeURIComponent(components[key]);
|
||||||
|
}).join('&');
|
||||||
|
url += urlQueryString;
|
||||||
|
|
||||||
$http.get(url).then((response) => {
|
$http.get(url).then((response) => {
|
||||||
$scope.images = $scope.images.concat(response.data.images);
|
$scope.images = $scope.images.concat(response.data.images);
|
||||||
$scope.hasMore = response.data.hasMore;
|
$scope.hasMore = response.data.hasMore;
|
||||||
page++;
|
page++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.fetchData = fetchData;
|
$scope.fetchData = fetchData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a search operation
|
||||||
|
* @param searchTerm
|
||||||
|
*/
|
||||||
|
$scope.searchImages = function() {
|
||||||
|
|
||||||
|
if ($scope.searchTerm === '') {
|
||||||
|
cancelSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.searching) {
|
||||||
|
preSearchImages = $scope.images;
|
||||||
|
preSearchHasMore = $scope.hasMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.searching = true;
|
||||||
|
$scope.images = [];
|
||||||
|
$scope.hasMore = false;
|
||||||
|
page = 0;
|
||||||
|
baseUrl = '/images/' + $scope.imageType + '/search/';
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current image listing view.
|
||||||
|
* @param viewName
|
||||||
|
*/
|
||||||
|
$scope.setView = function(viewName) {
|
||||||
|
cancelSearch();
|
||||||
|
$scope.images = [];
|
||||||
|
$scope.hasMore = false;
|
||||||
|
page = 0;
|
||||||
|
$scope.view = viewName;
|
||||||
|
baseUrl = '/images/' + $scope.imageType + '/' + viewName + '/';
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the details of an image.
|
* Save the details of an image.
|
||||||
* @param event
|
* @param event
|
||||||
|
@ -216,16 +288,20 @@ module.exports = function (ngApp, events) {
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', function ($scope, $http, $attrs, $interval, $timeout) {
|
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
|
||||||
|
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
|
||||||
|
|
||||||
$scope.editorOptions = require('./pages/page-form');
|
$scope.editorOptions = require('./pages/page-form');
|
||||||
$scope.editorHtml = '';
|
$scope.editContent = '';
|
||||||
$scope.draftText = '';
|
$scope.draftText = '';
|
||||||
var pageId = Number($attrs.pageId);
|
var pageId = Number($attrs.pageId);
|
||||||
var isEdit = pageId !== 0;
|
var isEdit = pageId !== 0;
|
||||||
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
var autosaveFrequency = 30; // AutoSave interval in seconds.
|
||||||
|
var isMarkdown = $attrs.editorType === 'markdown';
|
||||||
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
|
||||||
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
|
||||||
|
|
||||||
|
// Set inital header draft text
|
||||||
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
|
||||||
$scope.draftText = 'Editing Draft'
|
$scope.draftText = 'Editing Draft'
|
||||||
} else {
|
} else {
|
||||||
|
@ -245,7 +321,18 @@ module.exports = function (ngApp, events) {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.editorChange = function () {}
|
// Actions specifically for the markdown editor
|
||||||
|
if (isMarkdown) {
|
||||||
|
$scope.displayContent = '';
|
||||||
|
// Editor change event
|
||||||
|
$scope.editorChange = function (content) {
|
||||||
|
$scope.displayContent = $sce.trustAsHtml(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMarkdown) {
|
||||||
|
$scope.editorChange = function() {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the AutoSave loop, Checks for content change
|
* Start the AutoSave loop, Checks for content change
|
||||||
|
@ -253,17 +340,18 @@ module.exports = function (ngApp, events) {
|
||||||
*/
|
*/
|
||||||
function startAutoSave() {
|
function startAutoSave() {
|
||||||
currentContent.title = $('#name').val();
|
currentContent.title = $('#name').val();
|
||||||
currentContent.html = $scope.editorHtml;
|
currentContent.html = $scope.editContent;
|
||||||
|
|
||||||
autoSave = $interval(() => {
|
autoSave = $interval(() => {
|
||||||
var newTitle = $('#name').val();
|
var newTitle = $('#name').val();
|
||||||
var newHtml = $scope.editorHtml;
|
var newHtml = $scope.editContent;
|
||||||
|
|
||||||
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
|
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
|
||||||
currentContent.html = newHtml;
|
currentContent.html = newHtml;
|
||||||
currentContent.title = newTitle;
|
currentContent.title = newTitle;
|
||||||
saveDraft(newTitle, newHtml);
|
saveDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 1000 * autosaveFrequency);
|
}, 1000 * autosaveFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,20 +360,23 @@ module.exports = function (ngApp, events) {
|
||||||
* @param title
|
* @param title
|
||||||
* @param html
|
* @param html
|
||||||
*/
|
*/
|
||||||
function saveDraft(title, html) {
|
function saveDraft() {
|
||||||
$http.put('/ajax/page/' + pageId + '/save-draft', {
|
var data = {
|
||||||
name: title,
|
name: $('#name').val(),
|
||||||
html: html
|
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
|
||||||
}).then((responseData) => {
|
};
|
||||||
$scope.draftText = responseData.data.message;
|
|
||||||
|
if (isMarkdown) data.markdown = $scope.editContent;
|
||||||
|
|
||||||
|
$http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => {
|
||||||
|
var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
|
||||||
|
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
|
||||||
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.forceDraftSave = function() {
|
$scope.forceDraftSave = function() {
|
||||||
var newTitle = $('#name').val();
|
saveDraft();
|
||||||
var newHtml = $scope.editorHtml;
|
|
||||||
saveDraft(newTitle, newHtml);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -298,6 +389,7 @@ module.exports = function (ngApp, events) {
|
||||||
$scope.draftText = 'Editing Page';
|
$scope.draftText = 'Editing Page';
|
||||||
$scope.isUpdateDraft = false;
|
$scope.isUpdateDraft = false;
|
||||||
$scope.$broadcast('html-update', responseData.data.html);
|
$scope.$broadcast('html-update', responseData.data.html);
|
||||||
|
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
|
||||||
$('#name').val(responseData.data.name);
|
$('#name').val(responseData.data.name);
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
startAutoSave();
|
startAutoSave();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
var DropZone = require('dropzone');
|
var DropZone = require('dropzone');
|
||||||
|
var markdown = require('marked');
|
||||||
|
|
||||||
var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
||||||
var imagePickerTemplate = require('./components/image-picker.html');
|
var imagePickerTemplate = require('./components/image-picker.html');
|
||||||
|
@ -200,7 +201,82 @@ module.exports = function (ngApp, events) {
|
||||||
tinymce.init(scope.tinymce);
|
tinymce.init(scope.tinymce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
ngApp.directive('markdownInput', ['$timeout', function($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
scope: {
|
||||||
|
mdModel: '=',
|
||||||
|
mdChange: '='
|
||||||
|
},
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
|
||||||
|
// Set initial model content
|
||||||
|
var content = element.val();
|
||||||
|
scope.mdModel = content;
|
||||||
|
scope.mdChange(markdown(content));
|
||||||
|
|
||||||
|
element.on('change input', (e) => {
|
||||||
|
content = element.val();
|
||||||
|
$timeout(() => {
|
||||||
|
scope.mdModel = content;
|
||||||
|
scope.mdChange(markdown(content));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.$on('markdown-update', (event, value) => {
|
||||||
|
element.val(value);
|
||||||
|
scope.mdModel= value;
|
||||||
|
scope.mdChange(markdown(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
|
||||||
|
ngApp.directive('markdownEditor', ['$timeout', function($timeout) {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
link: function (scope, element, attrs) {
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
var input = element.find('textarea[markdown-input]');
|
||||||
|
var insertImage = element.find('button[data-action="insertImage"]');
|
||||||
|
|
||||||
|
var currentCaretPos = 0;
|
||||||
|
|
||||||
|
input.blur((event) => {
|
||||||
|
currentCaretPos = input[0].selectionStart;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert image shortcut
|
||||||
|
input.keydown((event) => {
|
||||||
|
if (event.which === 73 && event.ctrlKey && event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
var caretPos = input[0].selectionStart;
|
||||||
|
var currentContent = input.val();
|
||||||
|
var mdImageText = "";
|
||||||
|
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
||||||
|
input.focus();
|
||||||
|
input[0].selectionStart = caretPos + (";
|
||||||
|
input[0].selectionEnd = caretPos + (';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert image from image manager
|
||||||
|
insertImage.click((event) => {
|
||||||
|
window.ImageManager.showExternal((image) => {
|
||||||
|
var caretPos = currentCaretPos;
|
||||||
|
var currentContent = input.val();
|
||||||
|
var mdImageText = "";
|
||||||
|
input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
|
||||||
|
input.change();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
|
@ -94,3 +94,14 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* roboto-mono-regular - latin */
|
||||||
|
// https://google-webfonts-helper.herokuapp.com
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Mono'), local('RobotoMono-Regular'),
|
||||||
|
url('/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */
|
||||||
|
url('/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
|
}
|
|
@ -26,6 +26,59 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#markdown-editor {
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
textarea {
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: $-xs $-m;
|
||||||
|
color: #444;
|
||||||
|
border-radius: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
width: 100%;
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.markdown-display, .markdown-editor-wrap {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.markdown-editor-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
}
|
||||||
|
.markdown-display {
|
||||||
|
padding: 0 $-m 0;
|
||||||
|
margin-left: -1px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
.page-content {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.editor-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
padding: $-xs $-m;
|
||||||
|
font-family: 'Roboto Mono';
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
border-bottom: 1px solid #DDD;
|
||||||
|
background-color: #EEE;
|
||||||
|
flex: none;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
|
@ -160,6 +213,10 @@ input:checked + .toggle-switch {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.search-box {
|
.search-box {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -189,12 +189,13 @@ form.search-box {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-nav {
|
.nav-tabs {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a, .tab-item {
|
||||||
padding: $-m;
|
padding: $-m;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
&.selected {
|
&.selected {
|
||||||
border-bottom: 2px solid $primary;
|
border-bottom: 2px solid $primary;
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,6 @@
|
||||||
.image-manager-list {
|
.image-manager-list {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-manager-content {
|
.image-manager-content {
|
||||||
|
@ -128,6 +127,12 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.full-tab {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropzone
|
// Dropzone
|
||||||
|
|
|
@ -25,3 +25,12 @@ table {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.list-table {
|
||||||
|
margin: 0 -$-xs;
|
||||||
|
td {
|
||||||
|
border: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: $-xs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -157,6 +157,12 @@ span.code {
|
||||||
@extend .code-base;
|
@extend .code-base;
|
||||||
padding: 1px $-xs;
|
padding: 1px $-xs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* Text colors
|
* Text colors
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -177,3 +177,28 @@ $btt-size: 40px;
|
||||||
top: -5px;
|
top: -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contained-search-box {
|
||||||
|
display: flex;
|
||||||
|
input, button {
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
flex: 5;
|
||||||
|
&:focus, &:active {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
button i {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
button.cancel.active {
|
||||||
|
background-color: $negative;
|
||||||
|
color: #EEE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<input value="true" id="{{$name}}[{{$role->id}}][{{$action}}]" type="checkbox" name="{{$name}}[{{$role->id}}][{{$action}}]"
|
<input value="true" id="{{$name}}[{{$role->id}}][{{$action}}]" type="checkbox" name="{{$name}}[{{$role->id}}][{{$action}}]"
|
||||||
@if(old($name .'.'.$role->id.'.'.$action) || (!old() && isset($model) && $model->hasRestriction($role->id, $action))) checked="checked" @endif
|
@if(isset($model) && $model->hasRestriction($role->id, $action)) checked="checked" @endif>
|
||||||
>
|
|
||||||
{{ $label }}
|
{{ $label }}
|
||||||
</label>
|
</label>
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
<div class="page-editor flex-fill flex" ng-controller="PageEditController" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
|
<div class="page-editor flex-fill flex" ng-controller="PageEditController" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
|
||||||
|
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
|
@ -42,10 +42,45 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-area flex-fill flex">
|
<div class="edit-area flex-fill flex">
|
||||||
<textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editorHtml" name="html" rows="5"
|
@if(setting('app-editor') === 'wysiwyg')
|
||||||
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
<textarea id="html-editor" tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" name="html" rows="5"
|
||||||
@if($errors->has('html'))
|
@if($errors->has('html')) class="neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
|
||||||
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
@if($errors->has('html'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('html') }}</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(setting('app-editor') === 'markdown')
|
||||||
|
<div id="markdown-editor" markdown-editor class="flex-fill flex">
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span class="float left">Editor</span>
|
||||||
|
<div class="float right buttons">
|
||||||
|
<button class="text-button" type="button" data-action="insertImage"><i class="zmdi zmdi-image"></i>Insert Image</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea markdown-input md-change="editorChange" md-model="editContent" name="markdown" rows="5"
|
||||||
|
@if($errors->has('markdown')) class="neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="markdown-editor-wrap">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<div class="">Preview</div>
|
||||||
|
</div>
|
||||||
|
<div class="markdown-display">
|
||||||
|
<div class="page-content" ng-bind-html="displayContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="html" ng-value="displayContent">
|
||||||
|
|
||||||
|
@if($errors->has('markdown'))
|
||||||
|
<div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -12,7 +12,7 @@
|
||||||
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
|
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover, .button:focus {
|
||||||
background-color: {{ Setting::get('app-color') }};
|
background-color: {{ Setting::get('app-color') }};
|
||||||
}
|
}
|
||||||
.setting-nav a.selected {
|
.nav-tabs a.selected, .nav-tabs .tab-item.selected {
|
||||||
border-bottom-color: {{ Setting::get('app-color') }};
|
border-bottom-color: {{ Setting::get('app-color') }};
|
||||||
}
|
}
|
||||||
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
|
p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus {
|
||||||
|
|
|
@ -3,6 +3,20 @@
|
||||||
<div class="image-manager-body" ng-click="$event.stopPropagation()">
|
<div class="image-manager-body" ng-click="$event.stopPropagation()">
|
||||||
|
|
||||||
<div class="image-manager-content">
|
<div class="image-manager-content">
|
||||||
|
<div ng-if="imageType === 'gallery'" class="container">
|
||||||
|
<div class="image-manager-header row faded-small nav-tabs">
|
||||||
|
<div class="col-xs-4 tab-item" title="View all images" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> All</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this book" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> Book</div>
|
||||||
|
<div class="col-xs-4 tab-item" title="View images uploaded to this page" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> Page</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-show="view === 'all'" >
|
||||||
|
<form ng-submit="searchImages()" class="contained-search-box">
|
||||||
|
<input type="text" placeholder="Search by image name" ng-model="searchTerm">
|
||||||
|
<button ng-class="{active: searching}" title="Clear Search" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
|
||||||
|
<button title="Search" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div class="image-manager-list">
|
<div class="image-manager-list">
|
||||||
<div ng-repeat="image in images">
|
<div ng-repeat="image in images">
|
||||||
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
||||||
|
|
|
@ -17,29 +17,37 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-app-name">Application name</label>
|
<label for="setting-app-name">Application name</label>
|
||||||
<input type="text" value="{{ Setting::get('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
|
<input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Allow public viewing?</label>
|
<label>Allow public viewing?</label>
|
||||||
<toggle-switch name="setting-app-public" value="{{ Setting::get('app-public') }}"></toggle-switch>
|
<toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Enable higher security image uploads?</label>
|
<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>
|
<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>
|
<toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></toggle-switch>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="setting-app-editor">Page editor</label>
|
||||||
|
<p class="small">Select which editor will be used by all users to edit pages.</p>
|
||||||
|
<select name="setting-app-editor" id="setting-app-editor">
|
||||||
|
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
|
||||||
|
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group" id="logo-control">
|
<div class="form-group" id="logo-control">
|
||||||
<label for="setting-app-logo">Application Logo</label>
|
<label for="setting-app-logo">Application logo</label>
|
||||||
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
|
<p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
|
||||||
<image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ Setting::get('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
|
<image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="/logo.png" name="setting-app-logo" image-class="logo-image"></image-picker>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="color-control">
|
<div class="form-group" id="color-control">
|
||||||
<label for="setting-app-color">Application Primary Color</label>
|
<label for="setting-app-color">Application primary color</label>
|
||||||
<p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
|
<p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
|
||||||
<input type="text" value="{{ Setting::get('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
|
<input type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
|
||||||
<input type="hidden" value="{{ Setting::get('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)">
|
<input type="hidden" value="{{ setting('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,14 +61,14 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-registration-enabled">Allow registration?</label>
|
<label for="setting-registration-enabled">Allow registration?</label>
|
||||||
<toggle-switch name="setting-registration-enabled" value="{{ Setting::get('registration-enabled') }}"></toggle-switch>
|
<toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setting-registration-role">Default user role after registration</label>
|
<label for="setting-registration-role">Default user role after registration</label>
|
||||||
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
||||||
@foreach(\BookStack\Role::all() as $role)
|
@foreach(\BookStack\Role::all() as $role)
|
||||||
<option value="{{$role->id}}"
|
<option value="{{$role->id}}"
|
||||||
@if(\Setting::get('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
|
@if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
|
||||||
>
|
>
|
||||||
{{ $role->display_name }}
|
{{ $role->display_name }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -70,7 +78,7 @@
|
||||||
<div class="form-group">
|
<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>
|
<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>
|
<toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></toggle-switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -78,7 +86,7 @@
|
||||||
<label for="setting-registration-restrict">Restrict registration to domain</label>
|
<label for="setting-registration-restrict">Restrict registration to domain</label>
|
||||||
<p class="small">Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
|
<p class="small">Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
|
||||||
<br> Note that users will be able to change their email addresses after successful registration.</p>
|
<br> Note that users will be able to change their email addresses after successful registration.</p>
|
||||||
<input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ Setting::get('registration-restrict', '') }}">
|
<input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ setting('registration-restrict', '') }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="faded-small toolbar">
|
<div class="faded-small toolbar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12 setting-nav">
|
<div class="col-md-12 setting-nav nav-tabs">
|
||||||
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
|
<a href="/settings" @if($selected == 'settings') class="selected text-button" @endif><i class="zmdi zmdi-settings"></i>Settings</a>
|
||||||
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
<a href="/settings/users" @if($selected == 'users') class="selected text-button" @endif><i class="zmdi zmdi-accounts"></i>Users</a>
|
||||||
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
|
<a href="/settings/roles" @if($selected == 'roles') class="selected text-button" @endif><i class="zmdi zmdi-lock-open"></i>Roles</a>
|
||||||
|
|
|
@ -2,116 +2,130 @@
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-9">
|
||||||
<h3>Role Details</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Role Name</label>
|
|
||||||
@include('form/text', ['name' => 'display_name'])
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Short Role Description</label>
|
|
||||||
@include('form/text', ['name' => 'description'])
|
|
||||||
</div>
|
|
||||||
<h3>System Permissions</h3>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-5">
|
||||||
<label> @include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
|
<h3>Role Details</h3>
|
||||||
</div>
|
<div class="form-group">
|
||||||
<div class="col-md-6">
|
<label for="name">Role Name</label>
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage user roles</label>
|
@include('form/text', ['name' => 'display_name'])
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group">
|
||||||
<hr class="even">
|
<label for="name">Short Role Description</label>
|
||||||
<div class="row">
|
@include('form/text', ['name' => 'description'])
|
||||||
<div class="col-md-6">
|
</div>
|
||||||
|
<h3>System Permissions</h3>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) Manage users</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) Manage roles & role permissions</label>
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
|
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) Manage all Book, Chapter & Page permissions</label>
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage permissions on own Book, Chapter & Pages</label>
|
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) Manage permissions on own Book, Chapter & Pages</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
|
||||||
|
<h3>Asset Permissions</h3>
|
||||||
|
<p>
|
||||||
|
These permissions control default access to the assets within the system.
|
||||||
|
Permissions on Books, Chapters and Pages will override these permissions.
|
||||||
|
</p>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Create</th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Books</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Chapters</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Pages</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Images</td>
|
||||||
|
<td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
|
||||||
|
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="even">
|
<a href="/settings/roles" class="button muted">Cancel</a>
|
||||||
<div class="form-group">
|
<button type="submit" class="button pos">Save Role</button>
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) Manage app settings</label>
|
|
||||||
</div>
|
|
||||||
<hr class="even">
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<h3>Users in this role</h3>
|
||||||
|
|
||||||
<div class="col-md-6">
|
@if(isset($role) && count($role->users) > 0)
|
||||||
|
<table class="list-table">
|
||||||
<h3>Asset Permissions</h3>
|
@foreach($role->users as $user)
|
||||||
<p>
|
<tr>
|
||||||
These permissions control default access to the assets within the system. <br>
|
<td style="line-height: 0;"><img class="avatar small" src="{{$user->getAvatar(40)}}" alt="{{$user->name}}"></td>
|
||||||
Permissions on Books, Chapters and Pages will override these permissions.
|
<td>
|
||||||
</p>
|
@if(userCan('users-manage') || $currentUser->id == $user->id)
|
||||||
<table class="table">
|
<a href="/settings/users/{{$user->id}}">
|
||||||
<tr>
|
@endif
|
||||||
<th></th>
|
{{ $user->name }}
|
||||||
<th>Create</th>
|
@if(userCan('users-manage') || $currentUser->id == $user->id)
|
||||||
<th>Edit</th>
|
</a>
|
||||||
<th>Delete</th>
|
@endif
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Books</td>
|
@endforeach
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'book-create-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'book-update-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'book-delete-all']) All</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Chapters</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-create-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-update-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'chapter-delete-all']) All</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Pages</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-create-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-update-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'page-delete-all']) All</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Images</td>
|
|
||||||
<td>@include('settings/roles/checkbox', ['permission' => 'image-create-all'])</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'image-update-all']) All</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-own']) Own</label>
|
|
||||||
<label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
@else
|
||||||
|
<p class="text-muted">
|
||||||
|
No users currently in this role.
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/settings/roles" class="button muted">Cancel</a>
|
|
||||||
<button type="submit" class="button pos">Save Role</button>
|
|
|
@ -43,7 +43,7 @@ class LdapTest extends \TestCase
|
||||||
->press('Sign In')
|
->press('Sign In')
|
||||||
->seePageIs('/')
|
->seePageIs('/')
|
||||||
->see($this->mockUser->name)
|
->see($this->mockUser->name)
|
||||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
|
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
||||||
|
@ -67,7 +67,7 @@ class LdapTest extends \TestCase
|
||||||
->press('Sign In')
|
->press('Sign In')
|
||||||
->seePageIs('/')
|
->seePageIs('/')
|
||||||
->see($this->mockUser->name)
|
->see($this->mockUser->name)
|
||||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
|
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_initial_incorrect_details()
|
public function test_initial_incorrect_details()
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownTest extends TestCase
|
||||||
|
{
|
||||||
|
protected $page;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->page = \BookStack\Page::first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setMarkdownEditor()
|
||||||
|
{
|
||||||
|
$this->setSettings(['app-editor' => 'markdown']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_default_editor_is_wysiwyg()
|
||||||
|
{
|
||||||
|
$this->assertEquals(setting('app-editor'), 'wysiwyg');
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->pageHasElement('#html-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_markdown_setting_shows_markdown_editor()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->pageNotHasElement('#html-editor')
|
||||||
|
->pageHasElement('#markdown-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_markdown_content_given_to_editor()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$mdContent = '# hello. This is a test';
|
||||||
|
$this->page->markdown = $mdContent;
|
||||||
|
$this->page->save();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->seeInField('markdown', $mdContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_html_content_given_to_editor_if_no_markdown()
|
||||||
|
{
|
||||||
|
$this->setMarkdownEditor();
|
||||||
|
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
|
||||||
|
->seeInField('markdown', $this->page->html);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -171,11 +171,27 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function actingAsUsers($usersArray, $callback)
|
/**
|
||||||
|
* Check if the page contains the given element.
|
||||||
|
* @param string $selector
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function pageHasElement($selector)
|
||||||
{
|
{
|
||||||
foreach ($usersArray as $user) {
|
$elements = $this->crawler->filter($selector);
|
||||||
$this->actingAs($user);
|
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
|
||||||
$callback($user);
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the page contains the given element.
|
||||||
|
* @param string $selector
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function pageNotHasElement($selector)
|
||||||
|
{
|
||||||
|
$elements = $this->crawler->filter($selector);
|
||||||
|
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue