diff --git a/.gitignore b/.gitignore index 65c56ebbb..919b3e75d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ Homestead.yaml /public/bower /storage/images _ide_helper.php -/storage/debugbar \ No newline at end of file +/storage/debugbar +.phpstorm.meta.php +yarn.lock diff --git a/app/Attachment.php b/app/Attachment.php new file mode 100644 index 000000000..fe291bec2 --- /dev/null +++ b/app/Attachment.php @@ -0,0 +1,36 @@ +name, '.')) return $this->name; + return $this->name . '.' . $this->extension; + } + + /** + * Get the page this file was uploaded to. + * @return Page + */ + public function page() + { + return $this->belongsTo(Page::class, 'uploaded_to'); + } + + /** + * Get the url of this file. + * @return string + */ + public function getUrl() + { + return baseUrl('/attachments/' . $this->id); + } + +} diff --git a/app/Book.php b/app/Book.php index aa2dee9c0..91f74ca64 100644 --- a/app/Book.php +++ b/app/Book.php @@ -13,9 +13,9 @@ class Book extends Entity public function getUrl($path = false) { if ($path !== false) { - return baseUrl('/books/' . $this->slug . '/' . trim($path, '/')); + return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/')); } - return baseUrl('/books/' . $this->slug); + return baseUrl('/books/' . urlencode($this->slug)); } /* diff --git a/app/Chapter.php b/app/Chapter.php index 8f0453172..cc5518b7a 100644 --- a/app/Chapter.php +++ b/app/Chapter.php @@ -32,9 +32,9 @@ class Chapter extends Entity { $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; if ($path !== false) { - return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/')); + return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/')); } - return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug); + return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug)); } /** diff --git a/app/EmailConfirmation.php b/app/EmailConfirmation.php deleted file mode 100644 index e77b754bb..000000000 --- a/app/EmailConfirmation.php +++ /dev/null @@ -1,16 +0,0 @@ -belongsTo(User::class); - } - -} diff --git a/app/Entity.php b/app/Entity.php index 2c447814f..186059f00 100644 --- a/app/Entity.php +++ b/app/Entity.php @@ -162,18 +162,21 @@ class Entity extends Ownable $exactTerms = []; $fuzzyTerms = []; $search = static::newQuery(); + foreach ($terms as $key => $term) { - $safeTerm = htmlentities($term, ENT_QUOTES); - $safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm); - if (preg_match('/".*?"/', $safeTerm) || is_numeric($safeTerm)) { - $safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term); - $exactTerms[] = '%' . $safeTerm . '%'; + $term = htmlentities($term, ENT_QUOTES); + $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term); + if (preg_match('/".*?"/', $term) || is_numeric($term)) { + $term = str_replace('"', '', $term); + $exactTerms[] = '%' . $term . '%'; } else { - $safeTerm = '' . $safeTerm . '*'; - if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm; + $term = '' . $term . '*'; + if ($term !== '*') $fuzzyTerms[] = $term; } } - $isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0; + + $isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0; + // Perform fulltext search if relevant terms exist. if ($isFuzzy) { @@ -193,6 +196,7 @@ class Entity extends Ownable } }); } + $orderBy = $isFuzzy ? 'title_relevance' : 'updated_at'; // Add additional where terms diff --git a/app/Events/Event.php b/app/Events/Event.php deleted file mode 100644 index dfe173828..000000000 --- a/app/Events/Event.php +++ /dev/null @@ -1,8 +0,0 @@ -getPrevious()); return $message; } + + /** + * Convert an authentication exception into an unauthenticated response. + * + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception + * @return \Illuminate\Http\Response + */ + protected function unauthenticated($request, AuthenticationException $exception) + { + if ($request->expectsJson()) { + return response()->json(['error' => 'Unauthenticated.'], 401); + } + + return redirect()->guest('login'); + } } diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php new file mode 100644 index 000000000..62be0b852 --- /dev/null +++ b/app/Http/Controllers/AttachmentController.php @@ -0,0 +1,215 @@ +attachmentService = $attachmentService; + $this->attachment = $attachment; + $this->pageRepo = $pageRepo; + parent::__construct(); + } + + + /** + * Endpoint at which attachments are uploaded to. + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function upload(Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'file' => 'required|file' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId, true); + + $this->checkPermission('attachment-create-all'); + $this->checkOwnablePermission('page-update', $page); + + $uploadedFile = $request->file('file'); + + try { + $attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId); + } catch (FileUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($attachment); + } + + /** + * Update an uploaded attachment. + * @param int $attachmentId + * @param Request $request + * @return mixed + */ + public function uploadUpdate($attachmentId, Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'file' => 'required|file' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId, true); + $attachment = $this->attachment->findOrFail($attachmentId); + + $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission('attachment-create', $attachment); + + if (intval($pageId) !== intval($attachment->uploaded_to)) { + return $this->jsonError('Page mismatch during attached file update'); + } + + $uploadedFile = $request->file('file'); + + try { + $attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment); + } catch (FileUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($attachment); + } + + /** + * Update the details of an existing file. + * @param $attachmentId + * @param Request $request + * @return Attachment|mixed + */ + public function update($attachmentId, Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'name' => 'required|string|min:1|max:255', + 'link' => 'url|min:1|max:255' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId, true); + $attachment = $this->attachment->findOrFail($attachmentId); + + $this->checkOwnablePermission('page-update', $page); + $this->checkOwnablePermission('attachment-create', $attachment); + + if (intval($pageId) !== intval($attachment->uploaded_to)) { + return $this->jsonError('Page mismatch during attachment update'); + } + + $attachment = $this->attachmentService->updateFile($attachment, $request->all()); + return $attachment; + } + + /** + * Attach a link to a page. + * @param Request $request + * @return mixed + */ + public function attachLink(Request $request) + { + $this->validate($request, [ + 'uploaded_to' => 'required|integer|exists:pages,id', + 'name' => 'required|string|min:1|max:255', + 'link' => 'required|url|min:1|max:255' + ]); + + $pageId = $request->get('uploaded_to'); + $page = $this->pageRepo->getById($pageId, true); + + $this->checkPermission('attachment-create-all'); + $this->checkOwnablePermission('page-update', $page); + + $attachmentName = $request->get('name'); + $link = $request->get('link'); + $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId); + + return response()->json($attachment); + } + + /** + * Get the attachments for a specific page. + * @param $pageId + * @return mixed + */ + public function listForPage($pageId) + { + $page = $this->pageRepo->getById($pageId, true); + $this->checkOwnablePermission('page-view', $page); + return response()->json($page->attachments); + } + + /** + * Update the attachment sorting. + * @param $pageId + * @param Request $request + * @return mixed + */ + public function sortForPage($pageId, Request $request) + { + $this->validate($request, [ + 'files' => 'required|array', + 'files.*.id' => 'required|integer', + ]); + $page = $this->pageRepo->getById($pageId); + $this->checkOwnablePermission('page-update', $page); + + $attachments = $request->get('files'); + $this->attachmentService->updateFileOrderWithinPage($attachments, $pageId); + return response()->json(['message' => 'Attachment order updated']); + } + + /** + * Get an attachment from storage. + * @param $attachmentId + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response + */ + public function get($attachmentId) + { + $attachment = $this->attachment->findOrFail($attachmentId); + $page = $this->pageRepo->getById($attachment->uploaded_to); + $this->checkOwnablePermission('page-view', $page); + + if ($attachment->external) { + return redirect($attachment->path); + } + + $attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment); + return response($attachmentContents, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"' + ]); + } + + /** + * Delete a specific attachment in the system. + * @param $attachmentId + * @return mixed + */ + public function delete($attachmentId) + { + $attachment = $this->attachment->findOrFail($attachmentId); + $this->checkOwnablePermission('attachment-delete', $attachment); + $this->attachmentService->deleteFile($attachment); + return response()->json(['message' => 'Attachment deleted']); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 000000000..45e40e6fe --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,68 @@ +middleware('guest'); + parent::__construct(); + } + + + /** + * Send a reset link to the given user. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function sendResetLinkEmail(Request $request) + { + $this->validate($request, ['email' => 'required|email']); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $response = $this->broker()->sendResetLink( + $request->only('email') + ); + + if ($response === Password::RESET_LINK_SENT) { + $message = 'A password reset link has been sent to ' . $request->get('email') . '.'; + session()->flash('success', $message); + return back()->with('status', trans($response)); + } + + // If an error was returned by the password broker, we will get this message + // translated so we can notify a user of the problem. We'll redirect back + // to where the users came from so they can attempt this process again. + return back()->withErrors( + ['email' => trans($response)] + ); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 000000000..0de4a8282 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,123 @@ +middleware('guest', ['only' => ['getLogin', 'postLogin']]); + $this->socialAuthService = $socialAuthService; + $this->userRepo = $userRepo; + $this->redirectPath = baseUrl('/'); + $this->redirectAfterLogout = baseUrl('/login'); + parent::__construct(); + } + + public function username() + { + return config('auth.method') === 'standard' ? 'email' : 'username'; + } + + /** + * 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 + * @throws AuthException + */ + 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) { + + // Check for users with same email already + $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0; + if ($alreadyUser) { + throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.'); + } + + $user->save(); + $this->userRepo->attachDefaultRole($user); + auth()->login($user); + } + + $path = session()->pull('url.intended', '/'); + $path = baseUrl($path, true); + return redirect($path); + } + + /** + * Show the application login form. + * @return \Illuminate\Http\Response + */ + public function getLogin() + { + $socialDrivers = $this->socialAuthService->getActiveDrivers(); + $authMethod = config('auth.method'); + return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); + } + + /** + * Redirect to the relevant social site. + * @param $socialDriver + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function getSocialLogin($socialDriver) + { + session()->put('social-callback', 'login'); + return $this->socialAuthService->startLogIn($socialDriver); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/PasswordController.php b/app/Http/Controllers/Auth/PasswordController.php deleted file mode 100644 index 4dc6583ea..000000000 --- a/app/Http/Controllers/Auth/PasswordController.php +++ /dev/null @@ -1,76 +0,0 @@ -middleware('guest'); - } - - - /** - * Send a reset link to the given user. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response - */ - public function sendResetLinkEmail(Request $request) - { - $this->validate($request, ['email' => 'required|email']); - - $broker = $this->getBroker(); - - $response = Password::broker($broker)->sendResetLink( - $request->only('email'), $this->resetEmailBuilder() - ); - - switch ($response) { - case Password::RESET_LINK_SENT: - $message = 'A password reset link has been sent to ' . $request->get('email') . '.'; - session()->flash('success', $message); - return $this->getSendResetLinkEmailSuccessResponse($response); - - case Password::INVALID_USER: - default: - return $this->getSendResetLinkEmailFailureResponse($response); - } - } - - /** - * Get the response for after a successful password reset. - * - * @param string $response - * @return \Symfony\Component\HttpFoundation\Response - */ - protected function getResetSuccessResponse($response) - { - $message = 'Your password has been successfully reset.'; - session()->flash('success', $message); - return redirect($this->redirectPath())->with('status', trans($response)); - } -} diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/RegisterController.php similarity index 66% rename from app/Http/Controllers/Auth/AuthController.php rename to app/Http/Controllers/Auth/RegisterController.php index f2d3b2741..6bba6de04 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -1,62 +1,68 @@ -middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]); + $this->middleware('guest'); $this->socialAuthService = $socialAuthService; $this->emailConfirmationService = $emailConfirmationService; $this->userRepo = $userRepo; + $this->redirectTo = baseUrl('/'); $this->redirectPath = baseUrl('/'); - $this->redirectAfterLogout = baseUrl('/login'); - $this->username = config('auth.method') === 'standard' ? 'email' : 'username'; parent::__construct(); } /** * Get a validator for an incoming registration request. + * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ @@ -69,6 +75,10 @@ class AuthController extends Controller ]); } + /** + * Check whether or not registrations are allowed in the app settings. + * @throws UserRegistrationException + */ protected function checkRegistrationAllowed() { if (!setting('registration-enabled')) { @@ -78,7 +88,7 @@ class AuthController extends Controller /** * Show the application registration form. - * @return \Illuminate\Http\Response + * @return Response */ public function getRegister() { @@ -89,9 +99,10 @@ class AuthController extends Controller /** * Handle a registration request for the application. - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response + * @param Request|\Illuminate\Http\Request $request + * @return Response * @throws UserRegistrationException + * @throws \Illuminate\Foundation\Validation\ValidationException */ public function postRegister(Request $request) { @@ -108,66 +119,18 @@ 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 - * @throws AuthException + * Create a new user instance after a valid registration. + * @param array $data + * @return User */ - protected function authenticated(Request $request, Authenticatable $user) + protected function create(array $data) { - // 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) { - - // Check for users with same email already - $alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0; - if ($alreadyUser) { - throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.'); - } - - $user->save(); - $this->userRepo->attachDefaultRole($user); - auth()->login($user); - } - - $path = session()->pull('url.intended', '/'); - $path = baseUrl($path, true); - return redirect($path); - } - - /** - * Register a new user after a registration callback. - * @param $socialDriver - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws UserRegistrationException - */ - protected function socialRegisterCallback($socialDriver) - { - $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); - $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); - - // Create an array of the user data to create a new user instance - $userData = [ - 'name' => $socialUser->getName(), - 'email' => $socialUser->getEmail(), - 'password' => str_random(30) - ]; - return $this->registerUser($userData, $socialAccount); + return User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => bcrypt($data['password']), + ]); } /** @@ -176,7 +139,7 @@ class AuthController extends Controller * @param bool|false|SocialAccount $socialAccount * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException - * @throws \BookStack\Exceptions\ConfirmationEmailException + * @throws ConfirmationEmailException */ protected function registerUser(array $userData, $socialAccount = false) { @@ -195,7 +158,13 @@ class AuthController extends Controller if (setting('registration-confirmation') || setting('registration-restrict')) { $newUser->save(); - $this->emailConfirmationService->sendConfirmation($newUser); + + try { + $this->emailConfirmationService->sendConfirmation($newUser); + } catch (Exception $e) { + session()->flash('error', trans('auth.email_confirm_send_error')); + } + return redirect('/register/confirm'); } @@ -213,18 +182,6 @@ class AuthController extends Controller return view('auth/register-confirm'); } - /** - * View the confirmation email as a standard web page. - * @param $token - * @return \Illuminate\View\View - * @throws UserRegistrationException - */ - public function viewConfirmEmail($token) - { - $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token); - return view('emails/email-confirmation', ['token' => $confirmation->token]); - } - /** * Confirms an email via a token and logs the user into the system. * @param $token @@ -237,8 +194,8 @@ class AuthController extends Controller $user = $confirmation->user; $user->email_confirmed = true; $user->save(); - auth()->login($confirmation->user); - session()->flash('success', 'Your email has been confirmed!'); + auth()->login($user); + session()->flash('success', trans('auth.email_confirm_success')); $this->emailConfirmationService->deleteConfirmationsByUser($user); return redirect($this->redirectPath); } @@ -264,33 +221,19 @@ class AuthController extends Controller 'email' => 'required|email|exists:users,email' ]); $user = $this->userRepo->getByEmail($request->get('email')); + + try { + $this->emailConfirmationService->sendConfirmation($user); + } catch (Exception $e) { + session()->flash('error', trans('auth.email_confirm_send_error')); + return redirect('/register/confirm'); + } + $this->emailConfirmationService->sendConfirmation($user); - session()->flash('success', 'Confirmation email resent, Please check your inbox.'); + session()->flash('success', trans('auth.email_confirm_resent')); return redirect('/register/confirm'); } - /** - * Show the application login form. - * @return \Illuminate\Http\Response - */ - public function getLogin() - { - $socialDrivers = $this->socialAuthService->getActiveDrivers(); - $authMethod = config('auth.method'); - return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); - } - - /** - * Redirect to the relevant social site. - * @param $socialDriver - * @return \Symfony\Component\HttpFoundation\RedirectResponse - */ - public function getSocialLogin($socialDriver) - { - session()->put('social-callback', 'login'); - return $this->socialAuthService->startLogIn($socialDriver); - } - /** * Redirect to the social site for authentication intended to register. * @param $socialDriver @@ -334,4 +277,25 @@ class AuthController extends Controller return $this->socialAuthService->detachSocialAccount($socialDriver); } -} + /** + * Register a new user after a registration callback. + * @param $socialDriver + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws UserRegistrationException + */ + protected function socialRegisterCallback($socialDriver) + { + $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); + $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); + + // Create an array of the user data to create a new user instance + $userData = [ + 'name' => $socialUser->getName(), + 'email' => $socialUser->getEmail(), + 'password' => str_random(30) + ]; + return $this->registerUser($userData, $socialAccount); + } + + +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 000000000..bd64793f9 --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,49 @@ +middleware('guest'); + parent::__construct(); + } + + /** + * Get the response for a successful password reset. + * + * @param string $response + * @return \Illuminate\Http\Response + */ + protected function sendResetResponse($response) + { + $message = 'Your password has been successfully reset.'; + session()->flash('success', $message); + return redirect($this->redirectPath()) + ->with('status', trans($response)); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index 03ec2c110..a3fb600fd 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -115,9 +115,11 @@ class ChapterController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id); $this->checkOwnablePermission('chapter-update', $chapter); + if ($chapter->name !== $request->get('name')) { + $chapter->slug = $this->chapterRepo->findSuitableSlug($request->get('name'), $book->id, $chapter->id); + } $chapter->fill($request->all()); - $chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id); - $chapter->updated_by = auth()->user()->id; + $chapter->updated_by = user()->id; $chapter->save(); Activity::add($chapter, 'chapter_update', $book->id); return redirect($chapter->getUrl()); diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 26eeb3002..2b6c88fe0 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -3,13 +3,11 @@ namespace BookStack\Http\Controllers; use BookStack\Ownable; -use HttpRequestException; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Exception\HttpResponseException; +use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Session; use BookStack\User; abstract class Controller extends BaseController @@ -30,17 +28,21 @@ abstract class Controller extends BaseController */ public function __construct() { - // Get a user instance for the current user - $user = auth()->user(); - if (!$user) $user = User::getDefault(); + $this->middleware(function ($request, $next) { - // Share variables with views - view()->share('signedIn', auth()->check()); - view()->share('currentUser', $user); + // Get a user instance for the current user + $user = user(); - // Share variables with controllers - $this->currentUser = $user; - $this->signedIn = auth()->check(); + // Share variables with controllers + $this->currentUser = $user; + $this->signedIn = auth()->check(); + + // Share variables with views + view()->share('signedIn', $this->signedIn); + view()->share('currentUser', $user); + + return $next($request); + }); } /** @@ -67,8 +69,13 @@ abstract class Controller extends BaseController */ protected function showPermissionError() { - Session::flash('error', trans('errors.permission')); - $response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/'); + if (request()->wantsJson()) { + $response = response()->json(['error' => trans('errors.permissionJson')], 403); + } else { + $response = redirect('/'); + session()->flash('error', trans('errors.permission')); + } + throw new HttpResponseException($response); } @@ -79,7 +86,7 @@ abstract class Controller extends BaseController */ protected function checkPermission($permissionName) { - if (!$this->currentUser || !$this->currentUser->can($permissionName)) { + if (!user() || !user()->can($permissionName)) { $this->showPermissionError(); } return true; @@ -121,4 +128,22 @@ abstract class Controller extends BaseController return response()->json(['message' => $messageText], $statusCode); } + /** + * Create the response for when a request fails validation. + * + * @param \Illuminate\Http\Request $request + * @param array $errors + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function buildFailedValidationResponse(Request $request, array $errors) + { + if ($request->expectsJson()) { + return response()->json(['validation' => $errors], 422); + } + + return redirect()->to($this->getRedirectUrl()) + ->withInput($request->input()) + ->withErrors($errors, $this->errorBag()); + } + } diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 1509ace95..c2d8e257c 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -12,6 +12,7 @@ use BookStack\Repos\ChapterRepo; use BookStack\Repos\PageRepo; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Views; +use GatherContent\Htmldiff\Htmldiff; class PageController extends Controller { @@ -42,27 +43,60 @@ class PageController extends Controller /** * Show the form for creating a new page. - * @param $bookSlug - * @param bool $chapterSlug + * @param string $bookSlug + * @param string $chapterSlug * @return Response * @internal param bool $pageSlug */ - public function create($bookSlug, $chapterSlug = false) + public function create($bookSlug, $chapterSlug = null) { $book = $this->bookRepo->getBySlug($bookSlug); $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; $parent = $chapter ? $chapter : $book; $this->checkOwnablePermission('page-create', $parent); - $this->setPageTitle('Create New Page'); - $draft = $this->pageRepo->getDraftPage($book, $chapter); - return redirect($draft->getUrl()); + // Redirect to draft edit screen if signed in + if ($this->signedIn) { + $draft = $this->pageRepo->getDraftPage($book, $chapter); + return redirect($draft->getUrl()); + } + + // Otherwise show edit view + $this->setPageTitle('Create New Page'); + return view('pages/guest-create', ['parent' => $parent]); + } + + /** + * Create a new page as a guest user. + * @param Request $request + * @param string $bookSlug + * @param string|null $chapterSlug + * @return mixed + * @throws NotFoundException + */ + public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null) + { + $this->validate($request, [ + 'name' => 'required|string|max:255' + ]); + + $book = $this->bookRepo->getBySlug($bookSlug); + $chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null; + $parent = $chapter ? $chapter : $book; + $this->checkOwnablePermission('page-create', $parent); + + $page = $this->pageRepo->getDraftPage($book, $chapter); + $this->pageRepo->publishDraft($page, [ + 'name' => $request->get('name'), + 'html' => '' + ]); + return redirect($page->getUrl('/edit')); } /** * Show form to continue editing a draft page. - * @param $bookSlug - * @param $pageId + * @param string $bookSlug + * @param int $pageId * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function editDraft($bookSlug, $pageId) @@ -72,7 +106,13 @@ class PageController extends Controller $this->checkOwnablePermission('page-create', $book); $this->setPageTitle('Edit Page Draft'); - return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]); + $draftsEnabled = $this->signedIn; + return view('pages/edit', [ + 'page' => $draft, + 'book' => $book, + 'isDraft' => true, + 'draftsEnabled' => $draftsEnabled + ]); } /** @@ -112,8 +152,8 @@ class PageController extends Controller * Display the specified page. * If the page is not found via the slug the * revisions are searched for a match. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return Response */ public function show($bookSlug, $pageSlug) @@ -131,14 +171,17 @@ class PageController extends Controller $this->checkOwnablePermission('page-view', $page); $sidebarTree = $this->bookRepo->getChildren($book); + $pageNav = $this->pageRepo->getPageNav($page); + Views::add($page); $this->setPageTitle($page->getShortName()); - return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]); + return view('pages/show', ['page' => $page, 'book' => $book, + 'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]); } /** * Get page from an ajax request. - * @param $pageId + * @param int $pageId * @return \Illuminate\Http\JsonResponse */ public function getPageAjax($pageId) @@ -149,8 +192,8 @@ class PageController extends Controller /** * Show the form for editing the specified page. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return Response */ public function edit($bookSlug, $pageSlug) @@ -179,14 +222,20 @@ class PageController extends Controller if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings)); - return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]); + $draftsEnabled = $this->signedIn; + return view('pages/edit', [ + 'page' => $page, + 'book' => $book, + 'current' => $page, + 'draftsEnabled' => $draftsEnabled + ]); } /** * Update the specified page in storage. * @param Request $request - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return Response */ public function update(Request $request, $bookSlug, $pageSlug) @@ -205,13 +254,21 @@ class PageController extends Controller /** * Save a draft update as a revision. * @param Request $request - * @param $pageId + * @param int $pageId * @return \Illuminate\Http\JsonResponse */ public function saveDraft(Request $request, $pageId) { $page = $this->pageRepo->getById($pageId, true); $this->checkOwnablePermission('page-update', $page); + + if (!$this->signedIn) { + return response()->json([ + 'status' => 'error', + 'message' => 'Guests cannot save drafts', + ], 500); + } + if ($page->draft) { $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown'])); } else { @@ -230,7 +287,7 @@ class PageController extends Controller /** * Redirect from a special link url which * uses the page id rather than the name. - * @param $pageId + * @param int $pageId * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function redirectFromLink($pageId) @@ -241,8 +298,8 @@ class PageController extends Controller /** * Show the deletion page for the specified page. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\View\View */ public function showDelete($bookSlug, $pageSlug) @@ -257,8 +314,8 @@ class PageController extends Controller /** * Show the deletion page for the specified page. - * @param $bookSlug - * @param $pageId + * @param string $bookSlug + * @param int $pageId * @return \Illuminate\View\View * @throws NotFoundException */ @@ -273,8 +330,8 @@ class PageController extends Controller /** * Remove the specified page from storage. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return Response * @internal param int $id */ @@ -291,8 +348,8 @@ class PageController extends Controller /** * Remove the specified draft page from storage. - * @param $bookSlug - * @param $pageId + * @param string $bookSlug + * @param int $pageId * @return Response * @throws NotFoundException */ @@ -308,8 +365,8 @@ class PageController extends Controller /** * Shows the last revisions for this page. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\View\View */ public function showRevisions($bookSlug, $pageSlug) @@ -322,9 +379,9 @@ class PageController extends Controller /** * Shows a preview of a single revision - * @param $bookSlug - * @param $pageSlug - * @param $revisionId + * @param string $bookSlug + * @param string $pageSlug + * @param int $revisionId * @return \Illuminate\View\View */ public function showRevision($bookSlug, $pageSlug, $revisionId) @@ -332,16 +389,48 @@ class PageController extends Controller $book = $this->bookRepo->getBySlug($bookSlug); $page = $this->pageRepo->getBySlug($pageSlug, $book->id); $revision = $this->pageRepo->getRevisionById($revisionId); + $page->fill($revision->toArray()); $this->setPageTitle('Page Revision For ' . $page->getShortName()); - return view('pages/revision', ['page' => $page, 'book' => $book]); + + return view('pages/revision', [ + 'page' => $page, + 'book' => $book, + ]); + } + + /** + * Shows the changes of a single revision + * @param string $bookSlug + * @param string $pageSlug + * @param int $revisionId + * @return \Illuminate\View\View + */ + public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) + { + $book = $this->bookRepo->getBySlug($bookSlug); + $page = $this->pageRepo->getBySlug($pageSlug, $book->id); + $revision = $this->pageRepo->getRevisionById($revisionId); + + $prev = $revision->getPrevious(); + $prevContent = ($prev === null) ? '' : $prev->html; + $diff = (new Htmldiff)->diff($prevContent, $revision->html); + + $page->fill($revision->toArray()); + $this->setPageTitle('Page Revision For ' . $page->getShortName()); + + return view('pages/revision', [ + 'page' => $page, + 'book' => $book, + 'diff' => $diff, + ]); } /** * Restores a page using the content of the specified revision. - * @param $bookSlug - * @param $pageSlug - * @param $revisionId + * @param string $bookSlug + * @param string $pageSlug + * @param int $revisionId * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function restoreRevision($bookSlug, $pageSlug, $revisionId) @@ -357,8 +446,8 @@ class PageController extends Controller /** * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper. * https://github.com/barryvdh/laravel-dompdf - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\Http\Response */ public function exportPdf($bookSlug, $pageSlug) @@ -374,8 +463,8 @@ class PageController extends Controller /** * Export a page to a self-contained HTML file. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\Http\Response */ public function exportHtml($bookSlug, $pageSlug) @@ -391,8 +480,8 @@ class PageController extends Controller /** * Export a page to a simple plaintext .txt file. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\Http\Response */ public function exportPlainText($bookSlug, $pageSlug) @@ -434,8 +523,8 @@ class PageController extends Controller /** * Show the Restrictions view. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View */ public function showRestrict($bookSlug, $pageSlug) @@ -452,8 +541,8 @@ class PageController extends Controller /** * Show the view to choose a new parent to move a page into. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @return mixed * @throws NotFoundException */ @@ -470,8 +559,8 @@ class PageController extends Controller /** * Does the action of moving the location of a page - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @param Request $request * @return mixed * @throws NotFoundException @@ -513,8 +602,8 @@ class PageController extends Controller /** * Set the permissions for this page. - * @param $bookSlug - * @param $pageSlug + * @param string $bookSlug + * @param string $pageSlug * @param Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 61ce55fa9..65135eda3 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -17,10 +17,7 @@ class SettingController extends Controller $this->setPageTitle('Settings'); // Get application version - $version = false; - if (function_exists('exec')) { - $version = exec('git describe --always --tags '); - } + $version = trim(file_get_contents(base_path('version'))); return view('settings/index', ['version' => $version]); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 053d9ebd5..18ef1a671 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -3,6 +3,7 @@ namespace BookStack\Http\Controllers; use BookStack\Activity; +use Exception; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -56,7 +57,7 @@ class UserController extends Controller { $this->checkPermission('users-manage'); $authMethod = config('auth.method'); - $roles = $this->userRepo->getAssignableRoles(); + $roles = $this->userRepo->getAllRoles(); return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]); } @@ -100,9 +101,14 @@ class UserController extends Controller // Get avatar from gravatar and save if (!config('services.disable_services')) { - $avatar = \Images::saveUserGravatar($user); - $user->avatar()->associate($avatar); - $user->save(); + try { + $avatar = \Images::saveUserGravatar($user); + $user->avatar()->associate($avatar); + $user->save(); + } catch (Exception $e) { + \Log::error('Failed to save user gravatar image'); + } + } return redirect('/settings/users'); @@ -120,12 +126,13 @@ class UserController extends Controller return $this->currentUser->id == $id; }); - $authMethod = config('auth.method'); - $user = $this->user->findOrFail($id); + + $authMethod = ($user->system_name) ? 'system' : config('auth.method'); + $activeSocialDrivers = $socialAuthService->getActiveDrivers(); $this->setPageTitle('User Profile'); - $roles = $this->userRepo->getAssignableRoles(); + $roles = $this->userRepo->getAllRoles(); return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]); } @@ -180,7 +187,7 @@ class UserController extends Controller /** * Show the user delete page. - * @param $id + * @param int $id * @return \Illuminate\View\View */ public function delete($id) @@ -213,6 +220,11 @@ class UserController extends Controller return redirect($user->getEditUrl()); } + if ($user->system_name === 'public') { + session()->flash('error', 'You cannot delete the guest user'); + return redirect($user->getEditUrl()); + } + $this->userRepo->destroy($user); session()->flash('success', 'User successfully removed'); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index a1f2a581f..f1d95f5c0 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -9,15 +9,32 @@ class Kernel extends HttpKernel /** * The application's global HTTP middleware stack. * + * These middleware are run during every request to your application. + * * @var array */ protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, - \BookStack\Http\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \BookStack\Http\Middleware\VerifyCsrfToken::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array + */ + protected $middlewareGroups = [ + 'web' => [ + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \BookStack\Http\Middleware\VerifyCsrfToken::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ], + 'api' => [ + 'throttle:60,1', + 'bindings', + ], ]; /** @@ -26,6 +43,7 @@ class Kernel extends HttpKernel * @var array */ protected $routeMiddleware = [ + 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'auth' => \BookStack\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 372f30bf6..8461ed0ba 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -33,7 +33,7 @@ class Authenticate public function handle($request, Closure $next) { if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) { - return redirect()->guest(baseUrl('/register/confirm/awaiting')); + return redirect(baseUrl('/register/confirm/awaiting')); } if ($this->auth->guest() && !setting('app-public')) { diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index ab8ce4d3a..2b3c64695 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -34,7 +34,8 @@ class RedirectIfAuthenticated */ public function handle($request, Closure $next) { - if ($this->auth->check()) { + $requireConfirmation = setting('registration-confirmation'); + if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) { return redirect('/'); } diff --git a/app/Jobs/Job.php b/app/Jobs/Job.php deleted file mode 100644 index 780af746b..000000000 --- a/app/Jobs/Job.php +++ /dev/null @@ -1,21 +0,0 @@ -token = $token; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + $appName = ['appName' => setting('app-name')]; + return (new MailMessage) + ->subject(trans('auth.email_confirm_subject', $appName)) + ->greeting(trans('auth.email_confirm_greeting', $appName)) + ->line(trans('auth.email_confirm_text')) + ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token)); + } + +} diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php new file mode 100644 index 000000000..646030a10 --- /dev/null +++ b/app/Notifications/ResetPassword.php @@ -0,0 +1,50 @@ +token = $token; + } + + /** + * Get the notification's channels. + * + * @param mixed $notifiable + * @return array|string + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Build the mail representation of the notification. + * + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail() + { + return (new MailMessage) + ->line('You are receiving this email because we received a password reset request for your account.') + ->action('Reset Password', baseUrl('password/reset/' . $this->token)) + ->line('If you did not request a password reset, no further action is required.'); + } +} diff --git a/app/Page.php b/app/Page.php index 1961a4f7f..3ee9e90f4 100644 --- a/app/Page.php +++ b/app/Page.php @@ -54,6 +54,15 @@ class Page extends Entity return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc'); } + /** + * Get the attachments assigned to this page. + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function attachments() + { + return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc'); + } + /** * Get the url for this page. * @param string|bool $path @@ -63,13 +72,13 @@ class Page extends Entity { $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug; $midText = $this->draft ? '/draft/' : '/page/'; - $idComponent = $this->draft ? $this->id : $this->slug; + $idComponent = $this->draft ? $this->id : urlencode($this->slug); if ($path !== false) { - return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/')); + return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/')); } - return baseUrl('/books/' . $bookSlug . $midText . $idComponent); + return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent); } /** diff --git a/app/PageRevision.php b/app/PageRevision.php index 1ffd63dbd..ff469f0ed 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -25,11 +25,26 @@ class PageRevision extends Model /** * Get the url for this revision. + * @param null|string $path * @return string */ - public function getUrl() + public function getUrl($path = null) { - return $this->page->getUrl() . '/revisions/' . $this->id; + $url = $this->page->getUrl() . '/revisions/' . $this->id; + if ($path) return $url . '/' . trim($path, '/'); + return $url; + } + + /** + * Get the previous revision for the same page if existing + * @return \BookStack\PageRevision|null + */ + public function getPrevious() + { + if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) { + return static::find($id); + } + return null; } } diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 000000000..11e3cc6d2 --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,26 @@ +id === (int) $userId; +// }); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index f754b87a9..3802f20c0 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -21,13 +21,10 @@ class EventServiceProvider extends ServiceProvider /** * Register any other events for your application. * - * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ - public function boot(DispatcherContract $events) + public function boot() { - parent::boot($events); - - // + parent::boot(); } } diff --git a/app/Providers/PaginationServiceProvider.php b/app/Providers/PaginationServiceProvider.php index a0e97f70d..ec41267a4 100644 --- a/app/Providers/PaginationServiceProvider.php +++ b/app/Providers/PaginationServiceProvider.php @@ -1,11 +1,12 @@ app['view']; + }); + Paginator::currentPathResolver(function () { return baseUrl($this->app['request']->path()); }); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 2d9cd3b85..88ab23526 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -4,6 +4,7 @@ namespace BookStack\Providers; use Illuminate\Routing\Router; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; +use Route; class RouteServiceProvider extends ServiceProvider { @@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider /** * Define your route model bindings, pattern filters, etc. * - * @param \Illuminate\Routing\Router $router * @return void */ - public function boot(Router $router) + public function boot() { - // - - parent::boot($router); + parent::boot(); } /** * Define the routes for the application. * - * @param \Illuminate\Routing\Router $router * @return void */ - public function map(Router $router) + public function map() { - $router->group(['namespace' => $this->namespace], function ($router) { - require app_path('Http/routes.php'); + $this->mapWebRoutes(); +// $this->mapApiRoutes(); + } + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + * + * @return void + */ + protected function mapWebRoutes() + { + Route::group([ + 'middleware' => 'web', + 'namespace' => $this->namespace, + ], function ($router) { + require base_path('routes/web.php'); + }); + } + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + * + * @return void + */ + protected function mapApiRoutes() + { + Route::group([ + 'middleware' => 'api', + 'namespace' => $this->namespace, + 'prefix' => 'api', + ], function ($router) { + require base_path('routes/api.php'); }); } } diff --git a/app/Repos/BookRepo.php b/app/Repos/BookRepo.php index fdc4dd8d4..7bb91f472 100644 --- a/app/Repos/BookRepo.php +++ b/app/Repos/BookRepo.php @@ -132,8 +132,8 @@ class BookRepo extends EntityRepo { $book = $this->book->newInstance($input); $book->slug = $this->findSuitableSlug($book->name); - $book->created_by = auth()->user()->id; - $book->updated_by = auth()->user()->id; + $book->created_by = user()->id; + $book->updated_by = user()->id; $book->save(); $this->permissionService->buildJointPermissionsForEntity($book); return $book; @@ -147,9 +147,11 @@ class BookRepo extends EntityRepo */ public function updateFromInput(Book $book, $input) { + if ($book->name !== $input['name']) { + $book->slug = $this->findSuitableSlug($input['name'], $book->id); + } $book->fill($input); - $book->slug = $this->findSuitableSlug($book->name, $book->id); - $book->updated_by = auth()->user()->id; + $book->updated_by = user()->id; $book->save(); $this->permissionService->buildJointPermissionsForEntity($book); return $book; @@ -208,8 +210,7 @@ class BookRepo extends EntityRepo */ public function findSuitableSlug($name, $currentId = false) { - $slug = Str::slug($name); - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); + $slug = $this->nameToSlug($name); while ($this->doesSlugExist($slug, $currentId)) { $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); } diff --git a/app/Repos/ChapterRepo.php b/app/Repos/ChapterRepo.php index c12a9f0f2..4c13b9aaf 100644 --- a/app/Repos/ChapterRepo.php +++ b/app/Repos/ChapterRepo.php @@ -98,8 +98,8 @@ class ChapterRepo extends EntityRepo { $chapter = $this->chapter->newInstance($input); $chapter->slug = $this->findSuitableSlug($chapter->name, $book->id); - $chapter->created_by = auth()->user()->id; - $chapter->updated_by = auth()->user()->id; + $chapter->created_by = user()->id; + $chapter->updated_by = user()->id; $chapter = $book->chapters()->save($chapter); $this->permissionService->buildJointPermissionsForEntity($chapter); return $chapter; @@ -150,8 +150,7 @@ class ChapterRepo extends EntityRepo */ public function findSuitableSlug($name, $bookId, $currentId = false) { - $slug = Str::slug($name); - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); + $slug = $this->nameToSlug($name); while ($this->doesSlugExist($slug, $bookId, $currentId)) { $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); } diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index c94601738..7ecfb758c 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -132,9 +132,8 @@ class EntityRepo */ public function getUserDraftPages($count = 20, $page = 0) { - $user = auth()->user(); return $this->page->where('draft', '=', true) - ->where('created_by', '=', $user->id) + ->where('created_by', '=', user()->id) ->orderBy('updated_at', 'desc') ->skip($count * $page)->take($count)->get(); } @@ -270,6 +269,19 @@ class EntityRepo $this->permissionService->buildJointPermissionsForEntities($collection); } + /** + * Format a name as a url slug. + * @param $name + * @return string + */ + protected function nameToSlug($name) + { + $slug = str_replace(' ', '-', strtolower($name)); + $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug); + if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); + return $slug; + } + } diff --git a/app/Repos/ImageRepo.php b/app/Repos/ImageRepo.php index 435b8bbd7..8ddde7b0f 100644 --- a/app/Repos/ImageRepo.php +++ b/app/Repos/ImageRepo.php @@ -5,6 +5,7 @@ use BookStack\Image; use BookStack\Page; use BookStack\Services\ImageService; use BookStack\Services\PermissionService; +use Illuminate\Contracts\Filesystem\FileNotFoundException; use Setting; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -191,7 +192,12 @@ class ImageRepo */ public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { - return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); + try { + return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); + } catch (FileNotFoundException $exception) { + $image->delete(); + return []; + } } diff --git a/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 235246f82..e6d713f77 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -5,8 +5,10 @@ use BookStack\Book; use BookStack\Chapter; use BookStack\Entity; use BookStack\Exceptions\NotFoundException; +use BookStack\Services\AttachmentService; use Carbon\Carbon; use DOMDocument; +use DOMXPath; use Illuminate\Support\Str; use BookStack\Page; use BookStack\PageRevision; @@ -47,7 +49,7 @@ class PageRepo extends EntityRepo * Get a page via a specific ID. * @param $id * @param bool $allowDrafts - * @return mixed + * @return Page */ public function getById($id, $allowDrafts = false) { @@ -58,7 +60,7 @@ class PageRepo extends EntityRepo * Get a page identified by the given slug. * @param $slug * @param $bookId - * @return mixed + * @return Page * @throws NotFoundException */ public function getBySlug($slug, $bookId) @@ -110,31 +112,6 @@ class PageRepo extends EntityRepo return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count(); } - /** - * Save a new page into the system. - * Input validation must be done beforehand. - * @param array $input - * @param Book $book - * @param int $chapterId - * @return Page - */ - public function saveNew(array $input, Book $book, $chapterId = null) - { - $page = $this->newFromInput($input); - $page->slug = $this->findSuitableSlug($page->name, $book->id); - - if ($chapterId) $page->chapter_id = $chapterId; - - $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); - $page->created_by = auth()->user()->id; - $page->updated_by = auth()->user()->id; - - $book->pages()->save($page); - return $page; - } - - /** * Publish a draft page to make it a normal page. * Sets the slug and updates the content. @@ -172,8 +149,8 @@ class PageRepo extends EntityRepo { $page = $this->page->newInstance(); $page->name = 'New Page'; - $page->created_by = auth()->user()->id; - $page->updated_by = auth()->user()->id; + $page->created_by = user()->id; + $page->updated_by = user()->id; $page->draft = true; if ($chapter) $page->chapter_id = $chapter->id; @@ -183,6 +160,35 @@ class PageRepo extends EntityRepo return $page; } + /** + * Parse te headers on the page to get a navigation menu + * @param Page $page + * @return array + */ + public function getPageNav(Page $page) + { + if ($page->html == '') return null; + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); + + if (is_null($headers)) return null; + + $tree = []; + foreach ($headers as $header) { + $text = $header->nodeValue; + $tree[] = [ + 'nodeName' => strtolower($header->nodeName), + 'level' => intval(str_replace('h', '', $header->nodeName)), + 'link' => '#' . $header->getAttribute('id'), + 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text + ]; + } + return $tree; + } + /** * Formats a page's html to be tagged correctly * within the system. @@ -325,7 +331,7 @@ class PageRepo extends EntityRepo } // Update with new details - $userId = auth()->user()->id; + $userId = user()->id; $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = strip_tags($page->html); @@ -358,7 +364,7 @@ class PageRepo extends EntityRepo $page->fill($revision->toArray()); $page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id); $page->text = strip_tags($page->html); - $page->updated_by = auth()->user()->id; + $page->updated_by = user()->id; $page->save(); return $page; } @@ -371,21 +377,23 @@ class PageRepo extends EntityRepo */ public function saveRevision(Page $page, $summary = null) { - $revision = $this->pageRevision->fill($page->toArray()); + $revision = $this->pageRevision->newInstance($page->toArray()); if (setting('app-editor') !== 'markdown') $revision->markdown = ''; $revision->page_id = $page->id; $revision->slug = $page->slug; $revision->book_slug = $page->book->slug; - $revision->created_by = auth()->user()->id; + $revision->created_by = user()->id; $revision->created_at = $page->updated_at; $revision->type = 'version'; $revision->summary = $summary; $revision->save(); + // Clear old revisions if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { $this->pageRevision->where('page_id', '=', $page->id) ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); } + return $revision; } @@ -397,7 +405,7 @@ class PageRepo extends EntityRepo */ public function saveUpdateDraft(Page $page, $data = []) { - $userId = auth()->user()->id; + $userId = user()->id; $drafts = $this->userUpdateDraftsQuery($page, $userId)->get(); if ($drafts->count() > 0) { @@ -528,7 +536,7 @@ class PageRepo extends EntityRepo $query = $this->pageRevision->where('type', '=', 'update_draft') ->where('page_id', '=', $page->id) ->where('updated_at', '>', $page->updated_at) - ->where('created_by', '!=', auth()->user()->id) + ->where('created_by', '!=', user()->id) ->with('createdBy'); if ($minRange !== null) { @@ -541,7 +549,7 @@ class PageRepo extends EntityRepo /** * Gets a single revision via it's id. * @param $id - * @return mixed + * @return PageRevision */ public function getRevisionById($id) { @@ -606,8 +614,7 @@ class PageRepo extends EntityRepo */ public function findSuitableSlug($name, $bookId, $currentId = false) { - $slug = Str::slug($name); - if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); + $slug = $this->nameToSlug($name); while ($this->doesSlugExist($slug, $bookId, $currentId)) { $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); } @@ -626,12 +633,20 @@ class PageRepo extends EntityRepo $page->revisions()->delete(); $page->permissions()->delete(); $this->permissionService->deleteJointPermissionsForEntity($page); + + // Delete AttachedFiles + $attachmentService = app(AttachmentService::class); + foreach ($page->attachments as $attachment) { + $attachmentService->deleteFile($attachment); + } + $page->delete(); } /** * Get the latest pages added to the system. * @param $count + * @return mixed */ public function getRecentlyCreatedPaginated($count = 20) { @@ -641,6 +656,7 @@ class PageRepo extends EntityRepo /** * Get the latest pages added to the system. * @param $count + * @return mixed */ public function getRecentlyUpdatedPaginated($count = 20) { diff --git a/app/Repos/PermissionsRepo.php b/app/Repos/PermissionsRepo.php index e026d83e8..24497c911 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Repos/PermissionsRepo.php @@ -35,7 +35,7 @@ class PermissionsRepo */ public function getAllRoles() { - return $this->role->where('hidden', '=', false)->get(); + return $this->role->all(); } /** @@ -45,7 +45,7 @@ class PermissionsRepo */ public function getAllRolesExcept(Role $role) { - return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get(); + return $this->role->where('id', '!=', $role->id)->get(); } /** @@ -90,8 +90,6 @@ class PermissionsRepo { $role = $this->role->findOrFail($roleId); - if ($role->hidden) throw new PermissionsException("Cannot update a hidden role"); - $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; $this->assignRolePermissions($role, $permissions); diff --git a/app/Repos/UserRepo.php b/app/Repos/UserRepo.php index 0926f6304..ab3716fca 100644 --- a/app/Repos/UserRepo.php +++ b/app/Repos/UserRepo.php @@ -2,6 +2,7 @@ use BookStack\Role; use BookStack\User; +use Exception; use Setting; class UserRepo @@ -84,9 +85,14 @@ class UserRepo // Get avatar from gravatar and save if (!config('services.disable_services')) { - $avatar = \Images::saveUserGravatar($user); - $user->avatar()->associate($avatar); - $user->save(); + try { + $avatar = \Images::saveUserGravatar($user); + $user->avatar()->associate($avatar); + $user->save(); + } catch (Exception $e) { + $user->save(); + \Log::error('Failed to save user gravatar image'); + } } return $user; @@ -193,9 +199,9 @@ class UserRepo * Get the roles in the system that are assignable to a user. * @return mixed */ - public function getAssignableRoles() + public function getAllRoles() { - return $this->role->visible(); + return $this->role->all(); } /** @@ -205,7 +211,7 @@ class UserRepo */ public function getRestrictableRoles() { - return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get(); + return $this->role->where('system_name', '!=', 'admin')->get(); } } \ No newline at end of file diff --git a/app/Role.php b/app/Role.php index 8d0a79e75..bf9685ee2 100644 --- a/app/Role.php +++ b/app/Role.php @@ -66,7 +66,7 @@ class Role extends Model /** * Get the role object for the specified role. * @param $roleName - * @return mixed + * @return Role */ public static function getRole($roleName) { @@ -76,7 +76,7 @@ class Role extends Model /** * Get the role object for the specified system role. * @param $roleName - * @return mixed + * @return Role */ public static function getSystemRole($roleName) { diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index f6fea33a1..e41036238 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -19,7 +19,7 @@ class ActivityService { $this->activity = $activity; $this->permissionService = $permissionService; - $this->user = auth()->user(); + $this->user = user(); } /** diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php new file mode 100644 index 000000000..e0ee3a04d --- /dev/null +++ b/app/Services/AttachmentService.php @@ -0,0 +1,201 @@ +getStorageBasePath() . $attachment->path; + return $this->getStorage()->get($attachmentPath); + } + + /** + * Store a new attachment upon user upload. + * @param UploadedFile $uploadedFile + * @param int $page_id + * @return Attachment + * @throws FileUploadException + */ + public function saveNewUpload(UploadedFile $uploadedFile, $page_id) + { + $attachmentName = $uploadedFile->getClientOriginalName(); + $attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile); + $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order'); + + $attachment = Attachment::forceCreate([ + 'name' => $attachmentName, + 'path' => $attachmentPath, + 'extension' => $uploadedFile->getClientOriginalExtension(), + 'uploaded_to' => $page_id, + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1 + ]); + + return $attachment; + } + + /** + * Store a upload, saving to a file and deleting any existing uploads + * attached to that file. + * @param UploadedFile $uploadedFile + * @param Attachment $attachment + * @return Attachment + * @throws FileUploadException + */ + public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment) + { + if (!$attachment->external) { + $this->deleteFileInStorage($attachment); + } + + $attachmentName = $uploadedFile->getClientOriginalName(); + $attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile); + + $attachment->name = $attachmentName; + $attachment->path = $attachmentPath; + $attachment->external = false; + $attachment->extension = $uploadedFile->getClientOriginalExtension(); + $attachment->save(); + return $attachment; + } + + /** + * Save a new File attachment from a given link and name. + * @param string $name + * @param string $link + * @param int $page_id + * @return Attachment + */ + public function saveNewFromLink($name, $link, $page_id) + { + $largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order'); + return Attachment::forceCreate([ + 'name' => $name, + 'path' => $link, + 'external' => true, + 'extension' => '', + 'uploaded_to' => $page_id, + 'created_by' => user()->id, + 'updated_by' => user()->id, + 'order' => $largestExistingOrder + 1 + ]); + } + + /** + * Get the file storage base path, amended for storage type. + * This allows us to keep a generic path in the database. + * @return string + */ + private function getStorageBasePath() + { + return $this->isLocal() ? 'storage/' : ''; + } + + /** + * Updates the file ordering for a listing of attached files. + * @param array $attachmentList + * @param $pageId + */ + public function updateFileOrderWithinPage($attachmentList, $pageId) + { + foreach ($attachmentList as $index => $attachment) { + Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]); + } + } + + + /** + * Update the details of a file. + * @param Attachment $attachment + * @param $requestData + * @return Attachment + */ + public function updateFile(Attachment $attachment, $requestData) + { + $attachment->name = $requestData['name']; + if (isset($requestData['link']) && trim($requestData['link']) !== '') { + $attachment->path = $requestData['link']; + if (!$attachment->external) { + $this->deleteFileInStorage($attachment); + $attachment->external = true; + } + } + $attachment->save(); + return $attachment; + } + + /** + * Delete a File from the database and storage. + * @param Attachment $attachment + */ + public function deleteFile(Attachment $attachment) + { + if ($attachment->external) { + $attachment->delete(); + return; + } + + $this->deleteFileInStorage($attachment); + $attachment->delete(); + } + + /** + * Delete a file from the filesystem it sits on. + * Cleans any empty leftover folders. + * @param Attachment $attachment + */ + protected function deleteFileInStorage(Attachment $attachment) + { + $storedFilePath = $this->getStorageBasePath() . $attachment->path; + $storage = $this->getStorage(); + $dirPath = dirname($storedFilePath); + + $storage->delete($storedFilePath); + if (count($storage->allFiles($dirPath)) === 0) { + $storage->deleteDirectory($dirPath); + } + } + + /** + * Store a file in storage with the given filename + * @param $attachmentName + * @param UploadedFile $uploadedFile + * @return string + * @throws FileUploadException + */ + protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile) + { + $attachmentData = file_get_contents($uploadedFile->getRealPath()); + + $storage = $this->getStorage(); + $attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/'; + $storageBasePath = $this->getStorageBasePath() . $attachmentBasePath; + + $uploadFileName = $attachmentName; + while ($storage->exists($storageBasePath . $uploadFileName)) { + $uploadFileName = str_random(3) . $uploadFileName; + } + + $attachmentPath = $attachmentBasePath . $uploadFileName; + $attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath; + + try { + $storage->put($attachmentStoragePath, $attachmentData); + } catch (Exception $e) { + throw new FileUploadException('File path ' . $attachmentStoragePath . ' could not be uploaded to. Ensure it is writable to the server.'); + } + return $attachmentPath; + } + +} \ No newline at end of file diff --git a/app/Services/EmailConfirmationService.php b/app/Services/EmailConfirmationService.php index c3096c654..d4ec1e976 100644 --- a/app/Services/EmailConfirmationService.php +++ b/app/Services/EmailConfirmationService.php @@ -1,30 +1,27 @@ mailer = $mailer; - $this->emailConfirmation = $emailConfirmation; + $this->db = $db; + $this->users = $users; } /** @@ -38,16 +35,28 @@ class EmailConfirmationService if ($user->email_confirmed) { throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login'); } + $this->deleteConfirmationsByUser($user); + $token = $this->createEmailConfirmation($user); + + $user->notify(new ConfirmEmail($token)); + } + + /** + * Creates a new email confirmation in the database and returns the token. + * @param User $user + * @return string + */ + public function createEmailConfirmation(User $user) + { $token = $this->getToken(); - $this->emailConfirmation->create([ + $this->db->table('email_confirmations')->insert([ 'user_id' => $user->id, - 'token' => $token, + 'token' => $token, + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now() ]); - $this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) { - $appName = setting('app-name', 'BookStack'); - $message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.'); - }); + return $token; } /** @@ -59,22 +68,24 @@ class EmailConfirmationService */ public function getEmailConfirmationFromToken($token) { - $emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first(); - // If not found + $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first(); + + // If not found show error if ($emailConfirmation === null) { throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register'); } // If more than a day old - if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) { - $this->sendConfirmation($emailConfirmation->user); + if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) { + $user = $this->users->getById($emailConfirmation->user_id); + $this->sendConfirmation($user); throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm'); } + $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id); return $emailConfirmation; } - /** * Delete all email confirmations that belong to a user. * @param User $user @@ -82,7 +93,7 @@ class EmailConfirmationService */ public function deleteConfirmationsByUser(User $user) { - return $this->emailConfirmation->where('user_id', '=', $user->id)->delete(); + return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete(); } /** @@ -92,7 +103,7 @@ class EmailConfirmationService protected function getToken() { $token = str_random(24); - while ($this->emailConfirmation->where('token', '=', $token)->exists()) { + while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) { $token = str_random(25); } return $token; diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php index 4401cb230..dfe2cf453 100644 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@ -9,20 +9,13 @@ use Intervention\Image\ImageManager; use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; use Illuminate\Contracts\Cache\Repository as Cache; -use Setting; use Symfony\Component\HttpFoundation\File\UploadedFile; -class ImageService +class ImageService extends UploadService { protected $imageTool; - protected $fileSystem; protected $cache; - - /** - * @var FileSystemInstance - */ - protected $storageInstance; protected $storageUrl; /** @@ -34,8 +27,8 @@ class ImageService public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache) { $this->imageTool = $imageTool; - $this->fileSystem = $fileSystem; $this->cache = $cache; + parent::__construct($fileSystem); } /** @@ -88,6 +81,9 @@ class ImageService if ($secureUploads) $imageName = str_random(16) . '-' . $imageName; $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; + + if ($this->isLocal()) $imagePath = '/public' . $imagePath; + while ($storage->exists($imagePath . $imageName)) { $imageName = str_random(3) . $imageName; } @@ -100,6 +96,8 @@ class ImageService throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); } + if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath); + $imageDetails = [ 'name' => $imageName, 'path' => $fullPath, @@ -108,8 +106,8 @@ class ImageService 'uploaded_to' => $uploadedTo ]; - if (auth()->user() && auth()->user()->id !== 0) { - $userId = auth()->user()->id; + if (user()->id !== 0) { + $userId = user()->id; $imageDetails['created_by'] = $userId; $imageDetails['updated_by'] = $userId; } @@ -119,6 +117,16 @@ class ImageService return $image; } + /** + * Get the storage path, Dependant of storage type. + * @param Image $image + * @return mixed|string + */ + protected function getPath(Image $image) + { + return ($this->isLocal()) ? ('public/' . $image->path) : $image->path; + } + /** * Get the thumbnail for an image. * If $keepRatio is true only the width will be used. @@ -135,7 +143,8 @@ class ImageService public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { $thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/'; - $thumbFilePath = dirname($image->path) . $thumbDirName . basename($image->path); + $imagePath = $this->getPath($image); + $thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath); if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) { return $this->getPublicUrl($thumbFilePath); @@ -148,7 +157,7 @@ class ImageService } try { - $thumb = $this->imageTool->make($storage->get($image->path)); + $thumb = $this->imageTool->make($storage->get($imagePath)); } 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.'); @@ -183,8 +192,8 @@ class ImageService { $storage = $this->getStorage(); - $imageFolder = dirname($image->path); - $imageFileName = basename($image->path); + $imageFolder = dirname($this->getPath($image)); + $imageFileName = basename($this->getPath($image)); $allImages = collect($storage->allFiles($imageFolder)); $imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) { @@ -213,7 +222,7 @@ class ImageService public function saveUserGravatar(User $user, $size = 500) { $emailHash = md5(strtolower(trim($user->email))); - $url = 'http://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; + $url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); $image = $this->saveNewFromUrl($url, 'user', $imageName); $image->created_by = $user->id; @@ -222,35 +231,9 @@ class ImageService return $image; } - /** - * Get the storage that will be used for storing images. - * @return FileSystemInstance - */ - private function getStorage() - { - if ($this->storageInstance !== null) return $this->storageInstance; - - $storageType = config('filesystems.default'); - $this->storageInstance = $this->fileSystem->disk($storageType); - - return $this->storageInstance; - } - - /** - * Check whether or not a folder is empty. - * @param $path - * @return int - */ - private function isFolderEmpty($path) - { - $files = $this->getStorage()->files($path); - $folders = $this->getStorage()->directories($path); - return count($files) === 0 && count($folders) === 0; - } - /** * Gets a public facing url for an image by checking relevant environment variables. - * @param $filePath + * @param string $filePath * @return string */ private function getPublicUrl($filePath) @@ -273,6 +256,8 @@ class ImageService $this->storageUrl = $storageUrl; } + if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath); + return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; } diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index cee074cd7..bb78f0b0a 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -9,14 +9,15 @@ use BookStack\Page; use BookStack\Role; use BookStack\User; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; class PermissionService { - protected $userRoles; - protected $isAdmin; protected $currentAction; - protected $currentUser; + protected $isAdminUser; + protected $userRoles = false; + protected $currentUserModel = false; public $book; public $chapter; @@ -37,12 +38,6 @@ class PermissionService */ public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role) { - $this->currentUser = auth()->user(); - $userSet = $this->currentUser !== null; - $this->userRoles = false; - $this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false; - if (!$userSet) $this->currentUser = new User(); - $this->jointPermission = $jointPermission; $this->role = $role; $this->book = $book; @@ -117,7 +112,7 @@ class PermissionService } - foreach ($this->currentUser->roles as $role) { + foreach ($this->currentUser()->roles as $role) { $roles[] = $role->id; } return $roles; @@ -389,7 +384,11 @@ class PermissionService */ public function checkOwnableUserAccess(Ownable $ownable, $permission) { - if ($this->isAdmin) return true; + if ($this->isAdmin()) { + $this->clean(); + return true; + } + $explodedPermission = explode('-', $permission); $baseQuery = $ownable->where('id', '=', $ownable->id); @@ -400,10 +399,10 @@ class PermissionService // Handle non entity specific jointPermissions if (in_array($explodedPermission[0], $nonJointPermissions)) { - $allPermission = $this->currentUser && $this->currentUser->can($permission . '-all'); - $ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own'); + $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all'); + $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own'); $this->currentAction = 'view'; - $isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by; + $isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by; return ($allPermission || ($isOwner && $ownPermission)); } @@ -413,7 +412,9 @@ class PermissionService } - return $this->entityRestrictionQuery($baseQuery)->count() > 0; + $q = $this->entityRestrictionQuery($baseQuery)->count() > 0; + $this->clean(); + return $q; } /** @@ -443,7 +444,7 @@ class PermissionService */ protected function entityRestrictionQuery($query) { - return $query->where(function ($parentQuery) { + $q = $query->where(function ($parentQuery) { $parentQuery->whereHas('jointPermissions', function ($permissionQuery) { $permissionQuery->whereIn('role_id', $this->getRoles()) ->where('action', '=', $this->currentAction) @@ -451,11 +452,13 @@ class PermissionService $query->where('has_permission', '=', true) ->orWhere(function ($query) { $query->where('has_permission_own', '=', true) - ->where('created_by', '=', $this->currentUser->id); + ->where('created_by', '=', $this->currentUser()->id); }); }); }); }); + $this->clean(); + return $q; } /** @@ -469,9 +472,9 @@ class PermissionService // Prevent drafts being visible to others. $query = $query->where(function ($query) { $query->where('draft', '=', false); - if ($this->currentUser) { + if ($this->currentUser()) { $query->orWhere(function ($query) { - $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id); + $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id); }); } }); @@ -509,7 +512,10 @@ class PermissionService */ public function enforceEntityRestrictions($query, $action = 'view') { - if ($this->isAdmin) return $query; + if ($this->isAdmin()) { + $this->clean(); + return $query; + } $this->currentAction = $action; return $this->entityRestrictionQuery($query); } @@ -524,11 +530,15 @@ class PermissionService */ public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn) { - if ($this->isAdmin) return $query; + if ($this->isAdmin()) { + $this->clean(); + return $query; + } + $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; - return $query->where(function ($query) use ($tableDetails) { + $q = $query->where(function ($query) use ($tableDetails) { $query->whereExists(function ($permissionQuery) use (&$tableDetails) { $permissionQuery->select('id')->from('joint_permissions') ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) @@ -538,12 +548,12 @@ class PermissionService ->where(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission_own', '=', true) - ->where('created_by', '=', $this->currentUser->id); + ->where('created_by', '=', $this->currentUser()->id); }); }); }); }); - + return $q; } /** @@ -555,11 +565,15 @@ class PermissionService */ public function filterRelatedPages($query, $tableName, $entityIdColumn) { - if ($this->isAdmin) return $query; + if ($this->isAdmin()) { + $this->clean(); + return $query; + } + $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - return $query->where(function ($query) use ($tableDetails) { + $q = $query->where(function ($query) use ($tableDetails) { $query->where(function ($query) use (&$tableDetails) { $query->whereExists(function ($permissionQuery) use (&$tableDetails) { $permissionQuery->select('id')->from('joint_permissions') @@ -570,12 +584,50 @@ class PermissionService ->where(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission_own', '=', true) - ->where('created_by', '=', $this->currentUser->id); + ->where('created_by', '=', $this->currentUser()->id); }); }); }); })->orWhere($tableDetails['entityIdColumn'], '=', 0); }); + $this->clean(); + return $q; + } + + /** + * Check if the current user is an admin. + * @return bool + */ + private function isAdmin() + { + if ($this->isAdminUser === null) { + $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasRole('admin') : false; + } + + return $this->isAdminUser; + } + + /** + * Get the current user + * @return User + */ + private function currentUser() + { + if ($this->currentUserModel === false) { + $this->currentUserModel = user(); + } + + return $this->currentUserModel; + } + + /** + * Clean the cached user elements. + */ + private function clean() + { + $this->currentUserModel = false; + $this->userRoles = false; + $this->isAdminUser = null; } } \ No newline at end of file diff --git a/app/Services/SocialAuthService.php b/app/Services/SocialAuthService.php index b28a97ea4..d76a7231b 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Services/SocialAuthService.php @@ -100,7 +100,7 @@ class SocialAuthService $socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first(); $user = $this->userRepo->getByEmail($socialUser->getEmail()); $isLoggedIn = auth()->check(); - $currentUser = auth()->user(); + $currentUser = user(); // When a user is not logged in and a matching SocialAccount exists, // Simply log the user into the application. @@ -214,9 +214,9 @@ class SocialAuthService public function detachSocialAccount($socialDriver) { session(); - auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); + user()->socialAccounts()->where('driver', '=', $socialDriver)->delete(); session()->flash('success', title_case($socialDriver) . ' account successfully detached'); - return redirect(auth()->user()->getEditUrl()); + return redirect(user()->getEditUrl()); } } \ No newline at end of file diff --git a/app/Services/UploadService.php b/app/Services/UploadService.php new file mode 100644 index 000000000..44d4bb4f7 --- /dev/null +++ b/app/Services/UploadService.php @@ -0,0 +1,64 @@ +fileSystem = $fileSystem; + } + + /** + * Get the storage that will be used for storing images. + * @return FileSystemInstance + */ + protected function getStorage() + { + if ($this->storageInstance !== null) return $this->storageInstance; + + $storageType = config('filesystems.default'); + $this->storageInstance = $this->fileSystem->disk($storageType); + + return $this->storageInstance; + } + + + /** + * Check whether or not a folder is empty. + * @param $path + * @return bool + */ + protected function isFolderEmpty($path) + { + $files = $this->getStorage()->files($path); + $folders = $this->getStorage()->directories($path); + return (count($files) === 0 && count($folders) === 0); + } + + /** + * Check if using a local filesystem. + * @return bool + */ + protected function isLocal() + { + return strtolower(config('filesystems.default')) === 'local'; + } +} \ No newline at end of file diff --git a/app/Services/ViewService.php b/app/Services/ViewService.php index aac9831f7..1a9ee5f70 100644 --- a/app/Services/ViewService.php +++ b/app/Services/ViewService.php @@ -18,7 +18,7 @@ class ViewService public function __construct(View $view, PermissionService $permissionService) { $this->view = $view; - $this->user = auth()->user(); + $this->user = user(); $this->permissionService = $permissionService; } @@ -84,7 +84,7 @@ class ViewService ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel)); - $query = $query->where('user_id', '=', auth()->user()->id); + $query = $query->where('user_id', '=', user()->id); $viewables = $query->with('viewable')->orderBy('updated_at', 'desc') ->skip($count * $page)->take($count)->get()->pluck('viewable'); diff --git a/app/User.php b/app/User.php index 32449971d..09b189cbb 100644 --- a/app/User.php +++ b/app/User.php @@ -1,13 +1,16 @@ 'guest', - 'name' => 'Guest' - ]); + return static::where('system_name', '=', 'public')->first(); + } + + /** + * Check if the user is the default public user. + * @return bool + */ + public function isDefault() + { + return $this->system_name === 'public'; } /** * The roles that belong to the user. + * @return BelongsToMany */ public function roles() { + if ($this->id === 0) return ; return $this->belongsToMany(Role::class); } @@ -183,4 +195,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return ''; } + + /** + * Send the password reset notification. + * @param string $token + * @return void + */ + public function sendPasswordResetNotification($token) + { + $this->notify(new ResetPassword($token)); + } } diff --git a/app/helpers.php b/app/helpers.php index c12708877..b5be0fd11 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -11,29 +11,30 @@ use BookStack\Ownable; */ function versioned_asset($file = '') { - // Don't require css and JS assets for testing - if (config('app.env') === 'testing') return ''; + static $version = null; - static $manifest = null; - $manifestPath = 'build/manifest.json'; - - if (is_null($manifest) && file_exists($manifestPath)) { - $manifest = json_decode(file_get_contents(public_path($manifestPath)), true); - } else if (!file_exists($manifestPath)) { - if (config('app.env') !== 'production') { - $path = public_path($manifestPath); - $error = "No {$path} file found, Ensure you have built the css/js assets using gulp."; - } else { - $error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack"; - } - throw new \Exception($error); + if (is_null($version)) { + $versionFile = base_path('version'); + $version = trim(file_get_contents($versionFile)); } - if (isset($manifest[$file])) { - return baseUrl($manifest[$file]); + $additional = ''; + if (config('app.env') === 'development') { + $additional = sha1_file(public_path($file)); } - throw new InvalidArgumentException("File {$file} not defined in asset manifest."); + $path = $file . '?version=' . urlencode($version) . $additional; + return baseUrl($path); +} + +/** + * Helper method to get the current User. + * Defaults to public 'Guest' user if not logged in. + * @return \BookStack\User + */ +function user() +{ + return auth()->user() ?: \BookStack\User::getDefault(); } /** @@ -47,7 +48,7 @@ function versioned_asset($file = '') function userCan($permission, Ownable $ownable = null) { if ($ownable === null) { - return auth()->user() && auth()->user()->can($permission); + return user() && user()->can($permission); } // Check permission on ownable item @@ -63,7 +64,7 @@ function userCan($permission, Ownable $ownable = null) */ function setting($key, $default = false) { - $settingService = app('BookStack\Services\SettingService'); + $settingService = app(\BookStack\Services\SettingService::class); return $settingService->get($key, $default); } @@ -79,6 +80,7 @@ function baseUrl($path, $forceAppDomain = false) if ($isFullUrl && !$forceAppDomain) return $path; $path = trim($path, '/'); + // Remove non-specified domain if forced and we have a domain if ($isFullUrl && $forceAppDomain) { $explodedPath = explode('/', $path); $path = implode('/', array_splice($explodedPath, 3)); @@ -127,14 +129,14 @@ function sortUrl($path, $data, $overrideData = []) { $queryStringSections = []; $queryData = array_merge($data, $overrideData); - + // Change sorting direction is already sorted on current attribute if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) { $queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc'; } else { $queryData['order'] = 'asc'; } - + foreach ($queryData as $name => $value) { $trimmedVal = trim($value); if ($trimmedVal === '') continue; @@ -144,4 +146,4 @@ function sortUrl($path, $data, $overrideData = []) if (count($queryStringSections) === 0) return $path; return baseUrl($path . '?' . implode('&', $queryStringSections)); -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 5c77a68c4..7d4b5e62b 100644 --- a/composer.json +++ b/composer.json @@ -5,23 +5,24 @@ "license": "MIT", "type": "project", "require": { - "php": ">=5.5.9", - "laravel/framework": "5.2.*", + "php": ">=5.6.4", + "laravel/framework": "^5.3.4", + "ext-tidy": "*", "intervention/image": "^2.3", "laravel/socialite": "^2.0", "barryvdh/laravel-ide-helper": "^2.1", - "barryvdh/laravel-debugbar": "^2.0", + "barryvdh/laravel-debugbar": "^2.2.3", "league/flysystem-aws-s3-v3": "^1.0", - "barryvdh/laravel-dompdf": "0.6.*", - "predis/predis": "^1.0" + "barryvdh/laravel-dompdf": "^0.7", + "predis/predis": "^1.1", + "gathercontent/htmldiff": "^0.2.1" }, "require-dev": { "fzaninotto/faker": "~1.4", "mockery/mockery": "0.9.*", - "phpunit/phpunit": "~4.0", - "phpspec/phpspec": "~2.1", - "symfony/dom-crawler": "~3.0", - "symfony/css-selector": "~3.0" + "phpunit/phpunit": "~5.0", + "symfony/css-selector": "3.1.*", + "symfony/dom-crawler": "3.1.*" }, "autoload": { "classmap": [ @@ -37,21 +38,19 @@ ] }, "scripts": { - "post-install-cmd": [ - "php artisan clear-compiled", - "php artisan optimize" - ], - "pre-update-cmd": [ - "php artisan clear-compiled" - ], - "post-update-cmd": [ - "php artisan optimize" - ], "post-root-package-install": [ - "php -r \"copy('.env.example', '.env');\"" + "php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "php artisan key:generate" + ], + "post-install-cmd": [ + "Illuminate\\Foundation\\ComposerScripts::postInstall", + "php artisan optimize" + ], + "post-update-cmd": [ + "Illuminate\\Foundation\\ComposerScripts::postUpdate", + "php artisan optimize" ] }, "config": { diff --git a/composer.lock b/composer.lock index 63d378753..74a090288 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "eb7c71e9ed116d3fd2a1d0af07f9f134", - "content-hash": "17d2d7fc5fed682f2a290d6588538035", + "hash": "3124d900cfe857392a94de479f3ff6d4", + "content-hash": "a968767a73f77e66e865c276cf76eedf", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.17.5", + "version": "3.19.11", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "1cef9b334729b3564c9aef15481a55561c54b53f" + "reference": "19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1cef9b334729b3564c9aef15481a55561c54b53f", - "reference": "1cef9b334729b3564c9aef15481a55561c54b53f", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8", + "reference": "19bac3bdd7988cbf7f89d5ce8e2748d774e2cde8", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "~5.3|~6.0.1|~6.1", + "guzzlehttp/guzzle": "^5.3.1|^6.2.1", "guzzlehttp/promises": "~1.0", - "guzzlehttp/psr7": "~1.0", + "guzzlehttp/psr7": "~1.3.1", "mtdowling/jmespath.php": "~2.2", "php": ">=5.5" }, @@ -85,32 +85,32 @@ "s3", "sdk" ], - "time": "2016-04-07 22:44:13" + "time": "2016-09-27 19:38:36" }, { "name": "barryvdh/laravel-debugbar", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "13b7058d2120c8d5af7f1ada21b7c44dd87b666a" + "reference": "0c87981df959c7c1943abe227baf607c92f204f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/13b7058d2120c8d5af7f1ada21b7c44dd87b666a", - "reference": "13b7058d2120c8d5af7f1ada21b7c44dd87b666a", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/0c87981df959c7c1943abe227baf607c92f204f9", + "reference": "0c87981df959c7c1943abe227baf607c92f204f9", "shasum": "" }, "require": { - "illuminate/support": "5.1.*|5.2.*", - "maximebf/debugbar": "~1.11.0", + "illuminate/support": "5.1.*|5.2.*|5.3.*", + "maximebf/debugbar": "~1.13.0", "php": ">=5.5.9", "symfony/finder": "~2.7|~3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -139,31 +139,31 @@ "profiler", "webprofiler" ], - "time": "2016-02-17 08:32:21" + "time": "2016-09-15 14:05:56" }, { "name": "barryvdh/laravel-dompdf", - "version": "v0.6.1", + "version": "v0.7.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-dompdf.git", - "reference": "b606788108833f7765801dca35455fb23ce9f869" + "reference": "9b8bd179262ad6b200a11edfe7b53516afcfc42a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/b606788108833f7765801dca35455fb23ce9f869", - "reference": "b606788108833f7765801dca35455fb23ce9f869", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/9b8bd179262ad6b200a11edfe7b53516afcfc42a", + "reference": "9b8bd179262ad6b200a11edfe7b53516afcfc42a", "shasum": "" }, "require": { - "dompdf/dompdf": "0.6.*", - "illuminate/support": "5.0.x|5.1.x|5.2.x", - "php": ">=5.4.0" + "dompdf/dompdf": "^0.7", + "illuminate/support": "5.1.x|5.2.x|5.3.x", + "php": ">=5.5.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.6-dev" + "dev-master": "0.7-dev" } }, "autoload": { @@ -187,32 +187,35 @@ "laravel", "pdf" ], - "time": "2015-12-21 19:51:22" + "time": "2016-08-17 08:17:33" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.1.4", + "version": "v2.2.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "f1ebd847aac9a4545325d35108cafc285fe1605f" + "reference": "28af7cd19ca41cc0c63dd1de2b46c2b84d31c463" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/f1ebd847aac9a4545325d35108cafc285fe1605f", - "reference": "f1ebd847aac9a4545325d35108cafc285fe1605f", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/28af7cd19ca41cc0c63dd1de2b46c2b84d31c463", + "reference": "28af7cd19ca41cc0c63dd1de2b46c2b84d31c463", "shasum": "" }, "require": { - "illuminate/console": "5.0.x|5.1.x|5.2.x", - "illuminate/filesystem": "5.0.x|5.1.x|5.2.x", - "illuminate/support": "5.0.x|5.1.x|5.2.x", + "barryvdh/reflection-docblock": "^2.0.4", + "illuminate/console": "^5.0,<5.4", + "illuminate/filesystem": "^5.0,<5.4", + "illuminate/support": "^5.0,<5.4", "php": ">=5.4.0", - "phpdocumentor/reflection-docblock": "^2.0.4", - "symfony/class-loader": "~2.3|~3.0" + "symfony/class-loader": "^2.3|^3.0" }, "require-dev": { - "doctrine/dbal": "~2.3" + "doctrine/dbal": "~2.3", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1", + "squizlabs/php_codesniffer": "~2.3" }, "suggest": { "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" @@ -220,7 +223,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" } }, "autoload": { @@ -250,7 +253,56 @@ "phpstorm", "sublime" ], - "time": "2016-03-03 08:45:00" + "time": "2016-07-04 11:52:48" + }, + { + "name": "barryvdh/reflection-docblock", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/ReflectionDocBlock.git", + "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/3dcbd98b5d9384a5357266efba8fd29884458e5c", + "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0,<4.5" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Barryvdh": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2016-06-13 19:28:20" }, { "name": "classpreloader/classpreloader", @@ -306,6 +358,57 @@ ], "time": "2015-11-09 22:51:51" }, + { + "name": "cogpowered/finediff", + "version": "0.3.1", + "source": { + "type": "git", + "url": "https://github.com/cogpowered/FineDiff.git", + "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cogpowered/FineDiff/zipball/339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8", + "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "mockery/mockery": "*", + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "cogpowered\\FineDiff": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Crowe", + "email": "rob@cogpowered.com" + }, + { + "name": "Raymond Hill" + } + ], + "description": "PHP implementation of a Fine granularity Diff engine", + "homepage": "https://github.com/cogpowered/FineDiff", + "keywords": [ + "diff", + "finediff", + "opcode", + "string", + "text" + ], + "time": "2014-05-19 10:25:02" + }, { "name": "dnoegel/php-xdg-base-dir", "version": "0.1", @@ -408,30 +511,46 @@ }, { "name": "dompdf/dompdf", - "version": "v0.6.2", + "version": "v0.7.0", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "cc06008f75262510ee135b8cbb14e333a309f651" + "reference": "5c98652b1a5beb7e3cc8ec35419b2828dd63ab14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/cc06008f75262510ee135b8cbb14e333a309f651", - "reference": "cc06008f75262510ee135b8cbb14e333a309f651", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/5c98652b1a5beb7e3cc8ec35419b2828dd63ab14", + "reference": "5c98652b1a5beb7e3cc8ec35419b2828dd63ab14", "shasum": "" }, "require": { - "phenx/php-font-lib": "0.2.*" + "ext-dom": "*", + "ext-gd": "*", + "ext-mbstring": "*", + "phenx/php-font-lib": "0.4.*", + "phenx/php-svg-lib": "0.1.*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.7-dev" + } + }, "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, "classmap": [ - "include/" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL" + "LGPL-2.1" ], "authors": [ { @@ -441,74 +560,42 @@ { "name": "Brian Sweeney", "email": "eclecticgeek@gmail.com" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com" } ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", - "time": "2015-12-07 04:07:13" + "time": "2016-05-11 00:36:29" }, { - "name": "guzzle/guzzle", - "version": "v3.8.1", + "name": "gathercontent/htmldiff", + "version": "0.2.1", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + "url": "https://github.com/gathercontent/htmldiff.git", + "reference": "24674a62315f64330134b4a4c5b01a7b59193c93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "url": "https://api.github.com/repos/gathercontent/htmldiff/zipball/24674a62315f64330134b4a4c5b01a7b59193c93", + "reference": "24674a62315f64330134b4a4c5b01a7b59193c93", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": ">=2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" + "cogpowered/finediff": "0.3.1", + "ext-tidy": "*" }, "require-dev": { - "doctrine/cache": "*", - "monolog/monolog": "1.*", - "phpunit/phpunit": "3.7.*", - "psr/log": "1.0.*", - "symfony/class-loader": "*", - "zendframework/zend-cache": "<2.3", - "zendframework/zend-log": "<2.3" + "phpunit/phpunit": "4.*", + "squizlabs/php_codesniffer": "1.*" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.8-dev" - } - }, "autoload": { "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" + "GatherContent\\Htmldiff": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -517,51 +604,44 @@ ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "name": "Andrew Cairns", + "email": "andrew@gathercontent.com" }, { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" + "name": "Mathew Chapman", + "email": "mat@gathercontent.com" + }, + { + "name": "Peter Legierski", + "email": "peter@gathercontent.com" } ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2014-01-28 22:29:15" + "description": "Compare two HTML strings", + "time": "2015-04-15 15:39:46" }, { "name": "guzzlehttp/guzzle", - "version": "6.2.0", + "version": "6.2.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d094e337976dff9d8e2424e8485872194e768662" + "reference": "3f808fba627f2c5b69e2501217bf31af349c1427" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d094e337976dff9d8e2424e8485872194e768662", - "reference": "d094e337976dff9d8e2424e8485872194e768662", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/3f808fba627f2c5b69e2501217bf31af349c1427", + "reference": "3f808fba627f2c5b69e2501217bf31af349c1427", "shasum": "" }, "require": { - "guzzlehttp/promises": "~1.0", - "guzzlehttp/psr7": "~1.1", - "php": ">=5.5.0" + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.3.1", + "php": ">=5.5" }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "~4.0", - "psr/log": "~1.0" + "phpunit/phpunit": "^4.0", + "psr/log": "^1.0" }, "type": "library", "extra": { @@ -599,20 +679,20 @@ "rest", "web service" ], - "time": "2016-03-21 20:02:09" + "time": "2016-07-15 17:22:37" }, { "name": "guzzlehttp/promises", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8" + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bb9024c526b22f3fe6ae55a561fd70653d470aa8", - "reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/c10d860e2a9595f8883527fa0021c7da9e65f579", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579", "shasum": "" }, "require": { @@ -650,20 +730,20 @@ "keywords": [ "promise" ], - "time": "2016-03-08 01:15:46" + "time": "2016-05-18 16:56:05" }, { "name": "guzzlehttp/psr7", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b" + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/2e89629ff057ebb49492ba08e6995d3a6a80021b", - "reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", "shasum": "" }, "require": { @@ -679,7 +759,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -708,20 +788,20 @@ "stream", "uri" ], - "time": "2016-02-18 21:54:00" + "time": "2016-06-24 23:00:38" }, { "name": "intervention/image", - "version": "2.3.6", + "version": "2.3.8", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "e368d262887dbb2fdfaf710880571ede51e9c0e6" + "reference": "4064a980324f6c3bfa2bd981dfb247afa705ec3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/e368d262887dbb2fdfaf710880571ede51e9c0e6", - "reference": "e368d262887dbb2fdfaf710880571ede51e9c0e6", + "url": "https://api.github.com/repos/Intervention/image/zipball/4064a980324f6c3bfa2bd981dfb247afa705ec3c", + "reference": "4064a980324f6c3bfa2bd981dfb247afa705ec3c", "shasum": "" }, "require": { @@ -770,7 +850,7 @@ "thumbnail", "watermark" ], - "time": "2016-02-26 18:18:19" + "time": "2016-09-01 17:04:03" }, { "name": "jakub-onderka/php-console-color", @@ -919,16 +999,16 @@ }, { "name": "laravel/framework", - "version": "v5.2.29", + "version": "v5.3.11", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "e3d644eb131f18c5f3d28ff7bc678bc797091f20" + "reference": "ca48001b95a0543fb39fcd7219de960bbc03eaa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/e3d644eb131f18c5f3d28ff7bc678bc797091f20", - "reference": "e3d644eb131f18c5f3d28ff7bc678bc797091f20", + "url": "https://api.github.com/repos/laravel/framework/zipball/ca48001b95a0543fb39fcd7219de960bbc03eaa5", + "reference": "ca48001b95a0543fb39fcd7219de960bbc03eaa5", "shasum": "" }, "require": { @@ -941,20 +1021,20 @@ "monolog/monolog": "~1.11", "mtdowling/cron-expression": "~1.0", "nesbot/carbon": "~1.20", - "paragonie/random_compat": "~1.4", - "php": ">=5.5.9", + "paragonie/random_compat": "~1.4|~2.0", + "php": ">=5.6.4", "psy/psysh": "0.7.*", + "ramsey/uuid": "~3.0", "swiftmailer/swiftmailer": "~5.1", - "symfony/console": "2.8.*|3.0.*", - "symfony/debug": "2.8.*|3.0.*", - "symfony/finder": "2.8.*|3.0.*", - "symfony/http-foundation": "2.8.*|3.0.*", - "symfony/http-kernel": "2.8.*|3.0.*", - "symfony/polyfill-php56": "~1.0", - "symfony/process": "2.8.*|3.0.*", - "symfony/routing": "2.8.*|3.0.*", - "symfony/translation": "2.8.*|3.0.*", - "symfony/var-dumper": "2.8.*|3.0.*", + "symfony/console": "3.1.*", + "symfony/debug": "3.1.*", + "symfony/finder": "3.1.*", + "symfony/http-foundation": "3.1.*", + "symfony/http-kernel": "3.1.*", + "symfony/process": "3.1.*", + "symfony/routing": "3.1.*", + "symfony/translation": "3.1.*", + "symfony/var-dumper": "3.1.*", "vlucas/phpdotenv": "~2.2" }, "replace": { @@ -976,6 +1056,7 @@ "illuminate/http": "self.version", "illuminate/log": "self.version", "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", "illuminate/pagination": "self.version", "illuminate/pipeline": "self.version", "illuminate/queue": "self.version", @@ -985,16 +1066,17 @@ "illuminate/support": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "tightenco/collect": "self.version" }, "require-dev": { "aws/aws-sdk-php": "~3.0", - "mockery/mockery": "~0.9.2", + "mockery/mockery": "~0.9.4", "pda/pheanstalk": "~3.0", - "phpunit/phpunit": "~4.1", + "phpunit/phpunit": "~5.4", "predis/predis": "~1.0", - "symfony/css-selector": "2.8.*|3.0.*", - "symfony/dom-crawler": "2.8.*|3.0.*" + "symfony/css-selector": "3.1.*", + "symfony/dom-crawler": "3.1.*" }, "suggest": { "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).", @@ -1006,19 +1088,17 @@ "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).", "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", - "symfony/css-selector": "Required to use some of the crawler integration testing tools (2.8.*|3.0.*).", - "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (2.8.*|3.0.*)." + "symfony/css-selector": "Required to use some of the crawler integration testing tools (3.1.*).", + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (3.1.*).", + "symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.2-dev" + "dev-master": "5.3-dev" } }, "autoload": { - "classmap": [ - "src/Illuminate/Queue/IlluminateQueueClosure.php" - ], "files": [ "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Support/helpers.php" @@ -1034,29 +1114,29 @@ "authors": [ { "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com" + "email": "taylor@laravel.com" } ], "description": "The Laravel Framework.", - "homepage": "http://laravel.com", + "homepage": "https://laravel.com", "keywords": [ "framework", "laravel" ], - "time": "2016-04-03 01:43:55" + "time": "2016-09-28 02:15:37" }, { "name": "laravel/socialite", - "version": "v2.0.15", + "version": "v2.0.18", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "edd00ab96933e3ef053533cce81e958fb26921af" + "reference": "76ee5397fcdea5a062361392abca4eb397e519a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/edd00ab96933e3ef053533cce81e958fb26921af", - "reference": "edd00ab96933e3ef053533cce81e958fb26921af", + "url": "https://api.github.com/repos/laravel/socialite/zipball/76ee5397fcdea5a062361392abca4eb397e519a3", + "reference": "76ee5397fcdea5a062361392abca4eb397e519a3", "shasum": "" }, "require": { @@ -1069,7 +1149,7 @@ }, "require-dev": { "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.0|~5.0" }, "type": "library", "extra": { @@ -1097,20 +1177,20 @@ "laravel", "oauth" ], - "time": "2016-03-21 14:30:30" + "time": "2016-06-22 12:40:16" }, { "name": "league/flysystem", - "version": "1.0.20", + "version": "1.0.27", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "e87a786e3ae12a25cf78a71bb07b4b384bfaa83a" + "reference": "50e2045ed70a7e75a5e30bc3662904f3b67af8a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e87a786e3ae12a25cf78a71bb07b4b384bfaa83a", - "reference": "e87a786e3ae12a25cf78a71bb07b4b384bfaa83a", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/50e2045ed70a7e75a5e30bc3662904f3b67af8a9", + "reference": "50e2045ed70a7e75a5e30bc3662904f3b67af8a9", "shasum": "" }, "require": { @@ -1123,7 +1203,7 @@ "ext-fileinfo": "*", "mockery/mockery": "~0.9", "phpspec/phpspec": "^2.2", - "phpunit/phpunit": "~4.8 || ~5.0" + "phpunit/phpunit": "~4.8" }, "suggest": { "ext-fileinfo": "Required for MimeType", @@ -1180,20 +1260,20 @@ "sftp", "storage" ], - "time": "2016-03-14 21:54:11" + "time": "2016-08-10 08:55:11" }, { "name": "league/flysystem-aws-s3-v3", - "version": "1.0.9", + "version": "1.0.13", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6" + "reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6", - "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/dc56a8faf3aff0841f9eae04b6af94a50657896c", + "reference": "dc56a8faf3aff0841f9eae04b6af94a50657896c", "shasum": "" }, "require": { @@ -1227,30 +1307,30 @@ } ], "description": "Flysystem adapter for the AWS S3 SDK v3.x", - "time": "2015-11-19 08:44:16" + "time": "2016-06-21 21:34:35" }, { "name": "league/oauth1-client", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth1-client.git", - "reference": "cef3ceda13c78f89c323e4d5e6301c0eb7cea422" + "reference": "fca5f160650cb74d23fc11aa570dd61f86dcf647" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/cef3ceda13c78f89c323e4d5e6301c0eb7cea422", - "reference": "cef3ceda13c78f89c323e4d5e6301c0eb7cea422", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/fca5f160650cb74d23fc11aa570dd61f86dcf647", + "reference": "fca5f160650cb74d23fc11aa570dd61f86dcf647", "shasum": "" }, "require": { - "guzzle/guzzle": "3.*", - "php": ">=5.3.0" + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.5.0" }, "require-dev": { - "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "mockery/mockery": "^0.9", + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.0" }, "type": "library", "extra": { @@ -1290,20 +1370,20 @@ "tumblr", "twitter" ], - "time": "2015-10-23 04:02:07" + "time": "2016-08-17 00:36:58" }, { "name": "maximebf/debugbar", - "version": "v1.11.1", + "version": "v1.13.0", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "d9302891c1f0a0ac5a4f66725163a00537c6359f" + "reference": "5f49a5ed6cfde81d31d89378806670d77462526e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/d9302891c1f0a0ac5a4f66725163a00537c6359f", - "reference": "d9302891c1f0a0ac5a4f66725163a00537c6359f", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/5f49a5ed6cfde81d31d89378806670d77462526e", + "reference": "5f49a5ed6cfde81d31d89378806670d77462526e", "shasum": "" }, "require": { @@ -1322,7 +1402,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -1351,20 +1431,20 @@ "debug", "debugbar" ], - "time": "2016-01-22 12:22:23" + "time": "2016-09-15 14:01:59" }, { "name": "monolog/monolog", - "version": "1.18.2", + "version": "1.21.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "064b38c16790249488e7a8b987acf1c9d7383c09" + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/064b38c16790249488e7a8b987acf1c9d7383c09", - "reference": "064b38c16790249488e7a8b987acf1c9d7383c09", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f42fbdfd53e306bda545845e4dbfd3e72edb4952", + "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952", "shasum": "" }, "require": { @@ -1383,8 +1463,8 @@ "php-console/php-console": "^3.1.3", "phpunit/phpunit": "~4.5", "phpunit/phpunit-mock-objects": "2.3.0", - "raven/raven": "^0.13", "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", "swiftmailer/swiftmailer": "~5.3" }, "suggest": { @@ -1396,9 +1476,9 @@ "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", - "raven/raven": "Allow sending log messages to a Sentry server", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" }, "type": "library", "extra": { @@ -1429,7 +1509,7 @@ "logging", "psr-3" ], - "time": "2016-04-02 13:12:58" + "time": "2016-07-29 03:23:52" }, { "name": "mtdowling/cron-expression", @@ -1579,16 +1659,16 @@ }, { "name": "nikic/php-parser", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ce5be709d59b32dd8a88c80259028759991a4206" + "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ce5be709d59b32dd8a88c80259028759991a4206", - "reference": "ce5be709d59b32dd8a88c80259028759991a4206", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4dd659edadffdc2143e4753df655d866dbfeedf0", + "reference": "4dd659edadffdc2143e4753df655d866dbfeedf0", "shasum": "" }, "require": { @@ -1604,7 +1684,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1626,20 +1706,20 @@ "parser", "php" ], - "time": "2016-02-28 19:48:28" + "time": "2016-09-16 12:04:44" }, { "name": "paragonie/random_compat", - "version": "v1.4.1", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "c7e26a21ba357863de030f0b9e701c7d04593774" + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/c7e26a21ba357863de030f0b9e701c7d04593774", - "reference": "c7e26a21ba357863de030f0b9e701c7d04593774", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/088c04e2f261c33bed6ca5245491cfca69195ccf", + "reference": "088c04e2f261c33bed6ca5245491cfca69195ccf", "shasum": "" }, "require": { @@ -1674,31 +1754,31 @@ "pseudorandom", "random" ], - "time": "2016-03-18 20:34:03" + "time": "2016-04-03 06:00:07" }, { "name": "phenx/php-font-lib", - "version": "0.2.2", + "version": "0.4", "source": { "type": "git", "url": "https://github.com/PhenX/php-font-lib.git", - "reference": "c30c7fc00a6b0d863e9bb4c5d5dd015298b2dc82" + "reference": "b8af0cacdc3cbf1e41a586fcb78f506f4121a088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhenX/php-font-lib/zipball/c30c7fc00a6b0d863e9bb4c5d5dd015298b2dc82", - "reference": "c30c7fc00a6b0d863e9bb4c5d5dd015298b2dc82", + "url": "https://api.github.com/repos/PhenX/php-font-lib/zipball/b8af0cacdc3cbf1e41a586fcb78f506f4121a088", + "reference": "b8af0cacdc3cbf1e41a586fcb78f506f4121a088", "shasum": "" }, "type": "library", "autoload": { - "classmap": [ - "classes/" - ] + "psr-0": { + "FontLib\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL" + "LGPL-3.0" ], "authors": [ { @@ -1708,76 +1788,61 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", - "time": "2014-02-01 15:22:28" + "time": "2015-05-06 20:02:39" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "2.0.4", + "name": "phenx/php-svg-lib", + "version": "0.1", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + "url": "https://github.com/PhenX/php-svg-lib.git", + "reference": "b419766515b3426c6da74b0e29e93d71c4f17099" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/b419766515b3426c6da74b0e29e93d71c4f17099", + "reference": "b419766515b3426c6da74b0e29e93d71c4f17099", "shasum": "" }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-0": { - "phpDocumentor": [ - "src/" - ] + "Svg\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "LGPL-3.0" ], "authors": [ { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" } ], - "time": "2015-02-03 12:10:50" + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/PhenX/php-svg-lib", + "time": "2015-05-06 18:49:49" }, { "name": "predis/predis", - "version": "v1.0.3", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/nrk/predis.git", - "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04" + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nrk/predis/zipball/84060b9034d756b4d79641667d7f9efe1aeb8e04", - "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04", + "url": "https://api.github.com/repos/nrk/predis/zipball/f0210e38881631afeafb56ab43405a92cafd9fd1", + "reference": "f0210e38881631afeafb56ab43405a92cafd9fd1", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": ">=5.3.9" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8" }, "suggest": { "ext-curl": "Allows access to Webdis when paired with phpiredis", @@ -1800,27 +1865,27 @@ "homepage": "http://clorophilla.net" } ], - "description": "Flexible and feature-complete PHP client library for Redis", + "description": "Flexible and feature-complete Redis client for PHP and HHVM", "homepage": "http://github.com/nrk/predis", "keywords": [ "nosql", "predis", "redis" ], - "time": "2015-07-30 18:34:15" + "time": "2016-06-16 16:22:20" }, { "name": "psr/http-message", - "version": "1.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", - "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", "shasum": "" }, "require": { @@ -1848,6 +1913,7 @@ } ], "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", "keywords": [ "http", "http-message", @@ -1856,26 +1922,34 @@ "request", "response" ], - "time": "2015-05-04 20:22:00" + "time": "2016-08-06 14:39:51" }, { "name": "psr/log", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + "reference": "5277094ed527a1c4477177d102fe4c53551953e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "url": "https://api.github.com/repos/php-fig/log/zipball/5277094ed527a1c4477177d102fe4c53551953e0", + "reference": "5277094ed527a1c4477177d102fe4c53551953e0", "shasum": "" }, + "require": { + "php": ">=5.3.0" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { - "psr-0": { - "Psr\\Log\\": "" + "psr-4": { + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1889,12 +1963,13 @@ } ], "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ "log", "psr", "psr-3" ], - "time": "2012-12-21 11:40:51" + "time": "2016-09-19 16:02:08" }, { "name": "psy/psysh", @@ -1969,24 +2044,104 @@ "time": "2016-03-09 05:03:14" }, { - "name": "swiftmailer/swiftmailer", - "version": "v5.4.1", + "name": "ramsey/uuid", + "version": "3.5.0", "source": { "type": "git", - "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421" + "url": "https://github.com/ramsey/uuid.git", + "reference": "a6d15c8618ea3951fd54d34e326b68d3d0bc0786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/0697e6aa65c83edf97bb0f23d8763f94e3f11421", - "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/a6d15c8618ea3951fd54d34e326b68d3d0bc0786", + "reference": "a6d15c8618ea3951fd54d34e326b68d3d0bc0786", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0", + "php": ">=5.4" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "apigen/apigen": "^4.1", + "codeception/aspect-mock": "1.0.0", + "goaop/framework": "1.0.0-alpha.2", + "ircmaxell/random-lib": "^1.1", + "jakub-onderka/php-parallel-lint": "^0.9.0", + "mockery/mockery": "^0.9.4", + "moontoast/math": "^1.1", + "phpunit/phpunit": "^4.7|>=5.0 <5.4", + "satooshi/php-coveralls": "^0.6.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", + "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", + "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marijn Huizendveld", + "email": "marijn.huizendveld@gmail.com" + }, + { + "name": "Thibaud Fabre", + "email": "thibaud@aztech.io" + }, + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "homepage": "https://github.com/ramsey/uuid", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "time": "2016-08-02 18:39:32" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", + "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "mockery/mockery": "~0.9.1,<0.9.4" + "mockery/mockery": "~0.9.1" }, "type": "library", "extra": { @@ -2019,20 +2174,20 @@ "mail", "mailer" ], - "time": "2015-06-06 14:19:39" + "time": "2016-07-08 11:51:25" }, { "name": "symfony/class-loader", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877" + "reference": "2d0ba77c46ecc96a6641009a98f72632216811ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877", - "reference": "cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/2d0ba77c46ecc96a6641009a98f72632216811ba", + "reference": "2d0ba77c46ecc96a6641009a98f72632216811ba", "shasum": "" }, "require": { @@ -2048,7 +2203,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2075,20 +2230,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2016-03-30 10:41:14" + "time": "2016-08-23 13:39:15" }, { "name": "symfony/console", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6b1175135bc2a74c08a28d89761272de8beed8cd" + "reference": "8ea494c34f0f772c3954b5fbe00bffc5a435e563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6b1175135bc2a74c08a28d89761272de8beed8cd", - "reference": "6b1175135bc2a74c08a28d89761272de8beed8cd", + "url": "https://api.github.com/repos/symfony/console/zipball/8ea494c34f0f772c3954b5fbe00bffc5a435e563", + "reference": "8ea494c34f0f772c3954b5fbe00bffc5a435e563", "shasum": "" }, "require": { @@ -2108,7 +2263,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2135,20 +2290,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-03-16 17:00:50" + "time": "2016-08-19 06:48:39" }, { "name": "symfony/debug", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "a06d10888a45afd97534506afb058ec38d9ba35b" + "reference": "34f6ac18c2974ca5fce68adf419ee7d15def6f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/a06d10888a45afd97534506afb058ec38d9ba35b", - "reference": "a06d10888a45afd97534506afb058ec38d9ba35b", + "url": "https://api.github.com/repos/symfony/debug/zipball/34f6ac18c2974ca5fce68adf419ee7d15def6f11", + "reference": "34f6ac18c2974ca5fce68adf419ee7d15def6f11", "shasum": "" }, "require": { @@ -2165,7 +2320,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2192,20 +2347,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-03-30 10:41:14" + "time": "2016-08-23 13:39:15" }, { "name": "symfony/event-dispatcher", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9002dcf018d884d294b1ef20a6f968efc1128f39" + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9002dcf018d884d294b1ef20a6f968efc1128f39", - "reference": "9002dcf018d884d294b1ef20a6f968efc1128f39", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c0c00c80b3a69132c4e55c3e7db32b4a387615e5", + "reference": "c0c00c80b3a69132c4e55c3e7db32b4a387615e5", "shasum": "" }, "require": { @@ -2225,7 +2380,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2252,20 +2407,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-03-10 10:34:12" + "time": "2016-07-19 10:45:57" }, { "name": "symfony/finder", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "c54e407b35bc098916704e9fd090da21da4c4f52" + "reference": "e568ef1784f447a0e54dcb6f6de30b9747b0f577" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/c54e407b35bc098916704e9fd090da21da4c4f52", - "reference": "c54e407b35bc098916704e9fd090da21da4c4f52", + "url": "https://api.github.com/repos/symfony/finder/zipball/e568ef1784f447a0e54dcb6f6de30b9747b0f577", + "reference": "e568ef1784f447a0e54dcb6f6de30b9747b0f577", "shasum": "" }, "require": { @@ -2274,7 +2429,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2301,20 +2456,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-03-10 11:13:05" + "time": "2016-08-26 12:04:02" }, { "name": "symfony/http-foundation", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "99f38445a874e7becb8afc4b4a79ee181cf6ec3f" + "reference": "63592e00fd90632b57ee50220a1ddb29b6bf3bb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/99f38445a874e7becb8afc4b4a79ee181cf6ec3f", - "reference": "99f38445a874e7becb8afc4b4a79ee181cf6ec3f", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/63592e00fd90632b57ee50220a1ddb29b6bf3bb4", + "reference": "63592e00fd90632b57ee50220a1ddb29b6bf3bb4", "shasum": "" }, "require": { @@ -2327,7 +2482,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2354,20 +2509,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-03-27 14:50:32" + "time": "2016-08-22 12:11:19" }, { "name": "symfony/http-kernel", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "579f828489659d7b3430f4bd9b67b4618b387dea" + "reference": "aeda215d6b01f119508c090d2a09ebb5b0bc61f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/579f828489659d7b3430f4bd9b67b4618b387dea", - "reference": "579f828489659d7b3430f4bd9b67b4618b387dea", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/aeda215d6b01f119508c090d2a09ebb5b0bc61f3", + "reference": "aeda215d6b01f119508c090d2a09ebb5b0bc61f3", "shasum": "" }, "require": { @@ -2375,7 +2530,7 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~2.8|~3.0" + "symfony/http-foundation": "~2.8.8|~3.0.8|~3.1.2|~3.2" }, "conflict": { "symfony/config": "<2.8" @@ -2409,7 +2564,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2436,20 +2591,20 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2016-03-25 01:41:20" + "time": "2016-09-03 15:28:24" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "1289d16209491b584839022f29257ad859b8532d" + "reference": "dff51f72b0706335131b00a7f49606168c582594" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/1289d16209491b584839022f29257ad859b8532d", - "reference": "1289d16209491b584839022f29257ad859b8532d", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", + "reference": "dff51f72b0706335131b00a7f49606168c582594", "shasum": "" }, "require": { @@ -2461,7 +2616,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -2495,20 +2650,20 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-php56", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "4d891fff050101a53a4caabb03277284942d1ad9" + "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/4d891fff050101a53a4caabb03277284942d1ad9", - "reference": "4d891fff050101a53a4caabb03277284942d1ad9", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/3edf57a8fbf9a927533344cef65ad7e1cf31030a", + "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a", "shasum": "" }, "require": { @@ -2518,7 +2673,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -2551,20 +2706,20 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-util", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4" + "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4", - "reference": "8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ef830ce3d218e622b221d6bfad42c751d974bf99", + "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99", "shasum": "" }, "require": { @@ -2573,7 +2728,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -2603,20 +2758,20 @@ "polyfill", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/process", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "e6f1f98bbd355d209a992bfff45e7edfbd4a0776" + "reference": "e64e93041c80e77197ace5ab9385dedb5a143697" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/e6f1f98bbd355d209a992bfff45e7edfbd4a0776", - "reference": "e6f1f98bbd355d209a992bfff45e7edfbd4a0776", + "url": "https://api.github.com/repos/symfony/process/zipball/e64e93041c80e77197ace5ab9385dedb5a143697", + "reference": "e64e93041c80e77197ace5ab9385dedb5a143697", "shasum": "" }, "require": { @@ -2625,7 +2780,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2652,20 +2807,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-03-30 10:41:14" + "time": "2016-08-16 14:58:24" }, { "name": "symfony/routing", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "d061b609f2d0769494c381ec92f5c5cc5e4a20aa" + "reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/d061b609f2d0769494c381ec92f5c5cc5e4a20aa", - "reference": "d061b609f2d0769494c381ec92f5c5cc5e4a20aa", + "url": "https://api.github.com/repos/symfony/routing/zipball/8edf62498a1a4c57ba317664a4b698339c10cdf6", + "reference": "8edf62498a1a4c57ba317664a4b698339c10cdf6", "shasum": "" }, "require": { @@ -2694,7 +2849,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2727,20 +2882,20 @@ "uri", "url" ], - "time": "2016-03-23 13:23:25" + "time": "2016-08-16 14:58:24" }, { "name": "symfony/translation", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f7a07af51ea067745a521dab1e3152044a2fb1f2" + "reference": "a35edc277513c9bc0f063ca174c36b346f974528" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f7a07af51ea067745a521dab1e3152044a2fb1f2", - "reference": "f7a07af51ea067745a521dab1e3152044a2fb1f2", + "url": "https://api.github.com/repos/symfony/translation/zipball/a35edc277513c9bc0f063ca174c36b346f974528", + "reference": "a35edc277513c9bc0f063ca174c36b346f974528", "shasum": "" }, "require": { @@ -2764,7 +2919,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2791,20 +2946,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-03-25 01:41:20" + "time": "2016-08-05 08:37:39" }, { "name": "symfony/var-dumper", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "3841ed86527d18ee2c35fe4afb1b2fc60f8fae79" + "reference": "62ee73706c421654a4c840028954510277f7dfc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/3841ed86527d18ee2c35fe4afb1b2fc60f8fae79", - "reference": "3841ed86527d18ee2c35fe4afb1b2fc60f8fae79", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/62ee73706c421654a4c840028954510277f7dfc8", + "reference": "62ee73706c421654a4c840028954510277f7dfc8", "shasum": "" }, "require": { @@ -2820,7 +2975,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -2854,32 +3009,32 @@ "debug", "dump" ], - "time": "2016-03-10 10:34:12" + "time": "2016-08-31 09:05:42" }, { "name": "vlucas/phpdotenv", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412" + "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/9caf304153dc2288e4970caec6f1f3b3bc205412", - "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", + "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "phpunit/phpunit": "^4.8|^5.0" + "phpunit/phpunit": "^4.8 || ^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.4-dev" } }, "autoload": { @@ -2889,7 +3044,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD" + "BSD-3-Clause-Attribution" ], "authors": [ { @@ -2899,13 +3054,12 @@ } ], "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "homepage": "http://github.com/vlucas/phpdotenv", "keywords": [ "dotenv", "env", "environment" ], - "time": "2015-12-29 15:10:30" + "time": "2016-09-01 10:05:43" } ], "packages-dev": [ @@ -2965,33 +3119,29 @@ }, { "name": "fzaninotto/faker", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "d0190b156bcca848d401fb80f31f504f37141c8d" + "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d0190b156bcca848d401fb80f31f504f37141c8d", - "reference": "d0190b156bcca848d401fb80f31f504f37141c8d", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/44f9a286a04b80c76a4e5fb7aad8bb539b920123", + "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3|^7.0" }, "require-dev": { + "ext-intl": "*", "phpunit/phpunit": "~4.0", "squizlabs/php_codesniffer": "~1.5" }, - "suggest": { - "ext-intl": "*" - }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } + "branch-alias": [] }, "autoload": { "psr-4": { @@ -3013,7 +3163,7 @@ "faker", "fixtures" ], - "time": "2015-05-29 06:29:14" + "time": "2016-04-29 12:21:54" }, { "name": "hamcrest/hamcrest-php", @@ -3062,16 +3212,16 @@ }, { "name": "mockery/mockery", - "version": "0.9.4", + "version": "0.9.5", "source": { "type": "git", "url": "https://github.com/padraic/mockery.git", - "reference": "70bba85e4aabc9449626651f48b9018ede04f86b" + "reference": "4db079511a283e5aba1b3c2fb19037c645e70fc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", - "reference": "70bba85e4aabc9449626651f48b9018ede04f86b", + "url": "https://api.github.com/repos/padraic/mockery/zipball/4db079511a283e5aba1b3c2fb19037c645e70fc2", + "reference": "4db079511a283e5aba1b3c2fb19037c645e70fc2", "shasum": "" }, "require": { @@ -3123,90 +3273,81 @@ "test double", "testing" ], - "time": "2015-04-02 19:54:00" + "time": "2016-05-22 21:52:33" }, { - "name": "phpspec/php-diff", - "version": "v1.0.2", + "name": "myclabs/deep-copy", + "version": "1.5.4", "source": { "type": "git", - "url": "https://github.com/phpspec/php-diff.git", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/ea74994a3dc7f8d2f65a06009348f2d63c81e61f", + "reference": "ea74994a3dc7f8d2f65a06009348f2d63c81e61f", "shasum": "" }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, "type": "library", "autoload": { - "psr-0": { - "Diff": "lib/" + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Chris Boulton", - "homepage": "http://github.com/chrisboulton", - "role": "Original developer" - } + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" ], - "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", - "time": "2013-11-01 13:02:21" + "time": "2016-09-16 13:37:59" }, { - "name": "phpspec/phpspec", - "version": "2.5.0", + "name": "phpdocumentor/reflection-common", + "version": "1.0", "source": { "type": "git", - "url": "https://github.com/phpspec/phpspec.git", - "reference": "385ecb015e97c13818074f1517928b24d4a26067" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/385ecb015e97c13818074f1517928b24d4a26067", - "reference": "385ecb015e97c13818074f1517928b24d4a26067", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.1", - "ext-tokenizer": "*", - "php": ">=5.3.3", - "phpspec/php-diff": "~1.0.0", - "phpspec/prophecy": "~1.4", - "sebastian/exporter": "~1.0", - "symfony/console": "~2.3|~3.0", - "symfony/event-dispatcher": "~2.1|~3.0", - "symfony/finder": "~2.1|~3.0", - "symfony/process": "^2.6|~3.0", - "symfony/yaml": "~2.1|~3.0" + "php": ">=5.5" }, "require-dev": { - "behat/behat": "^3.0.11", - "bossa/phpspec2-expect": "~1.0", - "phpunit/phpunit": "~4.4", - "symfony/filesystem": "~2.1|~3.0" + "phpunit/phpunit": "^4.6" }, - "suggest": { - "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" - }, - "bin": [ - "bin/phpspec" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "psr-0": { - "PhpSpec": "src/" + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -3215,56 +3356,141 @@ ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "homepage": "http://marcelloduarte.net/" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Specification-oriented BDD framework for PHP 5.3+", - "homepage": "http://phpspec.net/", + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", "keywords": [ - "BDD", - "SpecBDD", - "TDD", - "spec", - "specification", - "testing", - "tests" + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" ], - "time": "2016-03-20 20:34:32" + "time": "2015-12-27 11:43:31" }, { - "name": "phpspec/prophecy", - "version": "v1.6.0", + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.0", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd", + "reference": "9270140b940ff02e58ec577c237274e92cd40cdd", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-06-10 09:48:41" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b39c7a5b194f9ed7bd0dd345c751007a41862443", + "reference": "b39c7a5b194f9ed7bd0dd345c751007a41862443", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-06-10 07:14:17" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", + "reference": "58a8137754bc24b25740d4281399a4a3596058e0", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "~2.0", - "sebastian/comparator": "~1.1", - "sebastian/recursion-context": "~1.0" + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0" }, "require-dev": { - "phpspec/phpspec": "~2.0" + "phpspec/phpspec": "^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -3297,43 +3523,44 @@ "spy", "stub" ], - "time": "2016-02-15 07:46:21" + "time": "2016-06-07 08:13:47" }, { "name": "phpunit/php-code-coverage", - "version": "2.2.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5f3f7e736d6319d5f1fc402aff8b026da26709a3", + "reference": "5f3f7e736d6319d5f1fc402aff8b026da26709a3", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": "^5.6 || ^7.0", "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "~1.0|~2.0" }, "require-dev": { "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" + "phpunit/phpunit": "^5.4" }, "suggest": { "ext-dom": "*", - "ext-xdebug": ">=2.2.1", + "ext-xdebug": ">=2.4.0", "ext-xmlwriter": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { @@ -3359,7 +3586,7 @@ "testing", "xunit" ], - "time": "2015-10-06 15:47:00" + "time": "2016-07-26 14:39:29" }, { "name": "phpunit/php-file-iterator", @@ -3451,21 +3678,24 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ @@ -3488,7 +3718,7 @@ "keywords": [ "timer" ], - "time": "2015-06-21 08:01:12" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", @@ -3541,40 +3771,51 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.24", + "version": "5.5.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1066c562c52900a142a0e2bbf0582994671385e" + "reference": "a57126dc681b08289fef6ac96a48e30656f84350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1066c562c52900a142a0e2bbf0582994671385e", - "reference": "a1066c562c52900a142a0e2bbf0582994671385e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a57126dc681b08289fef6ac96a48e30656f84350", + "reference": "a57126dc681b08289fef6ac96a48e30656f84350", "shasum": "" }, "require": { "ext-dom": "*", "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", + "phpunit/php-code-coverage": "^4.0.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": ">=1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", + "sebastian/environment": "^1.3 || ^2.0", "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", + "sebastian/object-enumerator": "~1.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", "symfony/yaml": "~2.1|~3.0" }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, "suggest": { + "ext-tidy": "*", + "ext-xdebug": "*", "phpunit/php-invoker": "~1.1" }, "bin": [ @@ -3583,7 +3824,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.8.x-dev" + "dev-master": "5.5.x-dev" } }, "autoload": { @@ -3609,30 +3850,33 @@ "testing", "xunit" ], - "time": "2016-03-14 06:16:08" + "time": "2016-09-21 14:40:13" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", + "version": "3.2.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + "reference": "546898a2c0c356ef2891b39dd7d07f5d82c8ed0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/546898a2c0c356ef2891b39dd7d07f5d82c8ed0a", + "reference": "546898a2c0c356ef2891b39dd7d07f5d82c8ed0a", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^5.4" }, "suggest": { "ext-soap": "*" @@ -3640,7 +3884,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3.x-dev" + "dev-master": "3.2.x-dev" } }, "autoload": { @@ -3665,7 +3909,52 @@ "mock", "xunit" ], - "time": "2015-10-02 06:51:40" + "time": "2016-09-06 16:07:45" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" }, { "name": "sebastian/comparator", @@ -3785,23 +4074,23 @@ }, { "name": "sebastian/environment", - "version": "1.3.5", + "version": "1.3.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf" + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^4.8 || ^5.0" }, "type": "library", "extra": { @@ -3831,20 +4120,20 @@ "environment", "hhvm" ], - "time": "2016-02-26 18:40:46" + "time": "2016-08-18 05:49:44" }, { "name": "sebastian/exporter", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e" + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", "shasum": "" }, "require": { @@ -3852,12 +4141,13 @@ "sebastian/recursion-context": "~1.0" }, "require-dev": { + "ext-mbstring": "*", "phpunit/phpunit": "~4.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -3897,7 +4187,7 @@ "export", "exporter" ], - "time": "2015-06-21 07:55:53" + "time": "2016-06-17 09:04:28" }, { "name": "sebastian/global-state", @@ -3950,6 +4240,52 @@ ], "time": "2015-10-12 03:26:01" }, + { + "name": "sebastian/object-enumerator", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", + "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-01-28 13:25:10" + }, { "name": "sebastian/recursion-context", "version": "1.0.2", @@ -4004,20 +4340,70 @@ "time": "2015-11-11 19:50:13" }, { - "name": "sebastian/version", - "version": "1.0.6", + "name": "sebastian/resource-operations", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", "shasum": "" }, + "require": { + "php": ">=5.6.0" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -4036,20 +4422,20 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21 13:59:46" + "time": "2016-02-04 12:56:52" }, { "name": "symfony/css-selector", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0" + "reference": "2851e1932d77ce727776154d659b232d061e816a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/65e764f404685f2dc20c057e889b3ad04b2e2db0", - "reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2851e1932d77ce727776154d659b232d061e816a", + "reference": "2851e1932d77ce727776154d659b232d061e816a", "shasum": "" }, "require": { @@ -4058,7 +4444,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -4089,20 +4475,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2016-03-04 07:55:57" + "time": "2016-06-29 05:41:56" }, { "name": "symfony/dom-crawler", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "18a06d7a9af41718c20764a674a0ebba3bc40d1f" + "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/18a06d7a9af41718c20764a674a0ebba3bc40d1f", - "reference": "18a06d7a9af41718c20764a674a0ebba3bc40d1f", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/bb7395e8b1db3654de82b9f35d019958276de4d7", + "reference": "bb7395e8b1db3654de82b9f35d019958276de4d7", "shasum": "" }, "require": { @@ -4118,7 +4504,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -4145,20 +4531,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2016-03-23 13:23:25" + "time": "2016-08-05 08:37:39" }, { "name": "symfony/yaml", - "version": "v3.0.4", + "version": "v3.1.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0047c8366744a16de7516622c5b7355336afae96" + "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", - "reference": "0047c8366744a16de7516622c5b7355336afae96", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f291ed25eb1435bddbe8a96caaef16469c2a092d", + "reference": "f291ed25eb1435bddbe8a96caaef16469c2a092d", "shasum": "" }, "require": { @@ -4167,7 +4553,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -4194,7 +4580,57 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-03-04 07:55:57" + "time": "2016-09-02 02:12:52" + }, + { + "name": "webmozart/assert", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", + "shasum": "" + }, + "require": { + "php": "^5.3.3|^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-08-09 15:02:57" } ], "aliases": [], @@ -4203,7 +4639,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.5.9" + "php": ">=5.6.4", + "ext-tidy": "*" }, "platform-dev": [] } diff --git a/config/app.php b/config/app.php index 0d6f6f2b0..786f005ac 100644 --- a/config/app.php +++ b/config/app.php @@ -57,7 +57,7 @@ return [ | */ - 'locale' => 'en', + 'locale' => env('APP_LANG', 'en'), /* |-------------------------------------------------------------------------- @@ -138,6 +138,7 @@ return [ Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, Laravel\Socialite\SocialiteServiceProvider::class, /** @@ -156,6 +157,7 @@ return [ BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\AppServiceProvider::class, + BookStack\Providers\BroadcastServiceProvider::class, BookStack\Providers\EventServiceProvider::class, BookStack\Providers\RouteServiceProvider::class, BookStack\Providers\CustomFacadeProvider::class, @@ -194,6 +196,7 @@ return [ 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, 'Password' => Illuminate\Support\Facades\Password::class, 'Queue' => Illuminate\Support\Facades\Queue::class, 'Redirect' => Illuminate\Support\Facades\Redirect::class, diff --git a/config/filesystems.php b/config/filesystems.php index dbcb03db1..836f68d3d 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -56,7 +56,7 @@ return [ 'local' => [ 'driver' => 'local', - 'root' => public_path(), + 'root' => base_path(), ], 'ftp' => [ diff --git a/config/setting-defaults.php b/config/setting-defaults.php index deafceb29..c681bb7f5 100644 --- a/config/setting-defaults.php +++ b/config/setting-defaults.php @@ -6,6 +6,7 @@ return [ 'app-name' => 'BookStack', + 'app-name-header' => true, 'app-editor' => 'wysiwyg', 'app-color' => '#0288D1', 'app-color-light' => 'rgba(21, 101, 192, 0.15)', diff --git a/database/migrations/2015_08_29_105422_add_roles_and_permissions.php b/database/migrations/2015_08_29_105422_add_roles_and_permissions.php index 763a33fec..47a77e29f 100644 --- a/database/migrations/2015_08_29_105422_add_roles_and_permissions.php +++ b/database/migrations/2015_08_29_105422_add_roles_and_permissions.php @@ -129,7 +129,7 @@ class AddRolesAndPermissions extends Migration // Set all current users as admins // (At this point only the initially create user should be an admin) - $users = DB::table('users')->get(); + $users = DB::table('users')->get()->all(); foreach ($users as $user) { DB::table('role_user')->insert([ 'role_id' => $adminId, diff --git a/database/migrations/2016_09_29_101449_remove_hidden_roles.php b/database/migrations/2016_09_29_101449_remove_hidden_roles.php new file mode 100644 index 000000000..f666cad2c --- /dev/null +++ b/database/migrations/2016_09_29_101449_remove_hidden_roles.php @@ -0,0 +1,66 @@ +dropColumn('hidden'); + }); + + // Add column to mark system users + Schema::table('users', function(Blueprint $table) { + $table->string('system_name')->nullable()->index(); + }); + + // Insert our new public system user. + $publicUserId = DB::table('users')->insertGetId([ + 'email' => 'guest@example.com', + 'name' => 'Guest', + 'system_name' => 'public', + 'email_confirmed' => true, + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), + ]); + + // Get the public role + $publicRole = DB::table('roles')->where('system_name', '=', 'public')->first(); + + // Connect the new public user to the public role + DB::table('role_user')->insert([ + 'user_id' => $publicUserId, + 'role_id' => $publicRole->id + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('roles', function(Blueprint $table) { + $table->boolean('hidden')->default(false); + $table->index('hidden'); + }); + + DB::table('users')->where('system_name', '=', 'public')->delete(); + + Schema::table('users', function(Blueprint $table) { + $table->dropColumn('system_name'); + }); + + DB::table('roles')->where('system_name', '=', 'public')->update(['hidden' => true]); + } +} diff --git a/database/migrations/2016_10_09_142037_create_attachments_table.php b/database/migrations/2016_10_09_142037_create_attachments_table.php new file mode 100644 index 000000000..627c237c4 --- /dev/null +++ b/database/migrations/2016_10_09_142037_create_attachments_table.php @@ -0,0 +1,71 @@ +increments('id'); + $table->string('name'); + $table->string('path'); + $table->string('extension', 20); + $table->integer('uploaded_to'); + + $table->boolean('external'); + $table->integer('order'); + + $table->integer('created_by'); + $table->integer('updated_by'); + + $table->index('uploaded_to'); + $table->timestamps(); + }); + + // Get roles with permissions we need to change + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Attachment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('attachments'); + + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Attachment'; + foreach ($ops as $op) { + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + DB::table('role_permissions')->where('name', '=', $permName)->delete(); + } + } +} diff --git a/gulpfile.js b/gulpfile.js index 7deefc71a..9d789d9b4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,27 +1,8 @@ var elixir = require('laravel-elixir'); -// Custom extensions -var gulp = require('gulp'); -var Task = elixir.Task; -var fs = require('fs'); - -elixir.extend('queryVersion', function(inputFiles) { - new Task('queryVersion', function() { - var manifestObject = {}; - var uidString = Date.now().toString(16).slice(4); - for (var i = 0; i < inputFiles.length; i++) { - var file = inputFiles[i]; - manifestObject[file] = file + '?version=' + uidString; - } - var fileContents = JSON.stringify(manifestObject, null, 1); - fs.writeFileSync('public/build/manifest.json', fileContents); - }).watch(['./public/css/*.css', './public/js/*.js']); -}); - -elixir(function(mix) { - mix.sass('styles.scss') - .sass('print-styles.scss') - .sass('export-styles.scss') - .browserify('global.js', 'public/js/common.js') - .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); +elixir(mix => { + mix.sass('styles.scss'); + mix.sass('print-styles.scss'); + mix.sass('export-styles.scss'); + mix.browserify('global.js', './public/js/common.js'); }); diff --git a/package.json b/package.json index fde090beb..30f288d45 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "private": true, - "devDependencies": { - "gulp": "^3.9.0" + "scripts": { + "prod": "gulp --production", + "dev": "gulp watch" }, - "dependencies": { + "devDependencies": { "angular": "^1.5.5", "angular-animate": "^1.5.5", "angular-resource": "^1.5.5", "angular-sanitize": "^1.5.5", - "angular-ui-sortable": "^0.14.0", - "babel-runtime": "^5.8.29", - "bootstrap-sass": "^3.0.0", + "angular-ui-sortable": "^0.15.0", "dropzone": "^4.0.1", - "laravel-elixir": "^5.0.0", + "gulp": "^3.9.0", + "laravel-elixir": "^6.0.0-11", + "laravel-elixir-browserify-official": "^0.1.3", "marked": "^0.3.5", "moment": "^2.12.0", "zeroclipboard": "^2.2.0" diff --git a/phpspec.yml b/phpspec.yml deleted file mode 100644 index 58f1d982e..000000000 --- a/phpspec.yml +++ /dev/null @@ -1,5 +0,0 @@ -suites: - main: - namespace: BookStack - psr4_prefix: BookStack - src_path: app \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index a2b26d413..72e06a3fc 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -30,6 +30,7 @@ + diff --git a/readme.md b/readme.md index 3a745beb1..5d3e79a2e 100644 --- a/readme.md +++ b/readme.md @@ -2,13 +2,15 @@ [![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest) [![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE) -[![Build Status](https://travis-ci.org/ssddanbrown/BookStack.svg)](https://travis-ci.org/ssddanbrown/BookStack) +[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack) A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/. * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation) * [Documentation](https://www.bookstackapp.com/docs) -* [Demo Instance](https://demo.bookstackapp.com) *(Login username: `admin@example.com`. Password: `password`)* +* [Demo Instance](https://demo.bookstackapp.com) + * *Username: `admin@example.com`* + * *Password: `password`* * [BookStack Blog](https://www.bookstackapp.com/blog) ## Development & Testing @@ -29,7 +31,7 @@ php artisan migrate --database=mysql_testing php artisan db:seed --class=DummyContentSeeder --database=mysql_testing ``` -Once done you can run `phpunit` (or `./vendor/bin/phpunit` if `phpunit` is not found) in the application root directory to run all tests. +Once done you can run `phpunit` in the application root directory to run all tests. ## License @@ -51,3 +53,5 @@ These are the great projects used to help build BookStack: * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) * [Marked](https://github.com/chjj/marked) * [Moment.js](http://momentjs.com/) + +Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy. diff --git a/resources/assets/js/components/drop-zone.html b/resources/assets/js/components/drop-zone.html deleted file mode 100644 index 26e0ee2aa..000000000 --- a/resources/assets/js/components/drop-zone.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
Drop files or click here to upload
-
\ No newline at end of file diff --git a/resources/assets/js/components/image-picker.html b/resources/assets/js/components/image-picker.html deleted file mode 100644 index 1a07b9274..000000000 --- a/resources/assets/js/components/image-picker.html +++ /dev/null @@ -1,15 +0,0 @@ - -
-
- Image Preview - Image Preview -
- -
- - - | - - - -
\ No newline at end of file diff --git a/resources/assets/js/components/toggle-switch.html b/resources/assets/js/components/toggle-switch.html deleted file mode 100644 index 455969a84..000000000 --- a/resources/assets/js/components/toggle-switch.html +++ /dev/null @@ -1,4 +0,0 @@ -
- -
-
\ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index fcaba2914..9d7f7ad70 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -1,8 +1,10 @@ "use strict"; -const moment = require('moment'); +import moment from 'moment'; +import 'moment/locale/en-gb'; +moment.locale('en-gb'); -module.exports = function (ngApp, events) { +export default function (ngApp, events) { ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', function ($scope, $attrs, $http, $timeout, imageManagerService) { @@ -17,7 +19,7 @@ module.exports = function (ngApp, events) { $scope.imageDeleteSuccess = false; $scope.uploadedTo = $attrs.uploadedTo; $scope.view = 'all'; - + $scope.searching = false; $scope.searchTerm = ''; @@ -48,7 +50,7 @@ module.exports = function (ngApp, events) { $scope.hasMore = preSearchHasMore; } $scope.cancelSearch = cancelSearch; - + /** * Runs on image upload, Adds an image to local list of images @@ -162,7 +164,6 @@ module.exports = function (ngApp, events) { /** * Start a search operation - * @param searchTerm */ $scope.searchImages = function() { @@ -196,7 +197,7 @@ module.exports = function (ngApp, events) { $scope.view = viewName; baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/'); fetchData(); - } + }; /** * Save the details of an image. @@ -205,7 +206,7 @@ module.exports = function (ngApp, events) { $scope.saveImageDetails = function (event) { event.preventDefault(); var url = window.baseUrl('/images/update/' + $scope.selectedImage.id); - $http.put(url, this.selectedImage).then((response) => { + $http.put(url, this.selectedImage).then(response => { events.emit('success', 'Image details updated'); }, (response) => { if (response.status === 422) { @@ -300,15 +301,16 @@ module.exports = function (ngApp, events) { var isEdit = pageId !== 0; var autosaveFrequency = 30; // AutoSave interval in seconds. var isMarkdown = $attrs.editorType === 'markdown'; + $scope.draftsEnabled = $attrs.draftsEnabled === 'true'; $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1; $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1; - // Set inital header draft text + // Set initial header draft text if ($scope.isUpdateDraft || $scope.isNewPageDraft) { $scope.draftText = 'Editing Draft' } else { $scope.draftText = 'Editing Page' - }; + } var autoSave = false; @@ -317,7 +319,7 @@ module.exports = function (ngApp, events) { html: false }; - if (isEdit) { + if (isEdit && $scope.draftsEnabled) { setTimeout(() => { startAutoSave(); }, 1000); @@ -336,6 +338,8 @@ module.exports = function (ngApp, events) { $scope.editorChange = function() {}; } + let lastSave = 0; + /** * Start the AutoSave loop, Checks for content change * before performing the costly AJAX request. @@ -345,6 +349,8 @@ module.exports = function (ngApp, events) { currentContent.html = $scope.editContent; autoSave = $interval(() => { + // Return if manually saved recently to prevent bombarding the server + if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return; var newTitle = $('#name').val(); var newHtml = $scope.editContent; @@ -357,10 +363,12 @@ module.exports = function (ngApp, events) { }, 1000 * autosaveFrequency); } + let draftErroring = false; /** * Save a draft update into the system via an AJAX request. */ function saveDraft() { + if (!$scope.draftsEnabled) return; var data = { name: $('#name').val(), html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent @@ -369,11 +377,17 @@ module.exports = function (ngApp, events) { if (isMarkdown) data.markdown = $scope.editContent; let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft'); - $http.put(url, data).then((responseData) => { + $http.put(url, data).then(responseData => { + draftErroring = false; var updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate(); $scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm'); if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; showDraftSaveNotification(); + lastSave = Date.now(); + }, errorRes => { + if (draftErroring) return; + events.emit('error', 'Failed to save draft. Ensure you have internet connection before saving this page.') + draftErroring = true; }); } @@ -424,7 +438,7 @@ module.exports = function (ngApp, events) { const pageId = Number($attrs.pageId); $scope.tags = []; - + $scope.sortOptions = { handle: '.handle', items: '> tr', @@ -447,7 +461,7 @@ module.exports = function (ngApp, events) { * Get all tags for the current book and add into scope. */ function getTags() { - let url = window.baseUrl('/ajax/tags/get/page/' + pageId); + let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`); $http.get(url).then((responseData) => { $scope.tags = responseData.data; addEmptyTag(); @@ -516,21 +530,201 @@ module.exports = function (ngApp, events) { }]); + + ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs', + function ($scope, $http, $attrs) { + + const pageId = $scope.uploadedTo = $attrs.pageId; + let currentOrder = ''; + $scope.files = []; + $scope.editFile = false; + $scope.file = getCleanFile(); + $scope.errors = { + link: {}, + edit: {} + }; + + function getCleanFile() { + return { + page_id: pageId + }; + } + + // Angular-UI-Sort options + $scope.sortOptions = { + handle: '.handle', + items: '> tr', + containment: "parent", + axis: "y", + stop: sortUpdate, + }; + + /** + * Event listener for sort changes. + * Updates the file ordering on the server. + * @param event + * @param ui + */ + function sortUpdate(event, ui) { + let newOrder = $scope.files.map(file => {return file.id}).join(':'); + if (newOrder === currentOrder) return; + + currentOrder = newOrder; + $http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => { + events.emit('success', resp.data.message); + }, checkError('sort')); + } + + /** + * Used by dropzone to get the endpoint to upload to. + * @returns {string} + */ + $scope.getUploadUrl = function (file) { + let suffix = (typeof file !== 'undefined') ? `/${file.id}` : ''; + return window.baseUrl(`/attachments/upload${suffix}`); + }; + + /** + * Get files for the current page from the server. + */ + function getFiles() { + let url = window.baseUrl(`/attachments/get/page/${pageId}`) + $http.get(url).then(resp => { + $scope.files = resp.data; + currentOrder = resp.data.map(file => {return file.id}).join(':'); + }, checkError('get')); + } + getFiles(); + + /** + * Runs on file upload, Adds an file to local file list + * and shows a success message to the user. + * @param file + * @param data + */ + $scope.uploadSuccess = function (file, data) { + $scope.$apply(() => { + $scope.files.push(data); + }); + events.emit('success', 'File uploaded'); + }; + + /** + * Upload and overwrite an existing file. + * @param file + * @param data + */ + $scope.uploadSuccessUpdate = function (file, data) { + $scope.$apply(() => { + let search = filesIndexOf(data); + if (search !== -1) $scope.files[search] = data; + + if ($scope.editFile) { + $scope.editFile = angular.copy(data); + data.link = ''; + } + }); + events.emit('success', 'File updated'); + }; + + /** + * Delete a file from the server and, on success, the local listing. + * @param file + */ + $scope.deleteFile = function(file) { + if (!file.deleting) { + file.deleting = true; + return; + } + $http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => { + events.emit('success', resp.data.message); + $scope.files.splice($scope.files.indexOf(file), 1); + }, checkError('delete')); + }; + + /** + * Attach a link to a page. + * @param file + */ + $scope.attachLinkSubmit = function(file) { + file.uploaded_to = pageId; + $http.post(window.baseUrl('/attachments/link'), file).then(resp => { + $scope.files.push(resp.data); + events.emit('success', 'Link attached'); + $scope.file = getCleanFile(); + }, checkError('link')); + }; + + /** + * Start the edit mode for a file. + * @param file + */ + $scope.startEdit = function(file) { + $scope.editFile = angular.copy(file); + $scope.editFile.link = (file.external) ? file.path : ''; + }; + + /** + * Cancel edit mode + */ + $scope.cancelEdit = function() { + $scope.editFile = false; + }; + + /** + * Update the name and link of a file. + * @param file + */ + $scope.updateFile = function(file) { + $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => { + let search = filesIndexOf(resp.data); + if (search !== -1) $scope.files[search] = resp.data; + + if ($scope.editFile && !file.external) { + $scope.editFile.link = ''; + } + $scope.editFile = false; + events.emit('success', 'Attachment details updated'); + }, checkError('edit')); + }; + + /** + * Get the url of a file. + */ + $scope.getFileUrl = function(file) { + return window.baseUrl('/attachments/' + file.id); + }; + + /** + * Search the local files via another file object. + * Used to search via object copies. + * @param file + * @returns int + */ + function filesIndexOf(file) { + for (let i = 0; i < $scope.files.length; i++) { + if ($scope.files[i].id == file.id) return i; + } + return -1; + } + + /** + * Check for an error response in a ajax request. + * @param errorGroupName + */ + function checkError(errorGroupName) { + $scope.errors[errorGroupName] = {}; + return function(response) { + if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { + events.emit('error', response.data.error); + } + if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') { + $scope.errors[errorGroupName] = response.data.validation; + console.log($scope.errors[errorGroupName]) + } + } + } + + }]); + }; - - - - - - - - - - - - - - - - - diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 933bbf5ff..44d1a14e1 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -2,10 +2,6 @@ const DropZone = require('dropzone'); const markdown = require('marked'); -const toggleSwitchTemplate = require('./components/toggle-switch.html'); -const imagePickerTemplate = require('./components/image-picker.html'); -const dropZoneTemplate = require('./components/drop-zone.html'); - module.exports = function (ngApp, events) { /** @@ -16,7 +12,12 @@ module.exports = function (ngApp, events) { ngApp.directive('toggleSwitch', function () { return { restrict: 'A', - template: toggleSwitchTemplate, + template: ` +
+ +
+
+ `, scope: true, link: function (scope, element, attrs) { scope.name = attrs.name; @@ -33,6 +34,59 @@ module.exports = function (ngApp, events) { }; }); + /** + * Common tab controls using simple jQuery functions. + */ + ngApp.directive('tabContainer', function() { + return { + restrict: 'A', + link: function (scope, element, attrs) { + const $content = element.find('[tab-content]'); + const $buttons = element.find('[tab-button]'); + + if (attrs.tabContainer) { + let initial = attrs.tabContainer; + $buttons.filter(`[tab-button="${initial}"]`).addClass('selected'); + $content.hide().filter(`[tab-content="${initial}"]`).show(); + } else { + $content.hide().first().show(); + $buttons.first().addClass('selected'); + } + + $buttons.click(function() { + let clickedTab = $(this); + $buttons.removeClass('selected'); + $content.hide(); + let name = clickedTab.addClass('selected').attr('tab-button'); + $content.filter(`[tab-content="${name}"]`).show(); + }); + } + }; + }); + + /** + * Sub form component to allow inner-form sections to act like thier own forms. + */ + ngApp.directive('subForm', function() { + return { + restrict: 'A', + link: function (scope, element, attrs) { + element.on('keypress', e => { + if (e.keyCode === 13) { + submitEvent(e); + } + }); + + element.find('button[type="submit"]').click(submitEvent); + + function submitEvent(e) { + e.preventDefault() + if (attrs.subForm) scope.$eval(attrs.subForm); + } + } + }; + }); + /** * Image Picker @@ -41,7 +95,22 @@ module.exports = function (ngApp, events) { ngApp.directive('imagePicker', ['$http', 'imageManagerService', function ($http, imageManagerService) { return { restrict: 'E', - template: imagePickerTemplate, + template: ` +
+
+ Image Preview + Image Preview +
+ +
+ + + | + + + +
+ `, scope: { name: '@', resizeHeight: '@', @@ -108,7 +177,11 @@ module.exports = function (ngApp, events) { ngApp.directive('dropZone', [function () { return { restrict: 'E', - template: dropZoneTemplate, + template: ` +
+
Drop files or click here to upload
+
+ `, scope: { uploadUrl: '@', eventSuccess: '=', @@ -116,6 +189,7 @@ module.exports = function (ngApp, events) { uploadedTo: '@' }, link: function (scope, element, attrs) { + if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder; var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { url: scope.uploadUrl, init: function () { @@ -488,8 +562,8 @@ module.exports = function (ngApp, events) { link: function (scope, elem, attrs) { // Get common elements - const $buttons = elem.find('[tab-button]'); - const $content = elem.find('[tab-content]'); + const $buttons = elem.find('[toolbox-tab-button]'); + const $content = elem.find('[toolbox-tab-content]'); const $toggle = elem.find('[toolbox-toggle]'); // Handle toolbox toggle click @@ -501,17 +575,17 @@ module.exports = function (ngApp, events) { function setActive(tabName, openToolbox) { $buttons.removeClass('active'); $content.hide(); - $buttons.filter(`[tab-button="${tabName}"]`).addClass('active'); - $content.filter(`[tab-content="${tabName}"]`).show(); + $buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active'); + $content.filter(`[toolbox-tab-content="${tabName}"]`).show(); if (openToolbox) elem.addClass('open'); } // Set the first tab content active on load - setActive($content.first().attr('tab-content'), false); + setActive($content.first().attr('toolbox-tab-content'), false); // Handle tab button click $buttons.click(function (e) { - let name = $(this).attr('tab-button'); + let name = $(this).attr('toolbox-tab-button'); setActive(name, true); }); } @@ -549,7 +623,7 @@ module.exports = function (ngApp, events) { let val = $input.val(); let url = $input.attr('autosuggest'); let type = $input.attr('autosuggest-type'); - + // Add name param to request if for a value if (type.toLowerCase() === 'value') { let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first(); @@ -850,17 +924,3 @@ module.exports = function (ngApp, events) { }; }]); }; - - - - - - - - - - - - - - diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 9ca335ee7..9aa5dff52 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -38,13 +38,17 @@ class EventManager { this.listeners[eventName].push(callback); return this; } -}; +} + window.Events = new EventManager(); - -var services = require('./services')(ngApp, window.Events); -var directives = require('./directives')(ngApp, window.Events); -var controllers = require('./controllers')(ngApp, window.Events); +// Load in angular specific items +import Services from './services'; +import Directives from './directives'; +import Controllers from './controllers'; +Services(ngApp, window.Events); +Directives(ngApp, window.Events); +Controllers(ngApp, window.Events); //Global jQuery Config & Extensions diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 755d558e8..1fb8b915f 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -6,11 +6,11 @@ * @param editor - editor instance */ function editorPaste(e, editor) { - if (!e.clipboardData) return + if (!e.clipboardData) return; let items = e.clipboardData.items; if (!items) return; for (let i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") === -1) return + if (items[i].type.indexOf("image") === -1) return; let file = items[i].getAsFile(); let formData = new FormData(); @@ -81,9 +81,10 @@ var mceOptions = module.exports = { toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen", content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}", style_formats: [ - {title: "Header 1", format: "h1"}, - {title: "Header 2", format: "h2"}, - {title: "Header 3", format: "h3"}, + {title: "Header Large", format: "h2"}, + {title: "Header Medium", format: "h3"}, + {title: "Header Small", format: "h4"}, + {title: "Header Tiny", format: "h5"}, {title: "Paragraph", format: "p", exact: true, classes: ''}, {title: "Blockquote", format: "blockquote"}, {title: "Code Block", icon: "code", format: "pre"}, diff --git a/resources/assets/sass/_components.scss b/resources/assets/sass/_components.scss index ccb69b44e..2f9051a52 100644 --- a/resources/assets/sass/_components.scss +++ b/resources/assets/sass/_components.scss @@ -43,10 +43,6 @@ } } -//body.ie .popup-body { -// min-height: 100%; -//} - .corner-button { position: absolute; top: 0; @@ -82,7 +78,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { min-height: 70vh; } -#image-manager .dropzone-container { +.dropzone-container { position: relative; border: 3px dashed #DDD; } @@ -456,3 +452,17 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { border-right: 6px solid transparent; border-bottom: 6px solid $negative; } + + +[tab-container] .nav-tabs { + text-align: left; + border-bottom: 1px solid #DDD; + margin-bottom: $-m; + .tab-item { + padding: $-s; + color: #666; + &.selected { + border-bottom-width: 3px; + } + } +} \ No newline at end of file diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 2658c4689..54fd55dff 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -1,5 +1,5 @@ .page-list { - h3 { + h4 { margin: $-l 0 $-xs 0; font-size: 1.666em; } @@ -11,11 +11,13 @@ overflow: hidden; margin-bottom: $-l; } - h4 { + h5 { display: block; margin: $-s 0 0 0; border-left: 5px solid $color-page; padding: $-xs 0 $-xs $-m; + font-size: 1.1em; + font-weight: normal; &.draft { border-left-color: $color-page-draft; } @@ -67,44 +69,39 @@ } } -.page-nav-list { +.sidebar-page-nav { $nav-indent: $-s; - margin-left: 2px; list-style: none; + margin: $-s 0 $-m 2px; + border-left: 2px dotted #BBB; li { - //border-left: 1px solid rgba(0, 0, 0, 0.1); - padding-left: $-xs; - border-left: 2px solid #888; + padding-left: $-s; margin-bottom: 4px; - } - li a { - color: #555; - } - .nav-H2 { - margin-left: $nav-indent; font-size: 0.95em; } - .nav-H3 { + .h1 { + margin-left: -2px; + } + .h2 { + margin-left: -2px; + } + .h3 { + margin-left: $nav-indent; + } + .h4 { margin-left: $nav-indent*2; - font-size: 0.90em } - .nav-H4 { + .h5 { margin-left: $nav-indent*3; - font-size: 0.85em } - .nav-H5 { + .h6 { margin-left: $nav-indent*4; - font-size: 0.80em - } - .nav-H6 { - margin-left: $nav-indent*5; - font-size: 0.75em } } // Sidebar list .book-tree { - padding: $-l 0 0 0; + padding: $-xs 0 0 0; position: relative; right: 0; top: 0; @@ -306,10 +303,10 @@ ul.pagination { } .entity-list { - >div { + > div { padding: $-m 0; } - h3 { + h4 { margin: 0; } p { @@ -327,9 +324,10 @@ ul.pagination { color: $color-page-draft; } } + .entity-list.compact { font-size: 0.6em; - h3, a { + h4, a { line-height: 1.2; } p { diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss old mode 100644 new mode 100755 index 42ca0a21f..880a9fdcc --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -71,6 +71,18 @@ max-width: 100%; height: auto !important; } + + // diffs + ins, + del { + text-decoration: none; + } + ins { + background: #dbffdb; + } + del { + background: #FFECEC; + } } // Page content pointers @@ -138,7 +150,6 @@ background-color: #FFF; border: 1px solid #DDD; right: $-xl*2; - z-index: 99; width: 48px; overflow: hidden; align-items: stretch; @@ -189,7 +200,7 @@ color: #444; background-color: rgba(0, 0, 0, 0.1); } - div[tab-content] { + div[toolbox-tab-content] { padding-bottom: 45px; display: flex; flex: 1; @@ -197,7 +208,7 @@ min-height: 0px; overflow-y: scroll; } - div[tab-content] .padded { + div[toolbox-tab-content] .padded { flex: 1; padding-top: 0; } @@ -216,21 +227,6 @@ padding-top: $-s; position: relative; } - button.pos { - position: absolute; - bottom: 0; - display: block; - width: 100%; - padding: $-s; - height: 45px; - border: 0; - margin: 0; - box-shadow: none; - border-radius: 0; - &:hover{ - box-shadow: none; - } - } .handle { user-select: none; cursor: move; @@ -242,9 +238,12 @@ flex-direction: column; overflow-y: scroll; } + table td, table th { + overflow: visible; + } } -[tab-content] { +[toolbox-tab-content] { display: none; } diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss index 1fc8e11c2..37c61159d 100644 --- a/resources/assets/sass/_tables.scss +++ b/resources/assets/sass/_tables.scss @@ -51,4 +51,14 @@ table.list-table { vertical-align: middle; padding: $-xs; } +} + +table.file-table { + @extend .no-style; + td { + padding: $-xs; + } + .ui-sortable-helper { + display: table; + } } \ No newline at end of file diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 8bf09a626..9bad2e83d 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -15,31 +15,41 @@ h2 { margin-bottom: 0.43137255em; } h3 { - font-size: 1.75em; + font-size: 2.333em; line-height: 1.571428572em; margin-top: 0.78571429em; margin-bottom: 0.43137255em; } h4 { - font-size: 1em; + font-size: 1.666em; line-height: 1.375em; margin-top: 0.78571429em; margin-bottom: 0.43137255em; } -h1, h2, h3, h4 { +h1, h2, h3, h4, h5, h6 { font-weight: 400; position: relative; display: block; color: #555; .subheader { - //display: block; font-size: 0.5em; line-height: 1em; color: lighten($text-dark, 32%); } } +h5 { + font-size: 1.4em; +} + +h5, h6 { + font-weight: 500; + line-height: 1.2em; + margin-top: 0.78571429em; + margin-bottom: 0.66em; +} + /* * Link styling */ @@ -183,7 +193,7 @@ p.neg, p .neg, span.neg, .text-neg { p.muted, p .muted, span.muted, .text-muted { color: lighten($text-dark, 26%); &.small, .small { - color: lighten($text-dark, 42%); + color: lighten($text-dark, 32%); } } diff --git a/resources/lang/de/activities.php b/resources/lang/de/activities.php new file mode 100644 index 000000000..c2d20b3a6 --- /dev/null +++ b/resources/lang/de/activities.php @@ -0,0 +1,40 @@ + 'Seite erstellt', + 'page_create_notification' => 'Seite erfolgreich erstellt', + 'page_update' => 'Seite aktualisiert', + 'page_update_notification' => 'Seite erfolgreich aktualisiert', + 'page_delete' => 'Seite gelöscht', + 'page_delete_notification' => 'Seite erfolgreich gelöscht', + 'page_restore' => 'Seite wiederhergstellt', + 'page_restore_notification' => 'Seite erfolgreich wiederhergstellt', + 'page_move' => 'Seite verschoben', + + // Chapters + 'chapter_create' => 'Kapitel erstellt', + 'chapter_create_notification' => 'Kapitel erfolgreich erstellt', + 'chapter_update' => 'Kapitel aktualisiert', + 'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert', + 'chapter_delete' => 'Kapitel gelöscht', + 'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht', + 'chapter_move' => 'Kapitel verschoben', + + // Books + 'book_create' => 'Buch erstellt', + 'book_create_notification' => 'Buch erfolgreich erstellt', + 'book_update' => 'Buch aktualisiert', + 'book_update_notification' => 'Buch erfolgreich aktualisiert', + 'book_delete' => 'Buch gelöscht', + 'book_delete_notification' => 'Buch erfolgreich gelöscht', + 'book_sort' => 'Buch sortiert', + 'book_sort_notification' => 'Buch erfolgreich neu sortiert', + +]; diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php new file mode 100644 index 000000000..c58d9d974 --- /dev/null +++ b/resources/lang/de/auth.php @@ -0,0 +1,26 @@ + 'Dies sind keine gültigen Anmeldedaten.', + 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.', + + /** + * Email Confirmation Text + */ + 'email_confirm_subject' => 'Bestätigen sie ihre E-Mail Adresse bei :appName', + 'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!', + 'email_confirm_text' => 'Bitte bestätigen sie ihre E-Mail Adresse, indem sie auf den Button klicken:', + 'email_confirm_action' => 'E-Mail Adresse bestätigen', + 'email_confirm_send_error' => 'Bestätigungs-E-Mail benötigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.', + 'email_confirm_success' => 'Ihre E-Mail Adresse wurde bestätigt!', + 'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen sie ihren Posteingang.', +]; diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php new file mode 100644 index 000000000..697952086 --- /dev/null +++ b/resources/lang/de/errors.php @@ -0,0 +1,12 @@ + 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.', + 'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuführen.' +]; diff --git a/resources/lang/de/pagination.php b/resources/lang/de/pagination.php new file mode 100644 index 000000000..a3bf7c8c8 --- /dev/null +++ b/resources/lang/de/pagination.php @@ -0,0 +1,19 @@ + '« Vorherige', + 'next' => 'Nächste »', + +]; diff --git a/resources/lang/de/passwords.php b/resources/lang/de/passwords.php new file mode 100644 index 000000000..f71358055 --- /dev/null +++ b/resources/lang/de/passwords.php @@ -0,0 +1,22 @@ + 'Passörter müssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.', + 'user' => "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.", + 'token' => 'Dieser Passwort-Reset-Token ist ungültig.', + 'sent' => 'Wir haben ihnen eine E-Mail mit einem Link zum ZurĂĽcksetzen des Passworts zugesendet!', + 'reset' => 'Ihr Passwort wurde zurückgesetzt!', + +]; diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php new file mode 100644 index 000000000..183480faa --- /dev/null +++ b/resources/lang/de/settings.php @@ -0,0 +1,39 @@ + 'Einstellungen', + 'settings_save' => 'Einstellungen speichern', + + 'app_settings' => 'Anwendungseinstellungen', + 'app_name' => 'Anwendungsname', + 'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.', + 'app_name_header' => 'Anwendungsname im Header anzeigen?', + 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', + 'app_secure_images' => 'Erh&oml;hte Sicherheit für Bilduploads aktivieren?', + 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.', + 'app_editor' => 'Seiteneditor', + 'app_editor_desc' => 'Wählen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.', + 'app_custom_html' => 'Benutzerdefinierter HTML Inhalt', + 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics Code hinzuzufügen.', + 'app_logo' => 'Anwendungslogo', + 'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein.
Größere Bilder werden verkleinert.', + 'app_primary_color' => 'Primäre Anwendungsfarbe', + 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein.
Leer lassen des Feldes setzt auf die Standard-Anwendungsfarbe zurück.', + + 'reg_settings' => 'Registrierungseinstellungen', + 'reg_allow' => 'Registrierung erlauben?', + 'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung', + 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?', + 'reg_confirm_email_desc' => 'Falls die Einschränkung für; Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.', + 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken', + 'reg_confirm_restrict_domain_desc' => 'Fügen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.
Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.', + 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', + +]; diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php new file mode 100644 index 000000000..3a6a1bc15 --- /dev/null +++ b/resources/lang/de/validation.php @@ -0,0 +1,108 @@ + ':attribute muss akzeptiert werden.', + 'active_url' => ':attribute ist keine valide URL.', + 'after' => ':attribute muss ein Datum nach :date sein.', + 'alpha' => ':attribute kann nur Buchstaben enthalten.', + 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', + 'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.', + 'array' => ':attribute muss eine Array sein.', + 'before' => ':attribute muss ein Datum vor :date sein.', + 'between' => [ + 'numeric' => ':attribute muss zwischen :min und :max liegen.', + 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.', + 'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.', + 'array' => ':attribute muss zwischen :min und :max Elemente enthalten.', + ], + 'boolean' => ':attribute Feld muss wahr oder falsch sein.', + 'confirmed' => ':attribute Bestätigung stimmt nicht überein.', + 'date' => ':attribute ist kein valides Datum.', + 'date_format' => ':attribute entspricht nicht dem Format :format.', + 'different' => ':attribute und :other müssen unterschiedlich sein.', + 'digits' => ':attribute muss :digits Stellen haben.', + 'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.', + 'email' => ':attribute muss eine valide E-Mail Adresse sein.', + 'filled' => ':attribute Feld ist erforderlich.', + 'exists' => 'Markiertes :attribute ist ungültig.', + 'image' => ':attribute muss ein Bild sein.', + 'in' => 'Markiertes :attribute ist ungültig.', + 'integer' => ':attribute muss eine Zahl sein.', + 'ip' => ':attribute muss eine valide IP-Adresse sein.', + 'max' => [ + 'numeric' => ':attribute darf nicht größer als :max sein.', + 'file' => ':attribute darf nicht größer als :max Kilobyte sein.', + 'string' => ':attribute darf nicht länger als :max Zeichen sein.', + 'array' => ':attribute darf nicht mehr als :max Elemente enthalten.', + ], + 'mimes' => ':attribute muss eine Datei vom Typ: :values sein.', + 'min' => [ + 'numeric' => ':attribute muss mindestens :min. sein', + 'file' => ':attribute muss mindestens :min Kilobyte groß sein.', + 'string' => ':attribute muss mindestens :min Zeichen lang sein.', + 'array' => ':attribute muss mindesten :min Elemente enthalten.', + ], + 'not_in' => 'Markiertes :attribute ist ungültig.', + 'numeric' => ':attribute muss eine Zahl sein.', + 'regex' => ':attribute Format ist ungültig.', + 'required' => ':attribute Feld ist erforderlich.', + 'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.', + 'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.', + 'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.', + 'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.', + 'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.', + 'same' => ':attribute und :other muss übereinstimmen.', + 'size' => [ + 'numeric' => ':attribute muss :size sein.', + 'file' => ':attribute muss :size Kilobytes groß sein.', + 'string' => ':attribute muss :size Zeichen lang sein.', + 'array' => ':attribute muss :size Elemente enthalten.', + ], + 'string' => ':attribute muss eine Zeichenkette sein.', + 'timezone' => ':attribute muss eine valide zeitzone sein.', + 'unique' => ':attribute wird bereits verwendet.', + 'url' => ':attribute ist kein valides Format.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [], + +]; diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php new file mode 100644 index 000000000..ffdb1cf45 --- /dev/null +++ b/resources/lang/en/auth.php @@ -0,0 +1,26 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + + /** + * Email Confirmation Text + */ + 'email_confirm_subject' => 'Confirm your email on :appName', + 'email_confirm_greeting' => 'Thanks for joining :appName!', + 'email_confirm_text' => 'Please confirm your email address by clicking the button below:', + 'email_confirm_action' => 'Confirm Email', + 'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.', + 'email_confirm_success' => 'Your email has been confirmed!', + 'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.', +]; \ No newline at end of file diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php new file mode 100644 index 000000000..1b0bcad33 --- /dev/null +++ b/resources/lang/en/settings.php @@ -0,0 +1,39 @@ + 'Settings', + 'settings_save' => 'Save Settings', + + 'app_settings' => 'App Settings', + 'app_name' => 'Application name', + 'app_name_desc' => 'This name is shown in the header and any emails.', + 'app_name_header' => 'Show Application name in header?', + 'app_public_viewing' => 'Allow public viewing?', + 'app_secure_images' => 'Enable higher security image uploads?', + 'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.', + 'app_editor' => 'Page editor', + 'app_editor_desc' => 'Select which editor will be used by all users to edit pages.', + 'app_custom_html' => 'Custom HTML head content', + 'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the section of every page. This is handy for overriding styles or adding analytics code.', + 'app_logo' => 'Application logo', + 'app_logo_desc' => 'This image should be 43px in height.
Large images will be scaled down.', + 'app_primary_color' => 'Application primary color', + 'app_primary_color_desc' => 'This should be a hex value.
Leave empty to reset to the default color.', + + 'reg_settings' => 'Registration Settings', + 'reg_allow' => 'Allow registration?', + 'reg_default_role' => 'Default user role after registration', + 'reg_confirm_email' => 'Require email confirmation?', + 'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.', + 'reg_confirm_restrict_domain' => 'Restrict registration to domain', + 'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
Note that users will be able to change their email addresses after successful registration.', + 'reg_confirm_restrict_domain_placeholder' => 'No restriction set', + +]; \ No newline at end of file diff --git a/resources/views/auth/password.blade.php b/resources/views/auth/passwords/email.blade.php similarity index 100% rename from resources/views/auth/password.blade.php rename to resources/views/auth/passwords/email.blade.php diff --git a/resources/views/auth/reset.blade.php b/resources/views/auth/passwords/reset.blade.php similarity index 100% rename from resources/views/auth/reset.blade.php rename to resources/views/auth/passwords/reset.blade.php diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index 961ead251..08acf725d 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -39,7 +39,9 @@ @if(setting('app-logo', '') !== 'none') Logo @endif - {{ setting('app-name') }} + @if (setting('app-name-header')) + {{ setting('app-name') }} + @endif
diff --git a/resources/views/books/list-item.blade.php b/resources/views/books/list-item.blade.php index 2eefdfbf5..605841f7f 100644 --- a/resources/views/books/list-item.blade.php +++ b/resources/views/books/list-item.blade.php @@ -1,5 +1,5 @@
-

{{$book->name}}

+

{{$book->name}}

@if(isset($book->searchSnippet))

{!! $book->searchSnippet !!}

@else diff --git a/resources/views/chapters/list-item.blade.php b/resources/views/chapters/list-item.blade.php index 35d3a7589..f70e59244 100644 --- a/resources/views/chapters/list-item.blade.php +++ b/resources/views/chapters/list-item.blade.php @@ -1,5 +1,5 @@
-

+

@if (isset($showPath) && $showPath) {{ $chapter->book->name }} @@ -9,7 +9,7 @@ {{ $chapter->name }} -

+ @if(isset($chapter->searchSnippet))

{!! $chapter->searchSnippet !!}

@else @@ -20,7 +20,7 @@

{{ count($chapter->pages) }} Pages

@foreach($chapter->pages as $page) -

{{$page->name}}

+
{{$page->name}}
@endforeach
@endif diff --git a/resources/views/emails/email-confirmation.blade.php b/resources/views/emails/email-confirmation.blade.php deleted file mode 100644 index 0a211c369..000000000 --- a/resources/views/emails/email-confirmation.blade.php +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - Confirm Your Email At {{ setting('app-name')}} - - - - - - - - - - - -
- -
- - - - -
-

- Email Confirmation

-

- Thanks for joining {{ setting('app-name')}}.
- Please confirm your email address by clicking the button below.

- - - - -
-

- Confirm - Email

-
-
-
- -
- - - - diff --git a/resources/views/emails/password.blade.php b/resources/views/emails/password.blade.php deleted file mode 100644 index a8f7911e0..000000000 --- a/resources/views/emails/password.blade.php +++ /dev/null @@ -1 +0,0 @@ - Password Reset From {{ setting('app-name')}}

Password Reset

A password reset was requested for this email address on {{ setting('app-name')}}. If you did not request a password change please ignore this email.

Click here to reset your password

\ No newline at end of file diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 2529c39c7..2fb4ac855 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -25,14 +25,14 @@
@if(count($draftPages) > 0) -

My Recent Drafts

+

My Recent Drafts

@include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact']) @endif
@if($signedIn) -

My Recently Viewed

+

My Recently Viewed

@else -

Recent Books

+

Recent Books

@endif @include('partials/entity-list', [ 'entities' => $recents, @@ -42,7 +42,7 @@
-

Recently Created Pages

+

Recently Created Pages

@include('partials/entity-list', [ 'entities' => $recentlyCreatedPages, @@ -51,7 +51,7 @@ ])
-

Recently Updated Pages

+

Recently Updated Pages

@include('partials/entity-list', [ 'entities' => $recentlyUpdatedPages, @@ -62,7 +62,7 @@
-

Recent Activity

+

Recent Activity

@include('partials/activity-list', ['activity' => $activity])
diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php index d39e24e92..e50cc7c5b 100644 --- a/resources/views/pages/edit.blade.php +++ b/resources/views/pages/edit.blade.php @@ -23,10 +23,4 @@ @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id]) @include('partials/entity-selector-popup') - - @stop \ No newline at end of file diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php index a03a208b6..a6e66a24a 100644 --- a/resources/views/pages/form-toolbox.blade.php +++ b/resources/views/pages/form-toolbox.blade.php @@ -3,10 +3,13 @@
- + + @if(userCan('attachment-create-all')) + + @endif
-
+

Page Tags

Add some tags to better categorise your content.
You can assign a value to a tag for more in-depth organisation.

@@ -34,4 +37,98 @@
+ @if(userCan('attachment-create-all')) +
+

Attachments

+
+ +
+

Upload some files or attach some link to display on your page. These are visible in the page sidebar. Changes here are saved instantly.

+ +
+ +
+ + + + + + + + + + +
+ +
+ Click delete again to confirm you want to delete this attachment. +
+ Cancel +
+
+

+ No files have been uploaded. +

+
+
+ +
+
+

You can attach a link if you'd prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.

+
+ + +

+
+
+ + +

+
+ + +
+
+ +
+ +
+
Edit File
+ +
+ + +

+
+ +
+ +
+ +
+
+
+
+ + +

+
+
+
+ + + +
+ +
+
+ @endif +
\ No newline at end of file diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index 0e0c3672e..c4baf38f7 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,7 +1,9 @@ -
+
{{ csrf_field() }} + + {{--Header Bar--}}
@@ -13,7 +15,7 @@
- + {{--Title input--}}
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
+ {{--Editors--}}
+ + {{--WYSIWYG Editor--}} @if(setting('app-editor') === 'wysiwyg')

-

Registration Settings

+

{{ trans('settings.reg_settings') }}

- +
- +
- -

If domain restriction is used then email confirmation will be required and the below value will be ignored.

+ +

{{ trans('settings.reg_confirm_email_desc') }}

- -

Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. -
Note that users will be able to change their email addresses after successful registration.

- + +

{!! trans('settings.reg_confirm_restrict_domain_desc') !!}

+
@@ -101,7 +109,7 @@ BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }} - +
diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 5e653f8de..78e9e1533 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -106,6 +106,19 @@ + + Attachments + @include('settings/roles/checkbox', ['permission' => 'attachment-create-all']) + Controlled by the asset they are uploaded to + + + + + + + + +
diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index d06ec09bc..6cbbdb7f7 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -15,7 +15,9 @@
diff --git a/resources/views/users/forms/system.blade.php b/resources/views/users/forms/system.blade.php new file mode 100644 index 000000000..3ee5f6409 --- /dev/null +++ b/resources/views/users/forms/system.blade.php @@ -0,0 +1,25 @@ +@if($user->system_name == 'public') +

This user represents any guest users that visit your instance. It cannot be used for logins but is assigned automatically.

+@endif + +
+ + @include('form.text', ['name' => 'name']) +
+ +
+ + @include('form.text', ['name' => 'email']) +
+ +@if(userCan('users-manage')) +
+ + @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles]) +
+@endif + +
+ Cancel + +
diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php index e72a9e61a..105fddb5b 100644 --- a/resources/views/users/index.blade.php +++ b/resources/views/users/index.blade.php @@ -22,7 +22,7 @@
- {!! $users->links() !!} + {{ $users->links() }}
@@ -76,7 +76,7 @@
- {!! $users->links() !!} + {{ $users->links() }}
diff --git a/resources/views/vendor/.gitkeep b/resources/views/vendor/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/views/vendor/notifications/email-plain.blade.php b/resources/views/vendor/notifications/email-plain.blade.php new file mode 100644 index 000000000..acefa6523 --- /dev/null +++ b/resources/views/vendor/notifications/email-plain.blade.php @@ -0,0 +1,22 @@ + + + + + + + + + + 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;', + 'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;', + + /* Masthead ----------------------- */ + + 'email-masthead' => 'padding: 25px 0; text-align: center;', + 'email-masthead_name' => 'font-size: 24px; font-weight: 400; color: #2F3133; text-decoration: none; text-shadow: 0 1px 0 white;', + + 'email-body' => 'width: 100%; margin: 0; padding: 0; border-top: 4px solid '.setting('app-color').'; border-bottom: 1px solid #EDEFF2; background-color: #FFF;', + 'email-body_inner' => 'width: auto; max-width: 100%; margin: 0 auto; padding: 0;', + 'email-body_cell' => 'padding: 35px;', + + 'email-footer' => 'width: auto; max-width: 570px; margin: 0 auto; padding: 0; text-align: center;', + 'email-footer_cell' => 'color: #AEAEAE; padding: 35px; text-align: center;', + + /* Body ------------------------------ */ + + 'body_action' => 'width: 100%; margin: 30px auto; padding: 0; text-align: center;', + 'body_sub' => 'margin-top: 25px; padding-top: 25px; border-top: 1px solid #EDEFF2;', + + /* Type ------------------------------ */ + + 'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;', + 'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;', + 'paragraph' => 'margin-top: 0; color: #74787E; font-size: 16px; line-height: 1.5em;', + 'paragraph-sub' => 'margin-top: 0; color: #74787E; font-size: 12px; line-height: 1.5em;', + 'paragraph-center' => 'text-align: center;', + + /* Buttons ------------------------------ */ + + 'button' => 'display: block; display: inline-block; width: 200px; min-height: 20px; padding: 10px; + background-color: #3869D4; border-radius: 3px; color: #ffffff; font-size: 15px; line-height: 25px; + text-align: center; text-decoration: none; -webkit-text-size-adjust: none;', + + 'button--green' => 'background-color: #22BC66;', + 'button--red' => 'background-color: #dc4d2f;', + 'button--blue' => 'background-color: '.setting('app-color').';', +]; +?> + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + +
+ + {{ setting('app-name') }} + +
+ + + + +
+ + + @if (!empty($greeting) || $level == 'error') +

+ @if (! empty($greeting)) + {{ $greeting }} + @else + @if ($level == 'error') + Whoops! + @endif + @endif +

+ @endif + + + @foreach ($introLines as $line) +

+ {{ $line }} +

+ @endforeach + + + @if (isset($actionText)) + + + + +
+ + + + {{ $actionText }} + +
+ @endif + + + @foreach ($outroLines as $line) +

+ {{ $line }} +

+ @endforeach + + + + @if (isset($actionText)) + + + + +
+

+ If you’re having trouble clicking the "{{ $actionText }}" button, + copy and paste the URL below into your web browser: +

+ +

+ + {{ $actionUrl }} + +

+
+ @endif + +
+
+ + + + +
+

+ © {{ date('Y') }} + {{ setting('app-name') }}. + All rights reserved. +

+
+
+
+
+ + diff --git a/app/Http/routes.php b/routes/web.php similarity index 76% rename from app/Http/routes.php rename to routes/web.php index eb35f2a11..28e6dccb1 100644 --- a/app/Http/routes.php +++ b/routes/web.php @@ -27,6 +27,7 @@ Route::group(['middleware' => 'auth'], function () { // Pages Route::get('/{bookSlug}/page/create', 'PageController@create'); + Route::post('/{bookSlug}/page/create/guest', 'PageController@createAsGuest'); Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft'); Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store'); Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); @@ -47,10 +48,12 @@ Route::group(['middleware' => 'auth'], function () { // 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}/changes', 'PageController@showRevisionChanges'); Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision'); // Chapters Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create'); + Route::post('/{bookSlug}/chapter/{chapterSlug}/page/create/guest', 'PageController@createAsGuest'); Route::get('/{bookSlug}/chapter/create', 'ChapterController@create'); Route::post('/{bookSlug}/chapter/create', 'ChapterController@store'); Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show'); @@ -84,6 +87,16 @@ Route::group(['middleware' => 'auth'], function () { Route::delete('/{imageId}', 'ImageController@destroy'); }); + // Attachments routes + Route::get('/attachments/{id}', 'AttachmentController@get'); + Route::post('/attachments/upload', 'AttachmentController@upload'); + Route::post('/attachments/upload/{id}', 'AttachmentController@uploadUpdate'); + Route::post('/attachments/link', 'AttachmentController@attachLink'); + Route::put('/attachments/{id}', 'AttachmentController@update'); + Route::get('/attachments/get/page/{pageId}', 'AttachmentController@listForPage'); + Route::put('/attachments/sort/page/{pageId}', 'AttachmentController@sortForPage'); + Route::delete('/attachments/{id}', 'AttachmentController@delete'); + // AJAX routes Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft'); Route::get('/ajax/page/{id}', 'PageController@getPageAjax'); @@ -139,27 +152,27 @@ Route::group(['middleware' => 'auth'], function () { }); -// Login using social authentication -Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin'); -Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback'); -Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount'); +// Social auth routes +Route::get('/login/service/{socialDriver}', 'Auth\RegisterController@getSocialLogin'); +Route::get('/login/service/{socialDriver}/callback', 'Auth\RegisterController@socialCallback'); +Route::get('/login/service/{socialDriver}/detach', 'Auth\RegisterController@detachSocialAccount'); +Route::get('/register/service/{socialDriver}', 'Auth\RegisterController@socialRegister'); // Login/Logout routes -Route::get('/login', 'Auth\AuthController@getLogin'); -Route::post('/login', 'Auth\AuthController@postLogin'); -Route::get('/logout', 'Auth\AuthController@getLogout'); -Route::get('/register', 'Auth\AuthController@getRegister'); -Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation'); -Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation'); -Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation'); -Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail'); -Route::get('/register/confirm/{token}/email', 'Auth\AuthController@viewConfirmEmail'); -Route::get('/register/service/{socialDriver}', 'Auth\AuthController@socialRegister'); -Route::post('/register', 'Auth\AuthController@postRegister'); +Route::get('/login', 'Auth\LoginController@getLogin'); +Route::post('/login', 'Auth\LoginController@login'); +Route::get('/logout', 'Auth\LoginController@logout'); +Route::get('/register', 'Auth\RegisterController@getRegister'); +Route::get('/register/confirm', 'Auth\RegisterController@getRegisterConfirmation'); +Route::get('/register/confirm/awaiting', 'Auth\RegisterController@showAwaitingConfirmation'); +Route::post('/register/confirm/resend', 'Auth\RegisterController@resendConfirmation'); +Route::get('/register/confirm/{token}', 'Auth\RegisterController@confirmEmail'); +Route::post('/register', 'Auth\RegisterController@postRegister'); // Password reset link request routes... -Route::get('/password/email', 'Auth\PasswordController@getEmail'); -Route::post('/password/email', 'Auth\PasswordController@postEmail'); +Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm'); +Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail'); + // Password reset routes... -Route::get('/password/reset/{token}', 'Auth\PasswordController@getReset'); -Route::post('/password/reset', 'Auth\PasswordController@postReset'); \ No newline at end of file +Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm'); +Route::post('/password/reset', 'Auth\ResetPasswordController@reset'); \ No newline at end of file diff --git a/public/build/.gitignore b/storage/uploads/files/.gitignore old mode 100644 new mode 100755 similarity index 100% rename from public/build/.gitignore rename to storage/uploads/files/.gitignore diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php new file mode 100644 index 000000000..df625bcc9 --- /dev/null +++ b/tests/AttachmentTest.php @@ -0,0 +1,201 @@ +getTestFile($name); + return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + } + + /** + * Get the expected upload path for a file. + * @param $fileName + * @return string + */ + protected function getUploadPath($fileName) + { + return 'uploads/files/' . Date('Y-m-M') . '/' . $fileName; + } + + /** + * Delete all uploaded files. + * To assist with cleanup. + */ + protected function deleteUploads() + { + $fileService = $this->app->make(\BookStack\Services\AttachmentService::class); + foreach (\BookStack\Attachment::all() as $file) { + $fileService->deleteFile($file); + } + } + + public function test_file_upload() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $admin = $this->getAdmin(); + $fileName = 'upload_test_file.txt'; + + $expectedResp = [ + 'name' => $fileName, + 'uploaded_to'=> $page->id, + 'extension' => 'txt', + 'order' => 1, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'path' => $this->getUploadPath($fileName) + ]; + + $this->uploadFile($fileName, $page->id); + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('attachments', $expectedResp); + + $this->deleteUploads(); + } + + public function test_file_display_and_access() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $admin = $this->getAdmin(); + $fileName = 'upload_test_file.txt'; + + $this->uploadFile($fileName, $page->id); + $this->assertResponseOk(); + $this->visit($page->getUrl()) + ->seeLink($fileName) + ->click($fileName) + ->see('Hi, This is a test file for testing the upload process.'); + + $this->deleteUploads(); + } + + public function test_attaching_link_to_page() + { + $page = \BookStack\Page::first(); + $admin = $this->getAdmin(); + $this->asAdmin(); + + $this->call('POST', 'attachments/link', [ + 'link' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + ]); + + $expectedResp = [ + 'path' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + 'created_by' => $admin->id, + 'updated_by' => $admin->id, + 'external' => true, + 'order' => 1, + 'extension' => '' + ]; + + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('attachments', $expectedResp); + + $this->visit($page->getUrl())->seeLink('Example Attachment Link') + ->click('Example Attachment Link')->seePageIs('https://example.com'); + + $this->deleteUploads(); + } + + public function test_attachment_updating() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + + $this->call('POST', 'attachments/link', [ + 'link' => 'https://example.com', + 'name' => 'Example Attachment Link', + 'uploaded_to' => $page->id, + ]); + + $attachmentId = \BookStack\Attachment::first()->id; + + $this->call('PUT', 'attachments/' . $attachmentId, [ + 'uploaded_to' => $page->id, + 'name' => 'My new attachment name', + 'link' => 'https://test.example.com' + ]); + + $expectedResp = [ + 'path' => 'https://test.example.com', + 'name' => 'My new attachment name', + 'uploaded_to' => $page->id + ]; + + $this->assertResponseOk(); + $this->seeJsonContains($expectedResp); + $this->seeInDatabase('attachments', $expectedResp); + + $this->deleteUploads(); + } + + public function test_file_deletion() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $fileName = 'deletion_test.txt'; + $this->uploadFile($fileName, $page->id); + + $filePath = base_path('storage/' . $this->getUploadPath($fileName)); + + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist'); + + $attachmentId = \BookStack\Attachment::first()->id; + $this->call('DELETE', 'attachments/' . $attachmentId); + + $this->dontSeeInDatabase('attachments', [ + 'name' => $fileName + ]); + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); + + $this->deleteUploads(); + } + + public function test_attachment_deletion_on_page_deletion() + { + $page = \BookStack\Page::first(); + $this->asAdmin(); + $fileName = 'deletion_test.txt'; + $this->uploadFile($fileName, $page->id); + + $filePath = base_path('storage/' . $this->getUploadPath($fileName)); + + $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist'); + $this->seeInDatabase('attachments', [ + 'name' => $fileName + ]); + + $this->call('DELETE', $page->getUrl()); + + $this->dontSeeInDatabase('attachments', [ + 'name' => $fileName + ]); + $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected'); + + $this->deleteUploads(); + } +} diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php index 99885d552..0d2e4ac17 100644 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@ -1,6 +1,7 @@ setSettings(['registration-enabled' => 'true', 'registration-confirmation' => 'true']); $user = factory(\BookStack\User::class)->make(); - // Mock Mailer to ensure mail is being sent - $mockMailer = Mockery::mock('Illuminate\Contracts\Mail\Mailer'); - $mockMailer->shouldReceive('send')->with('emails/email-confirmation', Mockery::type('array'), Mockery::type('callable'))->twice(); - $this->app->instance('mailer', $mockMailer); - // Go through registration process $this->visit('/register') ->see('Sign Up') @@ -76,6 +75,10 @@ class AuthTest extends TestCase ->seePageIs('/register/confirm') ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]); + // Ensure notification sent + $dbUser = \BookStack\User::where('email', '=', $user->email)->first(); + Notification::assertSentTo($dbUser, ConfirmEmail::class); + // Test access and resend confirmation email $this->login($user->email, $user->password) ->seePageIs('/register/confirm/awaiting') @@ -84,19 +87,18 @@ class AuthTest extends TestCase ->seePageIs('/register/confirm/awaiting') ->press('Resend Confirmation Email'); - // Get confirmation - $user = $user->where('email', '=', $user->email)->first(); - $emailConfirmation = EmailConfirmation::where('user_id', '=', $user->id)->first(); - - - // Check confirmation email button and confirmation activation. - $this->visit('/register/confirm/' . $emailConfirmation->token . '/email') - ->see('Email Confirmation') - ->click('Confirm Email') + // Get confirmation and confirm notification matches + $emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first(); + Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) { + return $notification->token === $emailConfirmation->token; + }); + + // Check confirmation email confirmation activation. + $this->visit('/register/confirm/' . $emailConfirmation->token) ->seePageIs('/') ->see($user->name) ->notSeeInDatabase('email_confirmations', ['token' => $emailConfirmation->token]) - ->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => true]); + ->seeInDatabase('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]); } public function test_restricted_registration() @@ -144,7 +146,7 @@ class AuthTest extends TestCase public function test_user_updating() { - $user = \BookStack\User::all()->last(); + $user = $this->getNormalUser(); $password = $user->password; $this->asAdmin() ->visit('/settings/users') @@ -160,7 +162,7 @@ class AuthTest extends TestCase public function test_user_password_update() { - $user = \BookStack\User::all()->last(); + $user = $this->getNormalUser(); $userProfilePage = '/settings/users/' . $user->id; $this->asAdmin() ->visit($userProfilePage) diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 76fbc662a..9573321fb 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -108,7 +108,7 @@ class LdapTest extends \TestCase public function test_user_edit_form() { - $editUser = User::all()->last(); + $editUser = $this->getNormalUser(); $this->asAdmin()->visit('/settings/users/' . $editUser->id) ->see('Edit User') ->dontSee('Password') @@ -126,7 +126,7 @@ class LdapTest extends \TestCase public function test_non_admins_cannot_change_auth_id() { - $testUser = User::all()->last(); + $testUser = $this->getNormalUser(); $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id) ->dontSee('External Authentication'); } diff --git a/tests/Entity/EntitySearchTest.php b/tests/Entity/EntitySearchTest.php index cfdabdb0a..60b5ceebd 100644 --- a/tests/Entity/EntitySearchTest.php +++ b/tests/Entity/EntitySearchTest.php @@ -97,6 +97,39 @@ class EntitySearchTest extends TestCase ->seeStatusCode(200); } + public function test_tag_search() + { + $newTags = [ + new \BookStack\Tag([ + 'name' => 'animal', + 'value' => 'cat' + ]), + new \BookStack\Tag([ + 'name' => 'color', + 'value' => 'red' + ]) + ]; + + $pageA = \BookStack\Page::first(); + $pageA->tags()->saveMany($newTags); + + $pageB = \BookStack\Page::all()->last(); + $pageB->tags()->create(['name' => 'animal', 'value' => 'dog']); + + $this->asAdmin()->visit('/search/all?term=%5Banimal%5D') + ->seeLink($pageA->name) + ->seeLink($pageB->name); + + $this->visit('/search/all?term=%5Bcolor%5D') + ->seeLink($pageA->name) + ->dontSeeLink($pageB->name); + + $this->visit('/search/all?term=%5Banimal%3Dcat%5D') + ->seeLink($pageA->name) + ->dontSeeLink($pageB->name); + + } + public function test_ajax_entity_search() { $page = \BookStack\Page::all()->last(); diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php index 296aa72ed..20721968f 100644 --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@ -236,8 +236,9 @@ class EntityTest extends TestCase ->type('super test page', '#name') ->press('Save Page') // Check redirect - ->seePageIs($newPageUrl) - ->visit($pageUrl) + ->seePageIs($newPageUrl); + + $this->visit($pageUrl) ->seePageIs($newPageUrl); } diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php index 108b7459f..1a46e30bc 100644 --- a/tests/Entity/PageDraftTest.php +++ b/tests/Entity/PageDraftTest.php @@ -86,7 +86,7 @@ class PageDraftTest extends TestCase ->visit($chapter->getUrl() . '/create-page') ->visit($book->getUrl()) ->seeInElement('.page-list', 'New Page'); - + $this->asAdmin() ->visit($book->getUrl()) ->dontSeeInElement('.page-list', 'New Page') diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 23373419f..031517cdb 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -10,7 +10,7 @@ class ImageTest extends TestCase */ protected function getTestImage($fileName) { - return new \Illuminate\Http\UploadedFile(base_path('tests/test-image.jpg'), $fileName, 'image/jpeg', 5238); + return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-image.jpg'), $fileName, 'image/jpeg', 5238); } /** @@ -62,7 +62,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $this->seeInDatabase('images', [ - 'url' => url($relPath), + 'url' => $this->baseUrl . $relPath, 'type' => 'gallery', 'uploaded_to' => $page->id, 'path' => $relPath, @@ -86,7 +86,7 @@ class ImageTest extends TestCase $this->assertResponseOk(); $this->dontSeeInDatabase('images', [ - 'url' => $relPath, + 'url' => $this->baseUrl . $relPath, 'type' => 'gallery' ]); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index b64f40dc6..7a0515fd9 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -544,27 +544,38 @@ class RolesTest extends TestCase ->dontSeeInElement('.book-content', $otherPage->name); } - public function test_public_role_not_visible_in_user_edit_screen() + public function test_public_role_visible_in_user_edit_screen() { $user = \BookStack\User::first(); $this->asAdmin()->visit('/settings/users/' . $user->id) ->seeElement('#roles-admin') - ->dontSeeElement('#roles-public'); + ->seeElement('#roles-public'); } - public function test_public_role_not_visible_in_role_listing() + public function test_public_role_visible_in_role_listing() { $this->asAdmin()->visit('/settings/roles') ->see('Admin') - ->dontSee('Public'); + ->see('Public'); } - public function test_public_role_not_visible_in_default_role_setting() + public function test_public_role_visible_in_default_role_setting() { $this->asAdmin()->visit('/settings') ->seeElement('[data-role-name="admin"]') - ->dontSeeElement('[data-role-name="public"]'); + ->seeElement('[data-role-name="public"]'); } + public function test_public_role_not_deleteable() + { + $this->asAdmin()->visit('/settings/roles') + ->click('Public') + ->see('Edit Role') + ->click('Delete Role') + ->press('Confirm') + ->see('Delete Role') + ->see('Cannot be deleted'); + } + } diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php new file mode 100644 index 000000000..685146423 --- /dev/null +++ b/tests/PublicActionTest.php @@ -0,0 +1,83 @@ +setSettings(['app-public' => 'false']); + $book = \BookStack\Book::orderBy('name', 'asc')->first(); + $this->visit('/books')->seePageIs('/login'); + $this->visit($book->getUrl())->seePageIs('/login'); + + $page = \BookStack\Page::first(); + $this->visit($page->getUrl())->seePageIs('/login'); + } + + public function test_books_viewable() + { + $this->setSettings(['app-public' => 'true']); + $books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get(); + $bookToVisit = $books[1]; + + // Check books index page is showing + $this->visit('/books') + ->seeStatusCode(200) + ->see($books[0]->name) + // 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 test_chapters_viewable() + { + $this->setSettings(['app-public' => 'true']); + $chapterToVisit = \BookStack\Chapter::first(); + $pageToVisit = $chapterToVisit->pages()->first(); + + // Check chapters index page is showing + $this->visit($chapterToVisit->getUrl()) + ->seeStatusCode(200) + ->see($chapterToVisit->name) + // Check individual chapter page is showing and it's child contents are visible. + ->see($pageToVisit->name) + ->click($pageToVisit->name) + ->see($chapterToVisit->book->name) + ->see($chapterToVisit->name) + ->seePageIs($pageToVisit->getUrl()); + } + + public function test_public_page_creation() + { + $this->setSettings(['app-public' => 'true']); + $publicRole = \BookStack\Role::getSystemRole('public'); + // Grant all permissions to public + $publicRole->permissions()->detach(); + foreach (\BookStack\RolePermission::all() as $perm) { + $publicRole->attachPermission($perm); + } + $this->app[\BookStack\Services\PermissionService::class]->buildJointPermissionForRole($publicRole); + + $chapter = \BookStack\Chapter::first(); + $this->visit($chapter->book->getUrl()); + $this->visit($chapter->getUrl()) + ->click('New Page') + ->see('Create Page') + ->seePageIs($chapter->getUrl('/create-page')); + + $this->submitForm('Continue', [ + 'name' => 'My guest page' + ])->seePageIs($chapter->book->getUrl('/page/my-guest-page/edit')); + + $user = \BookStack\User::getDefault(); + $this->seeInDatabase('pages', [ + 'name' => 'My guest page', + 'chapter_id' => $chapter->id, + 'created_by' => $user->id, + 'updated_by' => $user->id + ]); + } + +} \ No newline at end of file diff --git a/tests/PublicViewTest.php b/tests/PublicViewTest.php deleted file mode 100644 index 58e39dfd9..000000000 --- a/tests/PublicViewTest.php +++ /dev/null @@ -1,41 +0,0 @@ -setSettings(['app-public' => 'true']); - $books = \BookStack\Book::orderBy('name', 'asc')->take(10)->get(); - $bookToVisit = $books[1]; - - // Check books index page is showing - $this->visit('/books') - ->seeStatusCode(200) - ->see($books[0]->name) - // 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 test_chapters_viewable() - { - $this->setSettings(['app-public' => 'true']); - $chapterToVisit = \BookStack\Chapter::first(); - $pageToVisit = $chapterToVisit->pages()->first(); - - // Check chapters index page is showing - $this->visit($chapterToVisit->getUrl()) - ->seeStatusCode(200) - ->see($chapterToVisit->name) - // Check individual chapter page is showing and it's child contents are visible. - ->see($pageToVisit->name) - ->click($pageToVisit->name) - ->see($chapterToVisit->book->name) - ->see($chapterToVisit->name) - ->seePageIs($pageToVisit->getUrl()); - } - -} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 6a8c2d732..d3620eae0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -66,6 +66,14 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase return $this->actingAs($this->editor); } + /** + * Get a user that's not a system user such as the guest user. + */ + public function getNormalUser() + { + return \BookStack\User::where('system_name', '=', null)->get()->last(); + } + /** * Quickly sets an array of settings. * @param $settingsArray diff --git a/tests/UserProfileTest.php b/tests/UserProfileTest.php index 40ae004e9..9543adc1d 100644 --- a/tests/UserProfileTest.php +++ b/tests/UserProfileTest.php @@ -76,5 +76,23 @@ class UserProfileTest extends TestCase ->seePageIs('/user/' . $newUser->id) ->see($newUser->name); } + + public function test_guest_profile_shows_limited_form() + { + $this->asAdmin() + ->visit('/settings/users') + ->click('Guest') + ->dontSeeElement('#password'); + } + + public function test_guest_profile_cannot_be_deleted() + { + $guestUser = \BookStack\User::getDefault(); + $this->asAdmin()->visit('/settings/users/' . $guestUser->id . '/delete') + ->see('Delete User')->see('Guest') + ->press('Confirm') + ->seePageIs('/settings/users/' . $guestUser->id) + ->see('cannot delete the guest user'); + } } diff --git a/tests/test-data/test-file.txt b/tests/test-data/test-file.txt new file mode 100644 index 000000000..4c1f41af9 --- /dev/null +++ b/tests/test-data/test-file.txt @@ -0,0 +1 @@ +Hi, This is a test file for testing the upload process. \ No newline at end of file diff --git a/tests/test-image.jpg b/tests/test-data/test-image.jpg similarity index 100% rename from tests/test-image.jpg rename to tests/test-data/test-image.jpg diff --git a/version b/version new file mode 100644 index 000000000..8287f2896 --- /dev/null +++ b/version @@ -0,0 +1 @@ +v0.13-dev