mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-12 03:09:38 +03:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
e0279f93f9 | ||
|
|
9b83c57316 | ||
|
|
5d73d17c74 | ||
|
|
d32460070f | ||
|
|
105500e506 | ||
|
|
8296782149 | ||
|
|
8e8d582bc6 | ||
|
|
8dec674cc3 | ||
|
|
e87db96fc0 | ||
|
|
f784c03746 | ||
|
|
4bb7f0613f | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
080acf0a62 | ||
|
|
ea2e16cabb | ||
|
|
7bcd967fd9 | ||
|
|
bb87401d10 | ||
|
|
0821672e70 | ||
|
|
14feef3679 | ||
|
|
1c8c9e65c5 | ||
|
|
14ca31768c | ||
|
|
e27a630a09 | ||
|
|
9319f99a3d | ||
|
|
d6739c1158 | ||
|
|
7178c66cf5 |
19
.env.example
19
.env.example
@@ -7,7 +7,7 @@ APP_KEY=SomeRandomString
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=database_database
|
||||
DB_USERNAME=database_username
|
||||
DB_PASSWORD=database__user_password
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Cache and session
|
||||
CACHE_DRIVER=file
|
||||
@@ -25,6 +25,9 @@ STORAGE_S3_BUCKET=false
|
||||
# Used to prefix image urls for when using custom domains/cdns
|
||||
STORAGE_URL=false
|
||||
|
||||
# General auth
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Social Authentication information. Defaults as off.
|
||||
GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
@@ -33,8 +36,16 @@ GOOGLE_APP_SECRET=false
|
||||
# URL used for social login redirects, NO TRAILING SLASH
|
||||
APP_URL=http://bookstack.dev
|
||||
|
||||
# External services
|
||||
USE_GRAVATAR=true
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# LDAP Settings
|
||||
LDAP_SERVER=false
|
||||
LDAP_BASE_DN=false
|
||||
LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
|
||||
# Mail settings
|
||||
MAIL_DRIVER=smtp
|
||||
@@ -42,4 +53,4 @@ MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,7 +7,6 @@ Homestead.yaml
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/uploads
|
||||
/public/bower
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class ConfirmationEmailException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class ConfirmationEmailException extends NotifyException {}
|
||||
@@ -3,8 +3,12 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use PhpSpec\Exception\Example\ErrorException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -14,7 +18,10 @@ class Handler extends ExceptionHandler
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
AuthorizationException::class,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
ValidationException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -32,17 +39,26 @@ class Handler extends ExceptionHandler
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
{
|
||||
if($e instanceof NotifyException) {
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($e instanceof NotifyException) {
|
||||
\Session::flash('error', $e->message);
|
||||
return response()->redirectTo($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
|
||||
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
|
||||
return response()->view('errors/500', ['message' => $message], 500);
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
}
|
||||
|
||||
3
app/Exceptions/ImageUploadException.php
Normal file
3
app/Exceptions/ImageUploadException.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class ImageUploadException extends PrettyException {}
|
||||
3
app/Exceptions/LdapException.php
Normal file
3
app/Exceptions/LdapException.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class LdapException extends PrettyException {}
|
||||
5
app/Exceptions/PrettyException.php
Normal file
5
app/Exceptions/PrettyException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PrettyException extends Exception {}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialDriverNotConfigured extends \Exception
|
||||
{
|
||||
}
|
||||
class SocialDriverNotConfigured extends PrettyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class SocialSignInException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class SocialSignInException extends NotifyException {}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class UserRegistrationException extends NotifyException
|
||||
{
|
||||
|
||||
}
|
||||
class UserRegistrationException extends NotifyException {}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
@@ -29,9 +30,10 @@ class AuthController extends Controller
|
||||
|
||||
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
|
||||
|
||||
protected $loginPath = '/login';
|
||||
protected $redirectPath = '/';
|
||||
protected $redirectAfterLogout = '/login';
|
||||
protected $username = 'email';
|
||||
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $emailConfirmationService;
|
||||
@@ -49,6 +51,7 @@ class AuthController extends Controller
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -105,6 +108,38 @@ class AuthController extends Controller
|
||||
return $this->registerUser($userData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overrides the action when a user is authenticated.
|
||||
* If the user authenticated but does not exist in the user table we create them.
|
||||
* @param Request $request
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
$user->save();
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
return redirect()->intended($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
@@ -156,13 +191,14 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
$newUser->email_confirmed = true;
|
||||
|
||||
auth()->login($newUser);
|
||||
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
|
||||
return redirect($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page to tell the user to check thier email
|
||||
* Show the page to tell the user to check their email
|
||||
* and confirm their address.
|
||||
*/
|
||||
public function getRegisterConfirmation()
|
||||
@@ -222,7 +258,7 @@ class AuthController extends Controller
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
\Session::flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
session()->flash('success', 'Confirmation email resent, Please check your inbox.');
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
@@ -232,13 +268,9 @@ class AuthController extends Controller
|
||||
*/
|
||||
public function getLogin()
|
||||
{
|
||||
|
||||
if (view()->exists('auth.authenticate')) {
|
||||
return view('auth.authenticate');
|
||||
}
|
||||
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
return view('auth.login', ['socialDrivers' => $socialDrivers]);
|
||||
$authMethod = config('auth.method');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,7 +285,7 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the social site for authentication initended to register.
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
* @return mixed
|
||||
*/
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function preventAccessForDemoUsers()
|
||||
{
|
||||
if (env('APP_ENV', 'production') === 'demo') $this->showPermissionError();
|
||||
if (config('app.env') === 'demo') $this->showPermissionError();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -69,7 +70,13 @@ class ImageController extends Controller
|
||||
]);
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type);
|
||||
|
||||
try {
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Services\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -18,18 +19,21 @@ class PageController extends Controller
|
||||
protected $pageRepo;
|
||||
protected $bookRepo;
|
||||
protected $chapterRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param PageRepo $pageRepo
|
||||
* @param BookRepo $bookRepo
|
||||
* @param ChapterRepo $chapterRepo
|
||||
* @param ExportService $exportService
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
|
||||
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -221,4 +225,57 @@ class PageController extends Controller
|
||||
Activity::add($page, 'page_restore', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
|
||||
* https://github.com/barryvdh/laravel-dompdf
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a self-contained HTML file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a page to a simple plaintext .txt file.
|
||||
* @param $bookSlug
|
||||
* @param $pageSlug
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $pageSlug)
|
||||
{
|
||||
$book = $this->bookRepo->getBySlug($bookSlug);
|
||||
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ class UserController extends Controller
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
return view('users/create');
|
||||
$authMethod = config('auth.method');
|
||||
return view('users/create', ['authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,22 +58,35 @@ class UserController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-create');
|
||||
$this->validate($request, [
|
||||
$validationRules = [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email',
|
||||
'password' => 'required|min:5',
|
||||
'password-confirm' => 'required|same:password',
|
||||
'role' => 'required|exists:roles,id'
|
||||
]);
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
if ($authMethod === 'standard') {
|
||||
$validationRules['password'] = 'required|min:5';
|
||||
$validationRules['password-confirm'] = 'required|same:password';
|
||||
} elseif ($authMethod === 'ldap') {
|
||||
$validationRules['external_auth_id'] = 'required';
|
||||
}
|
||||
$this->validate($request, $validationRules);
|
||||
|
||||
|
||||
$user = $this->user->fill($request->all());
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
$user->save();
|
||||
|
||||
if ($authMethod === 'standard') {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} elseif ($authMethod === 'ldap') {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
$user->attachRoleId($request->get('role'));
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!env('DISABLE_EXTERNAL_SERVICES', false)) {
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
@@ -94,10 +108,12 @@ class UserController extends Controller
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||
$this->setPageTitle('User Profile');
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]);
|
||||
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,8 +130,8 @@ class UserController extends Controller
|
||||
});
|
||||
|
||||
$this->validate($request, [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email,' . $id,
|
||||
'name' => 'min:2',
|
||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||
'password' => 'min:5|required_with:password_confirm',
|
||||
'password-confirm' => 'same:password|required_with:password',
|
||||
'role' => 'exists:roles,id'
|
||||
@@ -124,17 +140,24 @@ class UserController extends Controller
|
||||
]);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user->fill($request->except('password'));
|
||||
$user->fill($request->all());
|
||||
|
||||
// Role updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('role')) {
|
||||
$user->attachRoleId($request->get('role'));
|
||||
}
|
||||
|
||||
// Password updates
|
||||
if ($request->has('password') && $request->get('password') != '') {
|
||||
$password = $request->get('password');
|
||||
$user->password = bcrypt($password);
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return redirect('/users');
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class Authenticate
|
||||
if(auth()->check() && auth()->user()->email_confirmed == false) {
|
||||
return redirect()->guest('/register/confirm/awaiting');
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !Setting::get('app-public')) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
|
||||
@@ -18,17 +18,19 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
Route::get('/{bookSlug}/sort', 'BookController@sort');
|
||||
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
|
||||
|
||||
|
||||
// Pages
|
||||
Route::get('/{bookSlug}/page/create', 'PageController@create');
|
||||
Route::post('/{bookSlug}/page', 'PageController@store');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
|
||||
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
|
||||
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
|
||||
|
||||
//Revisions
|
||||
// Revisions
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
|
||||
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
|
||||
@@ -45,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () {
|
||||
|
||||
});
|
||||
|
||||
|
||||
// Users
|
||||
Route::get('/users', 'UserController@index');
|
||||
Route::get('/users/create', 'UserController@create');
|
||||
|
||||
31
app/Providers/AuthServiceProvider.php
Normal file
31
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
|
||||
});
|
||||
}
|
||||
}
|
||||
133
app/Providers/LdapUserProvider.php
Normal file
133
app/Providers/LdapUserProvider.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
||||
class LdapUserProvider implements UserProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* The user model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var LdapService
|
||||
*/
|
||||
protected $ldapService;
|
||||
|
||||
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
* @param $model
|
||||
* @param LdapService $ldapService
|
||||
*/
|
||||
public function __construct($model, LdapService $ldapService)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->ldapService = $ldapService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function createModel()
|
||||
{
|
||||
$class = '\\' . ltrim($this->model, '\\');
|
||||
return new $class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
{
|
||||
return $this->createModel()->newQuery()->find($identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier and "remember me" token.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @param string $token
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
{
|
||||
$model = $this->createModel();
|
||||
|
||||
return $model->newQuery()
|
||||
->where($model->getAuthIdentifierName(), $identifier)
|
||||
->where($model->getRememberTokenName(), $token)
|
||||
->first();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the "remember me" token for the given user in storage.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
{
|
||||
if ($user->exists) {
|
||||
$user->setRememberToken($token);
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
// Get user via LDAP
|
||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||
if ($userDetails === null) return null;
|
||||
|
||||
// Search current user base by looking up a uid
|
||||
$model = $this->createModel();
|
||||
$currentUser = $model->newQuery()
|
||||
->where('external_auth_id', $userDetails['uid'])
|
||||
->first();
|
||||
|
||||
if ($currentUser !== null) return $currentUser;
|
||||
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
$model->email = $userDetails['email'];
|
||||
$model->email_confirmed = true;
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a user against the given credentials.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
{
|
||||
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Setting;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
@@ -47,6 +48,14 @@ class UserRepo
|
||||
{
|
||||
$user = $this->create($data);
|
||||
$this->attachDefaultRole($user);
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -56,7 +65,7 @@ class UserRepo
|
||||
*/
|
||||
public function attachDefaultRole($user)
|
||||
{
|
||||
$roleId = \Setting::get('registration-role');
|
||||
$roleId = Setting::get('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->getDefault()->id;
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
@@ -87,7 +96,7 @@ class UserRepo
|
||||
*/
|
||||
public function create(array $data)
|
||||
{
|
||||
return $this->user->create([
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password'])
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class Role extends Model
|
||||
{
|
||||
/**
|
||||
* Sets the default role name for newly registed users.
|
||||
* Sets the default role name for newly registered users.
|
||||
* @var string
|
||||
*/
|
||||
protected static $default = 'viewer';
|
||||
|
||||
115
app/Services/ExportService.php
Normal file
115
app/Services/ExportService.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Page;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a pdf file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
|
||||
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
|
||||
$containedHtml = $this->containHtml($pageHtml);
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
return $pdf->output();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle of the contents of a html file to be self-contained.
|
||||
* @param $htmlContent
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function containHtml($htmlContent)
|
||||
{
|
||||
$imageTagsOutput = [];
|
||||
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$pathString = public_path($srcString);
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
$linksOutput = [];
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||
$oldLinkString = $linkMatch;
|
||||
$srcString = $linksOutput[2][$index];
|
||||
if (strpos(trim($srcString), 'http') !== 0) {
|
||||
$newSrcString = url($srcString);
|
||||
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
|
||||
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with system domain
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the page contents into simple plain text.
|
||||
* This method filters any bad looking content to
|
||||
* provide a nice final output.
|
||||
* @param Page $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$text = $page->text;
|
||||
// Replace multiple spaces with single spaces
|
||||
$text = preg_replace('/\ {2,}/', ' ', $text);
|
||||
// Reduce multiple horrid whitespace characters.
|
||||
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
|
||||
$text = html_entity_decode($text);
|
||||
// Add title
|
||||
$text = $page->name . "\n\n" . $text;
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Image;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
@@ -71,6 +74,7 @@ class ImageService
|
||||
* @param string $imageData
|
||||
* @param string $type
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type)
|
||||
{
|
||||
@@ -86,17 +90,26 @@ class ImageService
|
||||
}
|
||||
$fullPath = $imagePath . $imageName;
|
||||
|
||||
$storage->put($fullPath, $imageData);
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
|
||||
}
|
||||
|
||||
$userId = auth()->user()->id;
|
||||
$image = Image::forceCreate([
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
'type' => $type
|
||||
];
|
||||
|
||||
if (auth()->user() && auth()->user()->id !== 0) {
|
||||
$userId = auth()->user()->id;
|
||||
$imageDetails['created_by'] = $userId;
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
|
||||
$image = Image::forceCreate($imageDetails);
|
||||
|
||||
return $image;
|
||||
}
|
||||
@@ -107,10 +120,12 @@ class ImageService
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
* @throws Exception
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
@@ -127,8 +142,16 @@ class ImageService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
// Otherwise create the thumbnail
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
try {
|
||||
$thumb = $this->imageTool->make($storage->get($image->path));
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
if ($keepRatio) {
|
||||
$thumb->resize($width, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
@@ -188,6 +211,7 @@ class ImageService
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
$image->updated_by = $user->id;
|
||||
$image->save();
|
||||
return $image;
|
||||
}
|
||||
@@ -200,7 +224,7 @@ class ImageService
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = env('STORAGE_TYPE');
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
@@ -226,10 +250,10 @@ class ImageService
|
||||
private function getPublicUrl($filePath)
|
||||
{
|
||||
if ($this->storageUrl === null) {
|
||||
$storageUrl = env('STORAGE_URL');
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
// Get the standard public s3 url if s3 is set as storage type
|
||||
if ($storageUrl == false && env('STORAGE_TYPE') === 's3') {
|
||||
if ($storageUrl == false && config('filesystems.default') === 's3') {
|
||||
$storageDetails = config('filesystems.disks.s3');
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
|
||||
86
app/Services/Ldap.php
Normal file
86
app/Services/Ldap.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
/**
|
||||
* Class Ldap
|
||||
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
||||
* Allows the standard LDAP functions to be mocked for testing.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class Ldap
|
||||
{
|
||||
|
||||
/**
|
||||
* Connect to a LDAP server.
|
||||
* @param string $hostName
|
||||
* @param int $port
|
||||
* @return resource
|
||||
*/
|
||||
public function connect($hostName, $port)
|
||||
{
|
||||
return ldap_connect($hostName, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a LDAP option for the given connection.
|
||||
* @param resource $ldapConnection
|
||||
* @param int $option
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function setOption($ldapConnection, $option, $value)
|
||||
{
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search LDAP tree using the provided filter.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array|null $attributes
|
||||
* @return resource
|
||||
*/
|
||||
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||
{
|
||||
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries from an ldap search result.
|
||||
* @param resource $ldapConnection
|
||||
* @param resource $ldapSearchResult
|
||||
* @return array
|
||||
*/
|
||||
public function getEntries($ldapConnection, $ldapSearchResult)
|
||||
{
|
||||
return ldap_get_entries($ldapConnection, $ldapSearchResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and get entries immediately.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $baseDn
|
||||
* @param string $filter
|
||||
* @param array|null $attributes
|
||||
* @return resource
|
||||
*/
|
||||
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
|
||||
{
|
||||
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
|
||||
return $this->getEntries($ldapConnection, $search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to LDAP directory.
|
||||
* @param resource $ldapConnection
|
||||
* @param string $bindRdn
|
||||
* @param string $bindPassword
|
||||
* @return bool
|
||||
*/
|
||||
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
|
||||
{
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
}
|
||||
148
app/Services/LdapService.php
Normal file
148
app/Services/LdapService.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
* Handles any app-specific LDAP tasks.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
* @param Ldap $ldap
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
* @param $userName
|
||||
* @return array|null
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails($userName)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
|
||||
if ($users['count'] === 0) return null;
|
||||
|
||||
$user = $users[0];
|
||||
return [
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Authenticatable $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) return false;
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
} catch (\ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
||||
return $ldapBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the system user to the LDAP connection using the given credentials
|
||||
* otherwise anonymous access is attempted.
|
||||
* @param $connection
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function bindSystemUser($connection)
|
||||
{
|
||||
$ldapDn = $this->config['dn'];
|
||||
$ldapPass = $this->config['pass'];
|
||||
|
||||
$isAnonymous = ($ldapDn === false || $ldapPass === false);
|
||||
if ($isAnonymous) {
|
||||
$ldapBind = $this->ldap->bind($connection);
|
||||
} else {
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) throw new LdapException('LDAP access failed using ' . ($isAnonymous ? ' anonymous bind.' : ' given dn & pass details'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) return $this->ldapConnection;
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
throw new LdapException('LDAP PHP extension not installed');
|
||||
}
|
||||
|
||||
// Get port from server string if specified.
|
||||
$ldapServer = explode(':', $this->config['server']);
|
||||
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException('Cannot connect to ldap server, Initial connection failed');
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filter string by injecting common variables.
|
||||
* @param string $filterString
|
||||
* @param array $attrs
|
||||
* @return string
|
||||
*/
|
||||
protected function buildFilter($filterString, array $attrs)
|
||||
{
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $attrText) {
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $attrText;
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class SettingService
|
||||
*/
|
||||
public function get($key, $default = false)
|
||||
{
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
return $this->formatValue($value, $default);
|
||||
}
|
||||
|
||||
@@ -50,13 +50,17 @@ class SettingService
|
||||
*/
|
||||
protected function getValueFromStore($key, $default)
|
||||
{
|
||||
$overrideValue = $this->getOverrideValue($key);
|
||||
if ($overrideValue !== null) return $overrideValue;
|
||||
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
if ($this->cache->has($cacheKey)) {
|
||||
return $this->cache->get($cacheKey);
|
||||
}
|
||||
|
||||
$settingObject = $this->getSettingObjectByKey($key);
|
||||
if($settingObject !== null) {
|
||||
|
||||
if ($settingObject !== null) {
|
||||
$value = $settingObject->value;
|
||||
$this->cache->forever($cacheKey, $value);
|
||||
return $value;
|
||||
@@ -65,6 +69,10 @@ class SettingService
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear an item from the cache completely.
|
||||
* @param $key
|
||||
*/
|
||||
protected function clearFromCache($key)
|
||||
{
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
@@ -136,9 +144,23 @@ class SettingService
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
private function getSettingObjectByKey($key)
|
||||
protected function getSettingObjectByKey($key)
|
||||
{
|
||||
return $this->setting->where('setting_key', '=', $key)->first();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an override value for a setting based on certain app conditions.
|
||||
* Used where certain configuration options overrule others.
|
||||
* Returns null if no override value is available.
|
||||
* @param $key
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function getOverrideValue($key)
|
||||
{
|
||||
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -76,9 +76,9 @@ class SocialAuthService
|
||||
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
|
||||
}
|
||||
|
||||
if($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException('The email '. $email.' is already in use. If you already have an account you can connect your ' . $socialDriver .' account from your profile settings.', '/login');
|
||||
throw new UserRegistrationException('The email ' . $email . ' is already in use. If you already have an account you can connect your ' . $socialDriver . ' account from your profile settings.', '/login');
|
||||
}
|
||||
|
||||
return $socialUser;
|
||||
@@ -129,7 +129,7 @@ class SocialAuthService
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
// Change the user that the social account is assigned to.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used buy another user.');
|
||||
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
@@ -172,9 +172,10 @@ class SocialAuthService
|
||||
*/
|
||||
private function checkDriverConfigured($driver)
|
||||
{
|
||||
$upperName = strtoupper($driver);
|
||||
$config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
|
||||
return (!in_array(false, $config) && !in_array(null, $config));
|
||||
$lowerName = strtolower($driver);
|
||||
$configPrefix = 'services.' . $lowerName . '.';
|
||||
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
||||
return !in_array(false, $config) && !in_array(null, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +194,7 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
* @return SocialAccount
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'password', 'image_id'];
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
@@ -68,7 +68,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's permissions from thier role.
|
||||
* Loads the user's permissions from their role.
|
||||
*/
|
||||
private function loadPermissions()
|
||||
{
|
||||
|
||||
@@ -6,18 +6,21 @@
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"laravel/framework": "5.1.*",
|
||||
"laravel/framework": "5.2.*",
|
||||
"intervention/image": "^2.3",
|
||||
"laravel/socialite": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"barryvdh/laravel-debugbar": "^2.0",
|
||||
"league/flysystem-aws-s3-v3": "^1.0"
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "0.6.*"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"phpspec/phpspec": "~2.1"
|
||||
"phpspec/phpspec": "~2.1",
|
||||
"symfony/dom-crawler": "~3.0",
|
||||
"symfony/css-selector": "~3.0"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
||||
1172
composer.lock
generated
1172
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,9 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
@@ -113,13 +116,11 @@ return [
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
Illuminate\Foundation\Providers\ArtisanServiceProvider::class,
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
Illuminate\Cache\CacheServiceProvider::class,
|
||||
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
|
||||
Illuminate\Routing\ControllerServiceProvider::class,
|
||||
Illuminate\Cookie\CookieServiceProvider::class,
|
||||
Illuminate\Database\DatabaseServiceProvider::class,
|
||||
Illuminate\Encryption\EncryptionServiceProvider::class,
|
||||
@@ -142,6 +143,7 @@ return [
|
||||
* Third Party
|
||||
*/
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
Barryvdh\DomPDF\ServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
|
||||
@@ -149,6 +151,7 @@ return [
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
@@ -208,6 +211,7 @@ return [
|
||||
*/
|
||||
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'PDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'Debugbar' => Barryvdh\Debugbar\Facade::class,
|
||||
|
||||
/**
|
||||
|
||||
119
config/auth.php
119
config/auth.php
@@ -2,66 +2,109 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Authentication Driver
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the authentication driver that will be utilized.
|
||||
| This driver manages the retrieval and authentication of the users
|
||||
| attempting to get access to protected areas of your application.
|
||||
| This option controls the default authentication "guard" and password
|
||||
| reset options for your application. You may change these defaults
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| here which uses session storage and the Eloquent user provider.
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| Supported: "session", "token"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication drivers have a user provider. This defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| mechanisms used by this application to persist your user's data.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| sources which represent each model / table. These sources may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => 'eloquent',
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
|
||||
'model' => BookStack\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Model
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "Eloquent" authentication driver, we need to know which
|
||||
| Eloquent model should be used to retrieve your users. Of course, it
|
||||
| is often just the "User" model but you may use whatever you like.
|
||||
|
|
||||
*/
|
||||
|
||||
'model' => BookStack\User::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "Database" authentication driver, we need to know which
|
||||
| table should be used to retrieve your users. We have chosen a basic
|
||||
| default value but you may easily change it to any table you like.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => 'users',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Settings
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may set the options for resetting passwords including the view
|
||||
| that is your password reset e-mail. You can also set the name of the
|
||||
| that is your password reset e-mail. You may also set the name of the
|
||||
| table that maintains all of the reset tokens for your application.
|
||||
|
|
||||
| You may specify multiple password reset configurations if you have more
|
||||
| than one user table or model in the application and you want to have
|
||||
| separate password reset settings based on the specific user types.
|
||||
|
|
||||
| The expire time is the number of minutes that the reset token should be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
*/
|
||||
|
||||
'password' => [
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
266
config/dompdf.php
Normal file
266
config/dompdf.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set some default values. It is possible to add all defines that can be set
|
||||
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||
|
|
||||
*/
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
'orientation' => 'portrait',
|
||||
'defines' => array(
|
||||
/**
|
||||
* The location of the DOMPDF font directory
|
||||
*
|
||||
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
* *Please note the trailing slash.*
|
||||
*
|
||||
* Notes regarding fonts:
|
||||
* Additional .afm font metrics can be added by executing load_font.php from command line.
|
||||
*
|
||||
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
|
||||
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
|
||||
* increase file size unless font subsetting is enabled. Before embedding a font please
|
||||
* review your rights under the font license.
|
||||
*
|
||||
* Any font specification in the source HTML is translated to the closest font available
|
||||
* in the font directory.
|
||||
*
|
||||
* The pdf standard "Base 14 fonts" are:
|
||||
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
|
||||
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
*
|
||||
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||
* This directory can be the same as DOMPDF_FONT_DIR
|
||||
*
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
*/
|
||||
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
|
||||
|
||||
/**
|
||||
* The location of a temporary directory.
|
||||
*
|
||||
* The directory specified must be writeable by the webserver process.
|
||||
* The temporary directory is required to download remote images and when
|
||||
* using the PFDLib back end.
|
||||
*/
|
||||
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
|
||||
|
||||
/**
|
||||
* ==== IMPORTANT ====
|
||||
*
|
||||
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||
* files on the webserver. All local files opened by dompdf must be in a
|
||||
* subdirectory of this directory. DO NOT set it to '/' since this could
|
||||
* allow an attacker to use dompdf to read any files on the server. This
|
||||
* should be an absolute path.
|
||||
* This is only checked on command line call by dompdf.php, but not by
|
||||
* direct class use like:
|
||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||
*/
|
||||
"DOMPDF_CHROOT" => realpath(base_path()),
|
||||
|
||||
/**
|
||||
* Whether to use Unicode fonts or not.
|
||||
*
|
||||
* When set to true the PDF backend must be set to "CPDF" and fonts must be
|
||||
* loaded via load_font.php.
|
||||
*
|
||||
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
||||
* document must be present in your fonts, however.
|
||||
*/
|
||||
"DOMPDF_UNICODE_ENABLED" => true,
|
||||
|
||||
/**
|
||||
* Whether to enable font subsetting or not.
|
||||
*/
|
||||
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
|
||||
|
||||
/**
|
||||
* The PDF rendering backend to use
|
||||
*
|
||||
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
|
||||
* Canvas_Factory} ultimately determines which rendering class to instantiate
|
||||
* based on this setting.
|
||||
*
|
||||
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||
* capabilities for dompdf, however additional features (e.g. object,
|
||||
* image and font support, etc.) differ between backends. Please see
|
||||
* {@link PDFLib_Adapter} for more information on the PDFLib backend
|
||||
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
|
||||
* on CPDF. Also see the documentation for each backend at the links
|
||||
* below.
|
||||
*
|
||||
* The GD rendering backend is a little different than PDFLib and
|
||||
* CPDF. Several features of CPDF and PDFLib are not supported or do
|
||||
* not make any sense when creating image files. For example,
|
||||
* multiple pages are not supported, nor are PDF 'objects'. Have a
|
||||
* look at {@link GD_Adapter} for more information. GD support is
|
||||
* experimental, so use it at your own risk.
|
||||
*
|
||||
* @link http://www.pdflib.com
|
||||
* @link http://www.ros.co.nz/pdf
|
||||
* @link http://www.php.net/image
|
||||
*/
|
||||
"DOMPDF_PDF_BACKEND" => "CPDF",
|
||||
|
||||
/**
|
||||
* PDFlib license key
|
||||
*
|
||||
* If you are using a licensed, commercial version of PDFlib, specify
|
||||
* your license key here. If you are using PDFlib-Lite or are evaluating
|
||||
* the commercial version of PDFlib, comment out this setting.
|
||||
*
|
||||
* @link http://www.pdflib.com
|
||||
*
|
||||
* If pdflib present in web server and auto or selected explicitely above,
|
||||
* a real license code must exist!
|
||||
*/
|
||||
//"DOMPDF_PDFLIB_LICENSE" => "your license key here",
|
||||
|
||||
/**
|
||||
* html target media view which should be rendered into pdf.
|
||||
* List of types and parsing rules for future extensions:
|
||||
* http://www.w3.org/TR/REC-html40/types.html
|
||||
* screen, tty, tv, projection, handheld, print, braille, aural, all
|
||||
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
|
||||
* Note, even though the generated pdf file is intended for print output,
|
||||
* the desired content might be different (e.g. screen or projection view of html file).
|
||||
* Therefore allow specification of content here.
|
||||
*/
|
||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
|
||||
|
||||
/**
|
||||
* The default paper size.
|
||||
*
|
||||
* North America standard is "letter"; other countries generally "a4"
|
||||
*
|
||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||
*/
|
||||
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
|
||||
|
||||
/**
|
||||
* The default font family
|
||||
*
|
||||
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||
* @var string
|
||||
*/
|
||||
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
|
||||
|
||||
/**
|
||||
* Image DPI setting
|
||||
*
|
||||
* This setting determines the default DPI setting for images and fonts. The
|
||||
* DPI may be overridden for inline images by explictly setting the
|
||||
* image's width & height style attributes (i.e. if the image's native
|
||||
* width is 600 pixels and you specify the image's width as 72 points,
|
||||
* the image will have a DPI of 600 in the rendered PDF. The DPI of
|
||||
* background images can not be overridden and is controlled entirely
|
||||
* via this parameter.
|
||||
*
|
||||
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
|
||||
* If a size in html is given as px (or without unit as image size),
|
||||
* this tells the corresponding size in pt.
|
||||
* This adjusts the relative sizes to be similar to the rendering of the
|
||||
* html page in a reference browser.
|
||||
*
|
||||
* In pdf, always 1 pt = 1/72 inch
|
||||
*
|
||||
* Rendering resolution of various browsers in px per inch:
|
||||
* Windows Firefox and Internet Explorer:
|
||||
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
|
||||
* Linux Firefox:
|
||||
* about:config *resolution: Default:96
|
||||
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
|
||||
*
|
||||
* Take care about extra font/image zoom factor of browser.
|
||||
*
|
||||
* In images, <img> size in pixel attribute, img css style, are overriding
|
||||
* the real image dimension in px for rendering.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
"DOMPDF_DPI" => 96,
|
||||
|
||||
/**
|
||||
* Enable inline PHP
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically evaluate
|
||||
* inline PHP contained within <script type="text/php"> ... </script> tags.
|
||||
*
|
||||
* Enabling this for documents you do not trust (e.g. arbitrary remote html
|
||||
* pages) is a security risk. Set this option to false if you wish to process
|
||||
* untrusted documents.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_PHP" => false,
|
||||
|
||||
/**
|
||||
* Enable inline Javascript
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically insert
|
||||
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => true,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
*
|
||||
* If this setting is set to true, DOMPDF will access remote sites for
|
||||
* images and CSS files as required.
|
||||
* This is required for part of test case www/test/image_variants.html through www/examples.php
|
||||
*
|
||||
* Attention!
|
||||
* This can be a security risk, in particular in combination with DOMPDF_ENABLE_PHP and
|
||||
* allowing remote access to dompdf.php or on allowing remote html code to be passed to
|
||||
* $dompdf = new DOMPDF(, $dompdf->load_html(...,
|
||||
* This allows anonymous users to download legally doubtful internet content which on
|
||||
* tracing back appears to being downloaded by your server, or allows malicious php code
|
||||
* in remote html pages to be executed by your server with your account privileges.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_REMOTE" => true,
|
||||
|
||||
/**
|
||||
* A ratio applied to the fonts height to be more like browsers' line height
|
||||
*/
|
||||
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
|
||||
|
||||
/**
|
||||
* Enable CSS float
|
||||
*
|
||||
* Allows people to disabled CSS float support
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_CSS_FLOAT" => true,
|
||||
|
||||
|
||||
/**
|
||||
* Use the more-than-experimental HTML5 Lib parser
|
||||
*/
|
||||
"DOMPDF_ENABLE_HTML5PARSER" => true,
|
||||
|
||||
|
||||
),
|
||||
|
||||
|
||||
);
|
||||
@@ -15,7 +15,18 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 'local',
|
||||
'default' => env('STORAGE_TYPE', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Storage URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the url to where the storage is located for when using an external
|
||||
| file storage service, such as s3, to store publicly accessible assets.
|
||||
|
|
||||
*/
|
||||
'url' => env('STORAGE_URL', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -13,6 +13,8 @@ return [
|
||||
| to have a conventional place to find your various credentials.
|
||||
|
|
||||
*/
|
||||
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
|
||||
'callback_url' => env('APP_URL', false),
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => '',
|
||||
@@ -47,4 +49,13 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||
],
|
||||
|
||||
'ldap' => [
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||
'version' => env('LDAP_VERSION', false)
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
@@ -21,10 +21,10 @@ class CreateUsersTable extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
\BookStack\User::create([
|
||||
\BookStack\User::forceCreate([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@admin.com',
|
||||
'password' => \Illuminate\Support\Facades\Hash::make('password')
|
||||
'password' => bcrypt('password')
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddExternalAuthToUsers extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('external_auth_id')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('external_auth_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
|
||||
elixir(function(mix) {
|
||||
mix.sass('styles.scss')
|
||||
.sass('print-styles.scss')
|
||||
.sass('export-styles.scss')
|
||||
.browserify('global.js', 'public/js/common.js')
|
||||
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="DB_CONNECTION" value="mysql_testing"/>
|
||||
<env name="MAIL_PRETEND" value="true"/>
|
||||
<env name="AUTH_METHOD" value="standard"/>
|
||||
<env name="DISABLE_EXTERNAL_SERVICES" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"css/styles.css": "css/styles.css?version=2d0edbb",
|
||||
"css/print-styles.css": "css/print-styles.css?version=2d0edbb",
|
||||
"js/common.js": "js/common.js?version=2d0edbb"
|
||||
"css/styles.css": "css/styles.css?version=2a2a428",
|
||||
"css/print-styles.css": "css/print-styles.css?version=2a2a428",
|
||||
"js/common.js": "js/common.js?version=2a2a428"
|
||||
}
|
||||
1
public/css/export-styles.css
vendored
Normal file
1
public/css/export-styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/css/print-styles.css
vendored
2
public/css/print-styles.css
vendored
@@ -1 +1 @@
|
||||
.faded-small,.print-hidden,header{display:none}@font-face{font-family:Roboto;src:url(/fonts/roboto-bold-webfont.eot);src:url(/fonts/roboto-bold-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-bold-webfont.woff2) format("woff2"),url(/fonts/roboto-bold-webfont.woff) format("woff"),url(/fonts/roboto-bold-webfont.ttf) format("truetype"),url(/fonts/roboto-bold-webfont.svg#robotobold) format("svg");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-bolditalic-webfont.eot);src:url(/fonts/roboto-bolditalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-bolditalic-webfont.woff2) format("woff2"),url(/fonts/roboto-bolditalic-webfont.woff) format("woff"),url(/fonts/roboto-bolditalic-webfont.ttf) format("truetype"),url(/fonts/roboto-bolditalic-webfont.svg#robotobold_italic) format("svg");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-italic-webfont.eot);src:url(/fonts/roboto-italic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-italic-webfont.woff2) format("woff2"),url(/fonts/roboto-italic-webfont.woff) format("woff"),url(/fonts/roboto-italic-webfont.ttf) format("truetype"),url(/fonts/roboto-italic-webfont.svg#robotoitalic) format("svg");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-light-webfont.eot);src:url(/fonts/roboto-light-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-light-webfont.woff2) format("woff2"),url(/fonts/roboto-light-webfont.woff) format("woff"),url(/fonts/roboto-light-webfont.ttf) format("truetype"),url(/fonts/roboto-light-webfont.svg#robotolight) format("svg");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-lightitalic-webfont.eot);src:url(/fonts/roboto-lightitalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-lightitalic-webfont.woff2) format("woff2"),url(/fonts/roboto-lightitalic-webfont.woff) format("woff"),url(/fonts/roboto-lightitalic-webfont.ttf) format("truetype"),url(/fonts/roboto-lightitalic-webfont.svg#robotolight_italic) format("svg");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-medium-webfont.eot);src:url(/fonts/roboto-medium-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-medium-webfont.woff2) format("woff2"),url(/fonts/roboto-medium-webfont.woff) format("woff"),url(/fonts/roboto-medium-webfont.ttf) format("truetype"),url(/fonts/roboto-medium-webfont.svg#robotomedium) format("svg");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(/fonts/roboto-mediumitalic-webfont.eot);src:url(/fonts/roboto-mediumitalic-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-mediumitalic-webfont.woff2) format("woff2"),url(/fonts/roboto-mediumitalic-webfont.woff) format("woff"),url(/fonts/roboto-mediumitalic-webfont.ttf) format("truetype"),url(/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic) format("svg");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(/fonts/roboto-regular-webfont.eot);src:url(/fonts/roboto-regular-webfont.eot?#iefix) format("embedded-opentype"),url(/fonts/roboto-regular-webfont.woff2) format("woff2"),url(/fonts/roboto-regular-webfont.woff) format("woff"),url(/fonts/roboto-regular-webfont.ttf) format("truetype"),url(/fonts/roboto-regular-webfont.svg#robotoregular) format("svg");font-weight:400;font-style:normal}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
.faded-small,.print-hidden,header{display:none}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
|
||||
2
public/css/styles.css
vendored
2
public/css/styles.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
public/uploads/.gitignore
vendored
Normal file
2
public/uploads/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
113
readme.md
113
readme.md
@@ -2,26 +2,32 @@
|
||||
|
||||
A platform to create documentation/wiki content. General information about BookStack can be found at https://www.bookstackapp.com/
|
||||
|
||||
1. [Requirements](#requirements)
|
||||
2. [Installation](#installation)
|
||||
- [Server Rewrite Rules](#url-rewrite-rules)
|
||||
3. [Updating](#updating-bookstack)
|
||||
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. On top of those are some front-end build tools which are only required when developing.
|
||||
BookStack has similar requirements to Laravel:
|
||||
|
||||
* PHP >= 5.5.9
|
||||
* OpenSSL PHP Extension
|
||||
* PDO PHP Extension
|
||||
* MBstring PHP Extension
|
||||
* Tokenizer PHP Extension
|
||||
* 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/)
|
||||
* [Node.js](https://nodejs.org/en/) **Development Only**
|
||||
* [Gulp](http://gulpjs.com/) **Development Only**
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure the requirements are met before installing.
|
||||
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.
|
||||
|
||||
@@ -35,10 +41,10 @@ git clone https://github.com/ssddanbrown/BookStack.git --branch release --single
|
||||
|
||||
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` folders are writable by the web server.
|
||||
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 migrate` to update the database.
|
||||
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
|
||||
@@ -59,8 +65,87 @@ location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
```
|
||||
## Updating BookStack
|
||||
|
||||
## Testing
|
||||
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
|
||||
|
||||
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**
|
||||
* [Gulp](http://gulpjs.com/) **Development Only**
|
||||
|
||||
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.
|
||||
|
||||
BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
|
||||
|
||||
@@ -71,7 +156,7 @@ php artisan migrate --database=mysql_testing
|
||||
php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
|
||||
```
|
||||
|
||||
Once done you can run `phpunit` in the application root directory to run all tests.
|
||||
Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function (ngApp) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
|
||||
function ($scope, $attrs, $http, $timeout, imageManagerService) {
|
||||
@@ -17,21 +17,40 @@ module.exports = function (ngApp) {
|
||||
var dataLoaded = false;
|
||||
var callback = false;
|
||||
|
||||
/**
|
||||
* Simple returns the appropriate upload url depending on the image type set.
|
||||
* @returns {string}
|
||||
*/
|
||||
$scope.getUploadUrl = function () {
|
||||
return '/images/' + $scope.imageType + '/upload';
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs on image upload, Adds an image to local list of images
|
||||
* and shows a success message to the user.
|
||||
* @param file
|
||||
* @param data
|
||||
*/
|
||||
$scope.uploadSuccess = function (file, data) {
|
||||
$scope.$apply(() => {
|
||||
$scope.images.unshift(data);
|
||||
});
|
||||
events.emit('success', 'Image uploaded');
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs the callback and hides the image manager.
|
||||
* @param returnData
|
||||
*/
|
||||
function callbackAndHide(returnData) {
|
||||
if (callback) callback(returnData);
|
||||
$scope.showing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image select action. Checks if a double-click was fired.
|
||||
* @param image
|
||||
*/
|
||||
$scope.imageSelect = function (image) {
|
||||
var dblClickTime = 300;
|
||||
var currentTime = Date.now();
|
||||
@@ -48,10 +67,19 @@ module.exports = function (ngApp) {
|
||||
previousClickTime = currentTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action that runs when the 'Select image' button is clicked.
|
||||
* Runs the callback and hides the image manager.
|
||||
*/
|
||||
$scope.selectButtonClick = function () {
|
||||
callbackAndHide($scope.selectedImage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the image manager.
|
||||
* Takes a callback to execute later on.
|
||||
* @param doneCallback
|
||||
*/
|
||||
function show(doneCallback) {
|
||||
callback = doneCallback;
|
||||
$scope.showing = true;
|
||||
@@ -62,6 +90,8 @@ module.exports = function (ngApp) {
|
||||
}
|
||||
}
|
||||
|
||||
// Connects up the image manger so it can be used externally
|
||||
// such as from TinyMCE.
|
||||
imageManagerService.show = show;
|
||||
imageManagerService.showExternal = function (doneCallback) {
|
||||
$scope.$apply(() => {
|
||||
@@ -70,10 +100,16 @@ module.exports = function (ngApp) {
|
||||
};
|
||||
window.ImageManager = imageManagerService;
|
||||
|
||||
/**
|
||||
* Hide the image manager
|
||||
*/
|
||||
$scope.hide = function () {
|
||||
$scope.showing = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the list image data from the server.
|
||||
*/
|
||||
function fetchData() {
|
||||
var url = '/images/' + $scope.imageType + '/all/' + page;
|
||||
$http.get(url).then((response) => {
|
||||
@@ -82,28 +118,33 @@ module.exports = function (ngApp) {
|
||||
page++;
|
||||
});
|
||||
}
|
||||
$scope.fetchData = fetchData;
|
||||
|
||||
/**
|
||||
* Save the details of an image.
|
||||
* @param event
|
||||
*/
|
||||
$scope.saveImageDetails = function (event) {
|
||||
event.preventDefault();
|
||||
var url = '/images/update/' + $scope.selectedImage.id;
|
||||
$http.put(url, this.selectedImage).then((response) => {
|
||||
$scope.imageUpdateSuccess = true;
|
||||
$timeout(() => {
|
||||
$scope.imageUpdateSuccess = false;
|
||||
}, 3000);
|
||||
events.emit('success', 'Image details updated');
|
||||
}, (response) => {
|
||||
var errors = response.data;
|
||||
var message = '';
|
||||
Object.keys(errors).forEach((key) => {
|
||||
message += errors[key].join('\n');
|
||||
});
|
||||
$scope.imageUpdateFailure = message;
|
||||
$timeout(() => {
|
||||
$scope.imageUpdateFailure = false;
|
||||
}, 5000);
|
||||
events.emit('error', message);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete an image from system and notify of success.
|
||||
* Checks if it should force delete when an image
|
||||
* has dependant pages.
|
||||
* @param event
|
||||
*/
|
||||
$scope.deleteImage = function (event) {
|
||||
event.preventDefault();
|
||||
var force = $scope.dependantPages !== false;
|
||||
@@ -112,10 +153,7 @@ module.exports = function (ngApp) {
|
||||
$http.delete(url).then((response) => {
|
||||
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
|
||||
$scope.selectedImage = false;
|
||||
$scope.imageDeleteSuccess = true;
|
||||
$timeout(() => {
|
||||
$scope.imageDeleteSuccess = false;
|
||||
}, 3000);
|
||||
events.emit('success', 'Image successfully deleted');
|
||||
}, (response) => {
|
||||
// Pages failure
|
||||
if (response.status === 400) {
|
||||
@@ -124,6 +162,15 @@ module.exports = function (ngApp) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple date creator used to properly format dates.
|
||||
* @param stringDate
|
||||
* @returns {Date}
|
||||
*/
|
||||
$scope.getDate = function(stringDate) {
|
||||
return new Date(stringDate);
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
|
||||
var imagePickerTemplate = require('./components/image-picker.html');
|
||||
var dropZoneTemplate = require('./components/drop-zone.html');
|
||||
|
||||
module.exports = function (ngApp) {
|
||||
module.exports = function (ngApp, events) {
|
||||
|
||||
/**
|
||||
* Toggle Switches
|
||||
@@ -56,6 +56,7 @@ module.exports = function (ngApp) {
|
||||
var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false';
|
||||
scope.image = scope.currentImage;
|
||||
scope.value = scope.currentImage || '';
|
||||
if (usingIds) scope.value = scope.currentId;
|
||||
|
||||
function setImage(imageModel, imageUrl) {
|
||||
scope.image = imageUrl;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// AngularJS - Create application and load components
|
||||
var angular = require('angular');
|
||||
@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
|
||||
var ngSanitize = require('angular-sanitize');
|
||||
|
||||
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
|
||||
var services = require('./services')(ngApp);
|
||||
var directives = require('./directives')(ngApp);
|
||||
var controllers = require('./controllers')(ngApp);
|
||||
|
||||
|
||||
// Global Event System
|
||||
var Events = {
|
||||
listeners: {},
|
||||
emit: function (eventName, eventData) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||
var eventsToStart = this.listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
var event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
listen: function (eventName, callback) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
||||
this.listeners[eventName].push(callback);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
window.Events = Events;
|
||||
|
||||
var services = require('./services')(ngApp, Events);
|
||||
var directives = require('./directives')(ngApp, Events);
|
||||
var controllers = require('./controllers')(ngApp, Events);
|
||||
|
||||
//Global jQuery Config & Extensions
|
||||
|
||||
@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
|
||||
// Global jQuery Elements
|
||||
$(function () {
|
||||
|
||||
|
||||
var notifications = $('.notification');
|
||||
var successNotification = notifications.filter('.pos');
|
||||
var errorNotification = notifications.filter('.neg');
|
||||
// Notification Events
|
||||
window.Events.listen('success', function (text) {
|
||||
successNotification.hide();
|
||||
successNotification.find('span').text(text);
|
||||
setTimeout(() => {
|
||||
successNotification.show();
|
||||
}, 1);
|
||||
});
|
||||
window.Events.listen('error', function (text) {
|
||||
errorNotification.find('span').text(text);
|
||||
errorNotification.show();
|
||||
});
|
||||
|
||||
// Notification hiding
|
||||
$('.notification').click(function () {
|
||||
notifications.click(function () {
|
||||
$(this).fadeOut(100);
|
||||
});
|
||||
|
||||
@@ -44,6 +83,29 @@ $(function () {
|
||||
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
|
||||
});
|
||||
|
||||
// Back to top button
|
||||
$('#back-to-top').click(function() {
|
||||
$('#header').smoothScrollTo();
|
||||
});
|
||||
var scrollTopShowing = false;
|
||||
var scrollTop = document.getElementById('back-to-top');
|
||||
var scrollTopBreakpoint = 1200;
|
||||
window.addEventListener('scroll', function() {
|
||||
if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
|
||||
scrollTop.style.display = 'block';
|
||||
scrollTopShowing = true;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.opacity = 1;
|
||||
}, 1);
|
||||
} else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
|
||||
scrollTop.style.opacity = 0;
|
||||
scrollTopShowing = false;
|
||||
setTimeout(() => {
|
||||
scrollTop.style.display = 'none';
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ module.exports = {
|
||||
statusbar: false,
|
||||
menubar: false,
|
||||
paste_data_images: false,
|
||||
//height: 700,
|
||||
extended_valid_elements: 'pre[*]',
|
||||
automatic_uploads: false,
|
||||
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
|
||||
@@ -31,7 +30,7 @@ module.exports = {
|
||||
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
|
||||
},
|
||||
file_browser_callback: function (field_name, url, type, win) {
|
||||
ImageManager.show(function (image) {
|
||||
window.ImageManager.showExternal(function (image) {
|
||||
win.document.getElementById(field_name).value = image.url;
|
||||
if ("createEvent" in document) {
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
@@ -40,6 +39,10 @@ module.exports = {
|
||||
} else {
|
||||
win.document.getElementById(field_name).fireEvent("onchange");
|
||||
}
|
||||
var html = '<a href="' + image.url + '" target="_blank">';
|
||||
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
|
||||
html += '</a>';
|
||||
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
|
||||
});
|
||||
},
|
||||
paste_preprocess: function (plugin, args) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = function(ngApp) {
|
||||
module.exports = function(ngApp, events) {
|
||||
|
||||
ngApp.factory('imageManagerService', function() {
|
||||
return {
|
||||
|
||||
96
resources/assets/sass/_fonts.scss
Normal file
96
resources/assets/sass/_fonts.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-bold-webfont.eot');
|
||||
src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-bold-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-bolditalic-webfont.eot');
|
||||
src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-italic-webfont.eot');
|
||||
src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-italic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-light-webfont.eot');
|
||||
src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-light-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-light-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-light-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-lightitalic-webfont.eot');
|
||||
src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-medium-webfont.eot');
|
||||
src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-medium-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-mediumitalic-webfont.eot');
|
||||
src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-regular-webfont.eot');
|
||||
src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-regular-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@@ -161,6 +161,12 @@ form.search-box {
|
||||
padding: $-xs 0;
|
||||
color: #555;
|
||||
text-align: left !important;
|
||||
&.wide {
|
||||
min-width: 220px;
|
||||
}
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
padding: $-xs $-m;
|
||||
@@ -187,7 +193,7 @@ form.search-box {
|
||||
}
|
||||
|
||||
.faded {
|
||||
a, button, span {
|
||||
a, button, span, span > div {
|
||||
color: #666;
|
||||
}
|
||||
.text-button {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
max-width: 1340px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@@ -44,18 +43,49 @@
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.image-manager-list img {
|
||||
.image-manager-list .image {
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
float: left;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: (100%/6);
|
||||
height: auto;
|
||||
border: 1px solid #FFF;
|
||||
border: 1px solid #DDD;
|
||||
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
|
||||
overflow: hidden;
|
||||
&.selected {
|
||||
transform: scale3d(0.92, 0.92, 0.92);
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.image-meta {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #EEE;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
font-size: 10px;
|
||||
padding: 3px 4px;
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include smaller-than($xl) {
|
||||
width: (100%/4);
|
||||
}
|
||||
@include smaller-than($m) {
|
||||
.image-meta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
.book-tree .sidebar-page-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin-top: $-xs;
|
||||
margin: $-xs 0 0;
|
||||
padding-left: 0;
|
||||
border-left: 5px solid $color-book;
|
||||
li a {
|
||||
display: block;
|
||||
|
||||
@@ -223,13 +223,13 @@ span.highlight {
|
||||
* Lists
|
||||
*/
|
||||
ul {
|
||||
list-style: disc;
|
||||
margin-left: $-m*1.5;
|
||||
padding-left: $-m * 1.5;
|
||||
list-style: disc inside;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
margin-left: $-m*1.5;
|
||||
list-style: decimal inside;
|
||||
padding-left: $-m * 1.5;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -27,8 +27,8 @@ $-xs: 6px;
|
||||
$-xxs: 3px;
|
||||
|
||||
// Fonts
|
||||
$heading: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$text: 'Roboto', Helvetica, Arial, sans-serif;
|
||||
$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
|
||||
$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
|
||||
$fs-m: 15px;
|
||||
$fs-s: 14px;
|
||||
|
||||
@@ -52,101 +52,3 @@ $text-light: #EEE;
|
||||
$bs-light: 0 0 4px 1px #CCC;
|
||||
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
|
||||
|
||||
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-bold-webfont.eot');
|
||||
src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-bold-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-bolditalic-webfont.eot');
|
||||
src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-italic-webfont.eot');
|
||||
src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-italic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-light-webfont.eot');
|
||||
src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-light-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-light-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-light-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-lightitalic-webfont.eot');
|
||||
src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-medium-webfont.eot');
|
||||
src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-medium-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-mediumitalic-webfont.eot');
|
||||
src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/roboto-regular-webfont.eot');
|
||||
src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
|
||||
url('/fonts/roboto-regular-webfont.woff') format('woff'),
|
||||
url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
|
||||
url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
17
resources/assets/sass/export-styles.scss
Normal file
17
resources/assets/sass/export-styles.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
//@import "reset";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
@import "html";
|
||||
@import "text";
|
||||
@import "grid";
|
||||
@import "blocks";
|
||||
@import "forms";
|
||||
@import "tables";
|
||||
@import "header";
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "reset";
|
||||
@import "variables";
|
||||
@import "fonts";
|
||||
@import "mixins";
|
||||
@import "html";
|
||||
@import "text";
|
||||
@@ -125,4 +126,42 @@ $loadingSize: 10px;
|
||||
i {
|
||||
padding-right: $-s;
|
||||
}
|
||||
}
|
||||
|
||||
// Back to top link
|
||||
$btt-size: 40px;
|
||||
#back-to-top {
|
||||
background-color: rgba($primary, 0.4);
|
||||
position: fixed;
|
||||
bottom: $-m;
|
||||
right: $-m;
|
||||
padding: $-xs $-s;
|
||||
cursor: pointer;
|
||||
color: #FFF;
|
||||
width: $btt-size;
|
||||
height: $btt-size;
|
||||
border-radius: $btt-size;
|
||||
transition: all ease-in-out 180ms;
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
&:hover {
|
||||
width: $btt-size*3.4;
|
||||
background-color: rgba($primary, 1);
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
width: $btt-size*3.4;
|
||||
}
|
||||
i {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
padding: 0 $-s 0 0;
|
||||
}
|
||||
span {
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
}
|
||||
}
|
||||
19
resources/views/auth/forms/login/ldap.blade.php
Normal file
19
resources/views/auth/forms/login/ldap.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
@include('form/text', ['name' => 'username', 'tabindex' => 1])
|
||||
</div>
|
||||
|
||||
@if(session('request-email', false) === true)
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form/text', ['name' => 'email', 'tabindex' => 1])
|
||||
<span class="text-neg">
|
||||
Please enter an email to use for this account.
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
@include('form/password', ['name' => 'password', 'tabindex' => 2])
|
||||
</div>
|
||||
10
resources/views/auth/forms/login/standard.blade.php
Normal file
10
resources/views/auth/forms/login/standard.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form/text', ['name' => 'email', 'tabindex' => 1])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
@include('form/password', ['name' => 'password', 'tabindex' => 2])
|
||||
<span class="block small"><a href="/password/email">Forgot Password?</a></span>
|
||||
</div>
|
||||
@@ -15,16 +15,8 @@
|
||||
<form action="/login" method="POST" id="login-form">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form/text', ['name' => 'email', 'tabindex' => 1])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
@include('form/password', ['name' => 'password', 'tabindex' => 2])
|
||||
<span class="block small"><a href="/password/email">Forgot Password?</a></span>
|
||||
</div>
|
||||
@include('auth/forms/login/' . $authMethod)
|
||||
|
||||
<div class="form-group">
|
||||
<label for="remember" class="inline">Remember Me</label>
|
||||
@@ -34,7 +26,7 @@
|
||||
|
||||
|
||||
<div class="from-group">
|
||||
<button class="button block pos" tabindex="3">Sign In</button>
|
||||
<button class="button block pos" tabindex="3"><i class="zmdi zmdi-sign-in"></i> Sign In</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -77,6 +77,11 @@
|
||||
@yield('content')
|
||||
</section>
|
||||
|
||||
<div id="back-to-top">
|
||||
<div class="inner">
|
||||
<i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
|
||||
</div>
|
||||
</div>
|
||||
@yield('bottom')
|
||||
<script src="{{ versioned_asset('js/common.js') }}"></script>
|
||||
@yield('scripts')
|
||||
|
||||
10
resources/views/errors/500.blade.php
Normal file
10
resources/views/errors/500.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
@extends('base')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="container">
|
||||
<h1 class="text-muted">An Error Occurred</h1>
|
||||
<p>{{ $message }}</p>
|
||||
</div>
|
||||
|
||||
@stop
|
||||
33
resources/views/pages/export.blade.php
Normal file
33
resources/views/pages/export.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>{{ $page->name }}</title>
|
||||
|
||||
<style>
|
||||
{!! $css !!}
|
||||
</style>
|
||||
@yield('head')
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="page-show">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="page-content">
|
||||
|
||||
@include('pages/page-display')
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="text-muted small">
|
||||
Created {{$page->created_at->toDayDateTimeString()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
|
||||
<br>
|
||||
Last Updated {{$page->updated_at->toDayDateTimeString()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="faded-small">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 faded">
|
||||
<div class="col-sm-4 faded">
|
||||
<div class="action-buttons text-left">
|
||||
<a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 faded">
|
||||
<div class="col-sm-8 faded">
|
||||
<div class="action-buttons">
|
||||
<a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a>
|
||||
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
|
||||
|
||||
30
resources/views/pages/pdf.blade.php
Normal file
30
resources/views/pages/pdf.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
@extends('pages/export')
|
||||
|
||||
@section('head')
|
||||
<style>
|
||||
body {
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
table {
|
||||
max-width: 800px !important;
|
||||
font-size: 0.8em;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
table td {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.page-content img.align-left, .page-content img.align-right {
|
||||
float: none !important;
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@stop
|
||||
@@ -19,6 +19,14 @@
|
||||
</div>
|
||||
<div class="col-sm-6 faded">
|
||||
<div class="action-buttons">
|
||||
<span dropdown class="dropdown-container">
|
||||
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
|
||||
<ul class="wide">
|
||||
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
|
||||
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
|
||||
</ul>
|
||||
</span>
|
||||
@if($currentUser->can('page-update'))
|
||||
<a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
|
||||
<a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
<div class="image-manager-content">
|
||||
<div class="image-manager-list">
|
||||
<div ng-repeat="image in images">
|
||||
<img class="anim fadeIn"
|
||||
ng-class="{selected: (image==selectedImage)}"
|
||||
ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"
|
||||
ng-click="imageSelect(image)"
|
||||
ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
|
||||
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
|
||||
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
|
||||
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
|
||||
<div class="image-meta">
|
||||
<span class="name" ng-bind="image.name"></span>
|
||||
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
|
||||
</div>
|
||||
@@ -19,18 +22,20 @@
|
||||
|
||||
<div class="image-manager-sidebar">
|
||||
<h2>Images</h2>
|
||||
<hr class="even">
|
||||
<drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone>
|
||||
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
|
||||
|
||||
<hr class="even">
|
||||
|
||||
<form ng-submit="saveImageDetails($event)">
|
||||
<div>
|
||||
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
|
||||
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">Image Name</label>
|
||||
<input type="text" id="name" name="name" ng-model="selectedImage.name">
|
||||
<p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p>
|
||||
<p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -53,8 +58,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p>
|
||||
|
||||
<div class="image-manager-bottom">
|
||||
<button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
|
||||
<i class="zmdi zmdi-square-right"></i>Select Image
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
@if(Session::has('success'))
|
||||
<div class="notification anim pos">
|
||||
<i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(Session::has('error'))
|
||||
<div class="notification anim neg stopped">
|
||||
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
|
||||
<i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
|
||||
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
|
||||
</div>
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
<!-- Meta -->
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="token" content="{{ csrf_token() }}">
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!-- Styles and Fonts -->
|
||||
<link rel="stylesheet" href="{{ versioned_asset('css/styles.css') }}">
|
||||
<link rel="stylesheet" media="print" href="{{ versioned_asset('css/print-styles.css') }}">
|
||||
<link href='//fonts.googleapis.com/css?family=Roboto:400,400italic,500,500italic,700,700italic,300italic,100,300' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="/libs/material-design-iconic-font/css/material-design-iconic-font.min.css">
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||
<script src="/libs/jquery/jquery.min.js?version=2.1.4"></script>
|
||||
|
||||
</head>
|
||||
<body class="@yield('body-class')" id="app">
|
||||
<body class="@yield('body-class')" ng-app="bookStack">
|
||||
|
||||
@include('partials/notifications')
|
||||
|
||||
@@ -37,12 +37,15 @@
|
||||
@yield('header-buttons')
|
||||
</div>
|
||||
@if(isset($signedIn) && $signedIn)
|
||||
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
|
||||
<div class="dropdown-container" data-dropdown>
|
||||
<span class="user-name" data-dropdown-toggle>
|
||||
{{ $currentUser->name }} <i class="zmdi zmdi-caret-down"></i>
|
||||
<div class="dropdown-container" dropdown>
|
||||
<span class="user-name" dropdown-toggle>
|
||||
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
|
||||
<span class="name" ng-non-bindable>{{ $currentUser->name }}</span> <i class="zmdi zmdi-caret-down"></i>
|
||||
</span>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/users/{{$currentUser->id}}" class="text-primary"><i class="zmdi zmdi-edit zmdi-hc-lg"></i>Edit Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logout" class="text-neg"><i class="zmdi zmdi-run zmdi-hc-lg"></i>Logout</a>
|
||||
</li>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<form action="/users/create" method="post">
|
||||
{!! csrf_field() !!}
|
||||
@include('users/form')
|
||||
@include('users.forms.' . $authMethod)
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<h1>Edit {{ $user->id === $currentUser->id ? 'Profile' : 'User' }}</h1>
|
||||
{!! csrf_field() !!}
|
||||
<input type="hidden" name="_method" value="put">
|
||||
@include('users/form', ['model' => $user])
|
||||
@include('users.forms.' . $authMethod, ['model' => $user])
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
30
resources/views/users/forms/ldap.blade.php
Normal file
30
resources/views/users/forms/ldap.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
@include('form.text', ['name' => 'name'])
|
||||
</div>
|
||||
|
||||
@if($currentUser->can('user-update'))
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form.text', ['name' => 'email'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($currentUser->can('user-update'))
|
||||
<div class="form-group">
|
||||
<label for="role">User Role</label>
|
||||
@include('form.role-select', ['name' => 'role', 'options' => \BookStack\Role::all(), 'displayKey' => 'display_name'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($currentUser->can('user-update'))
|
||||
<div class="form-group">
|
||||
<label for="external_auth_id">External Authentication ID</label>
|
||||
@include('form.text', ['name' => 'external_auth_id'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<a href="/users" class="button muted">Cancel</a>
|
||||
<button class="button pos" type="submit">Save</button>
|
||||
</div>
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
@include('form/text', ['name' => 'name'])
|
||||
@include('form.text', ['name' => 'name'])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
@include('form/text', ['name' => 'email'])
|
||||
@include('form.text', ['name' => 'email'])
|
||||
</div>
|
||||
|
||||
@if($currentUser->can('user-update'))
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
@include('form/password', ['name' => 'password'])
|
||||
@include('form.password', ['name' => 'password'])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password-confirm">Confirm Password</label>
|
||||
@include('form/password', ['name' => 'password-confirm'])
|
||||
@include('form.password', ['name' => 'password-confirm'])
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
2
storage/fonts/.gitignore
vendored
Normal file
2
storage/fonts/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
class ActivityTrackingTest extends TestCase
|
||||
{
|
||||
|
||||
public function testRecentlyViewedBooks()
|
||||
public function test_recently_viewed_books()
|
||||
{
|
||||
$books = \BookStack\Book::all()->take(10);
|
||||
|
||||
@@ -21,7 +21,7 @@ class ActivityTrackingTest extends TestCase
|
||||
->seeInElement('#recents', $books[1]->name);
|
||||
}
|
||||
|
||||
public function testPopularBooks()
|
||||
public function test_popular_books()
|
||||
{
|
||||
$books = \BookStack\Book::all()->take(10);
|
||||
|
||||
|
||||
@@ -5,23 +5,19 @@ use BookStack\EmailConfirmation;
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
|
||||
public function testAuthWorking()
|
||||
public function test_auth_working()
|
||||
{
|
||||
$this->visit('/')
|
||||
->seePageIs('/login');
|
||||
}
|
||||
|
||||
public function testLogin()
|
||||
public function test_login()
|
||||
{
|
||||
$this->visit('/')
|
||||
->seePageIs('/login');
|
||||
|
||||
$this->login('admin@admin.com', 'password')
|
||||
->seePageIs('/')
|
||||
->see('BookStack');
|
||||
->seePageIs('/');
|
||||
}
|
||||
|
||||
public function testPublicViewing()
|
||||
public function test_public_viewing()
|
||||
{
|
||||
$settings = app('BookStack\Services\SettingService');
|
||||
$settings->put('app-public', 'true');
|
||||
@@ -30,7 +26,7 @@ class AuthTest extends TestCase
|
||||
->see('Sign In');
|
||||
}
|
||||
|
||||
public function testRegistrationShowing()
|
||||
public function test_registration_showing()
|
||||
{
|
||||
// Ensure registration form is showing
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
@@ -40,7 +36,7 @@ class AuthTest extends TestCase
|
||||
->seePageIs('/register');
|
||||
}
|
||||
|
||||
public function testNormalRegistration()
|
||||
public function test_normal_registration()
|
||||
{
|
||||
// Set settings and get user instance
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
@@ -58,7 +54,8 @@ class AuthTest extends TestCase
|
||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email]);
|
||||
}
|
||||
|
||||
public function testConfirmedRegistration()
|
||||
|
||||
public function test_confirmed_registration()
|
||||
{
|
||||
// Set settings and get user instance
|
||||
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']);
|
||||
@@ -102,7 +99,32 @@ class AuthTest extends TestCase
|
||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]);
|
||||
}
|
||||
|
||||
public function testUserCreation()
|
||||
public function test_restricted_registration()
|
||||
{
|
||||
$this->setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true', 'registration-restrict' => 'example.com']);
|
||||
$user = factory(\BookStack\User::class)->make();
|
||||
// Go through registration process
|
||||
$this->visit('/register')
|
||||
->type($user->name, '#name')
|
||||
->type($user->email, '#email')
|
||||
->type($user->password, '#password')
|
||||
->press('Create Account')
|
||||
->seePageIs('/register')
|
||||
->dontSeeInDatabase('users', ['email' => $user->email])
|
||||
->see('That email domain does not have access to this application');
|
||||
|
||||
$user->email = 'barry@example.com';
|
||||
|
||||
$this->visit('/register')
|
||||
->type($user->name, '#name')
|
||||
->type($user->email, '#email')
|
||||
->type($user->password, '#password')
|
||||
->press('Create Account')
|
||||
->seePageIs('/register/confirm')
|
||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
|
||||
}
|
||||
|
||||
public function test_user_creation()
|
||||
{
|
||||
$user = factory(\BookStack\User::class)->make();
|
||||
|
||||
@@ -120,7 +142,7 @@ class AuthTest extends TestCase
|
||||
->see($user->name);
|
||||
}
|
||||
|
||||
public function testUserUpdating()
|
||||
public function test_user_updating()
|
||||
{
|
||||
$user = \BookStack\User::all()->last();
|
||||
$password = $user->password;
|
||||
@@ -136,7 +158,7 @@ class AuthTest extends TestCase
|
||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||
}
|
||||
|
||||
public function testUserPasswordUpdate()
|
||||
public function test_user_password_update()
|
||||
{
|
||||
$user = \BookStack\User::all()->last();
|
||||
$userProfilePage = '/users/' . $user->id;
|
||||
@@ -156,7 +178,7 @@ class AuthTest extends TestCase
|
||||
$this->assertTrue(Hash::check('newpassword', $userPassword));
|
||||
}
|
||||
|
||||
public function testUserDeletion()
|
||||
public function test_user_deletion()
|
||||
{
|
||||
$userDetails = factory(\BookStack\User::class)->make();
|
||||
$user = $this->getNewUser($userDetails->toArray());
|
||||
@@ -170,7 +192,7 @@ class AuthTest extends TestCase
|
||||
->notSeeInDatabase('users', ['name' => $user->name]);
|
||||
}
|
||||
|
||||
public function testUserCannotBeDeletedIfLastAdmin()
|
||||
public function test_user_cannot_be_deleted_if_last_admin()
|
||||
{
|
||||
$adminRole = \BookStack\Role::getRole('admin');
|
||||
// Ensure we currently only have 1 admin user
|
||||
@@ -184,7 +206,7 @@ class AuthTest extends TestCase
|
||||
->see('You cannot delete the only admin');
|
||||
}
|
||||
|
||||
public function testLogout()
|
||||
public function test_logout()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
@@ -200,7 +222,7 @@ class AuthTest extends TestCase
|
||||
* @param string $password
|
||||
* @return $this
|
||||
*/
|
||||
private function login($email, $password)
|
||||
protected function login($email, $password)
|
||||
{
|
||||
return $this->visit('/login')
|
||||
->type($email, '#email')
|
||||
134
tests/Auth/LdapTest.php
Normal file
134
tests/Auth/LdapTest.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\User;
|
||||
|
||||
class LdapTest extends \TestCase
|
||||
{
|
||||
|
||||
protected $mockLdap;
|
||||
protected $mockUser;
|
||||
protected $resourceId = 'resource-test';
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
|
||||
$this->mockLdap = Mockery::mock(BookStack\Services\Ldap::class);
|
||||
$this->app['BookStack\Services\Ldap'] = $this->mockLdap;
|
||||
$this->mockUser = factory(User::class)->make();
|
||||
}
|
||||
|
||||
public function test_login()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setOption')->once();
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
|
||||
|
||||
$this->visit('/login')
|
||||
->see('Username')
|
||||
->type($this->mockUser->name, '#username')
|
||||
->type($this->mockUser->password, '#password')
|
||||
->press('Sign In')
|
||||
->seePageIs('/login')->see('Please enter an email to use for this account.');
|
||||
|
||||
$this->type($this->mockUser->email, '#email')
|
||||
->press('Sign In')
|
||||
->seePageIs('/')
|
||||
->see($this->mockUser->name)
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
|
||||
}
|
||||
|
||||
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setOption')->once();
|
||||
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => $ldapDn,
|
||||
'mail' => [$this->mockUser->email]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
|
||||
|
||||
$this->visit('/login')
|
||||
->see('Username')
|
||||
->type($this->mockUser->name, '#username')
|
||||
->type($this->mockUser->password, '#password')
|
||||
->press('Sign In')
|
||||
->seePageIs('/')
|
||||
->see($this->mockUser->name)
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
|
||||
}
|
||||
|
||||
public function test_initial_incorrect_details()
|
||||
{
|
||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||
$this->mockLdap->shouldReceive('setOption')->once();
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$this->mockUser->name],
|
||||
'cn' => [$this->mockUser->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||
]]);
|
||||
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
|
||||
|
||||
$this->visit('/login')
|
||||
->see('Username')
|
||||
->type($this->mockUser->name, '#username')
|
||||
->type($this->mockUser->password, '#password')
|
||||
->press('Sign In')
|
||||
->seePageIs('/login')->see('These credentials do not match our records.')
|
||||
->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
|
||||
}
|
||||
|
||||
public function test_create_user_form()
|
||||
{
|
||||
$this->asAdmin()->visit('/users/create')
|
||||
->dontSee('Password')
|
||||
->type($this->mockUser->name, '#name')
|
||||
->type($this->mockUser->email, '#email')
|
||||
->press('Save')
|
||||
->see('The external auth id field is required.')
|
||||
->type($this->mockUser->name, '#external_auth_id')
|
||||
->press('Save')
|
||||
->seePageIs('/users')
|
||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
|
||||
}
|
||||
|
||||
public function test_user_edit_form()
|
||||
{
|
||||
$editUser = User::all()->last();
|
||||
$this->asAdmin()->visit('/users/' . $editUser->id)
|
||||
->see('Edit User')
|
||||
->dontSee('Password')
|
||||
->type('test_auth_id', '#external_auth_id')
|
||||
->press('Save')
|
||||
->seePageIs('/users')
|
||||
->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
|
||||
}
|
||||
|
||||
public function test_registration_disabled()
|
||||
{
|
||||
$this->visit('/register')
|
||||
->seePageIs('/login');
|
||||
}
|
||||
|
||||
public function test_non_admins_cannot_change_auth_id()
|
||||
{
|
||||
$testUser = User::all()->last();
|
||||
$this->actingAs($testUser)->visit('/users/' . $testUser->id)
|
||||
->dontSee('External Authentication');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
class SocialAuthTest extends TestCase
|
||||
{
|
||||
|
||||
public function testSocialRegistration()
|
||||
public function test_social_registration()
|
||||
{
|
||||
// http://docs.mockery.io/en/latest/reference/startup_methods.html
|
||||
$user = factory(\BookStack\User::class)->make();
|
||||
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
$this->setEnvironment(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
|
||||
config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
|
||||
|
||||
$mockSocialite = Mockery::mock('Laravel\Socialite\Contracts\Factory');
|
||||
$this->app['Laravel\Socialite\Contracts\Factory'] = $mockSocialite;
|
||||
@@ -32,11 +32,4 @@ class SocialAuthTest extends TestCase
|
||||
$this->seeInDatabase('social_accounts', ['user_id' => $user->id]);
|
||||
}
|
||||
|
||||
protected function setEnvironment($array)
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
putenv("$key=$value");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\DB;
|
||||
class EntityTest extends TestCase
|
||||
{
|
||||
|
||||
public function testEntityCreation()
|
||||
public function test_entity_creation()
|
||||
{
|
||||
|
||||
// Test Creation
|
||||
@@ -51,7 +51,7 @@ class EntityTest extends TestCase
|
||||
return \BookStack\Book::find($book->id);
|
||||
}
|
||||
|
||||
public function testBookSortPageShows()
|
||||
public function test_book_sort_page_shows()
|
||||
{
|
||||
$books = \BookStack\Book::all();
|
||||
$bookToSort = $books[0];
|
||||
@@ -65,7 +65,7 @@ class EntityTest extends TestCase
|
||||
->see($books[1]->name);
|
||||
}
|
||||
|
||||
public function testBookSortItemReturnsBookContent()
|
||||
public function test_book_sort_item_returns_book_content()
|
||||
{
|
||||
$books = \BookStack\Book::all();
|
||||
$bookToSort = $books[0];
|
||||
@@ -155,7 +155,7 @@ class EntityTest extends TestCase
|
||||
return $book;
|
||||
}
|
||||
|
||||
public function testPageSearch()
|
||||
public function test_page_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->first();
|
||||
@@ -170,7 +170,7 @@ class EntityTest extends TestCase
|
||||
->seePageIs($page->getUrl());
|
||||
}
|
||||
|
||||
public function testInvalidPageSearch()
|
||||
public function test_invalid_page_search()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
@@ -180,7 +180,7 @@ class EntityTest extends TestCase
|
||||
->seeStatusCode(200);
|
||||
}
|
||||
|
||||
public function testEmptySearchRedirectsBack()
|
||||
public function test_empty_search_redirects_back()
|
||||
{
|
||||
$this->asAdmin()
|
||||
->visit('/')
|
||||
@@ -188,7 +188,7 @@ class EntityTest extends TestCase
|
||||
->seePageIs('/');
|
||||
}
|
||||
|
||||
public function testBookSearch()
|
||||
public function test_book_search()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$page = $book->pages->last();
|
||||
@@ -202,7 +202,7 @@ class EntityTest extends TestCase
|
||||
->see($chapter->name);
|
||||
}
|
||||
|
||||
public function testEmptyBookSearchRedirectsBack()
|
||||
public function test_empty_book_search_redirects_back()
|
||||
{
|
||||
$book = \BookStack\Book::all()->first();
|
||||
$this->asAdmin()
|
||||
@@ -212,7 +212,7 @@ class EntityTest extends TestCase
|
||||
}
|
||||
|
||||
|
||||
public function testEntitiesViewableAfterCreatorDeletion()
|
||||
public function test_entities_viewable_after_creator_deletion()
|
||||
{
|
||||
// Create required assets and revisions
|
||||
$creator = $this->getNewUser();
|
||||
@@ -225,7 +225,7 @@ class EntityTest extends TestCase
|
||||
$this->checkEntitiesViewable($entities);
|
||||
}
|
||||
|
||||
public function testEntitiesViewableAfterUpdaterDeletion()
|
||||
public function test_entities_viewable_after_updater_deletion()
|
||||
{
|
||||
// Create required assets and revisions
|
||||
$creator = $this->getNewUser();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class PublicViewTest extends TestCase
|
||||
{
|
||||
|
||||
public function testBooksViewable()
|
||||
public function test_books_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get();
|
||||
@@ -13,14 +13,14 @@ class PublicViewTest extends TestCase
|
||||
$this->visit('/books')
|
||||
->seeStatusCode(200)
|
||||
->see($books[0]->name)
|
||||
// Check indavidual book page is showing and it's child contents are visible.
|
||||
// Check individual book page is showing and it's child contents are visible.
|
||||
->click($bookToVisit->name)
|
||||
->seePageIs($bookToVisit->getUrl())
|
||||
->see($bookToVisit->name)
|
||||
->see($bookToVisit->chapters()->first()->name);
|
||||
}
|
||||
|
||||
public function testChaptersViewable()
|
||||
public function test_chapters_viewable()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$chapterToVisit = \BookStack\Chapter::first();
|
||||
@@ -30,7 +30,7 @@ class PublicViewTest extends TestCase
|
||||
$this->visit($chapterToVisit->getUrl())
|
||||
->seeStatusCode(200)
|
||||
->see($chapterToVisit->name)
|
||||
// Check indavidual chapter page is showing and it's child contents are visible.
|
||||
// Check individual chapter page is showing and it's child contents are visible.
|
||||
->see($pageToVisit->name)
|
||||
->click($pageToVisit->name)
|
||||
->see($chapterToVisit->book->name)
|
||||
|
||||
Reference in New Issue
Block a user