]> BookStack Code Mirror - bookstack/commitdiff
Merge fixes from branch 'v0.12'
authorDan Brown <redacted>
Sat, 12 Nov 2016 11:40:54 +0000 (11:40 +0000)
committerDan Brown <redacted>
Sat, 12 Nov 2016 11:40:54 +0000 (11:40 +0000)
117 files changed:
.gitignore
app/EmailConfirmation.php [deleted file]
app/Entity.php
app/Events/Event.php [deleted file]
app/Exceptions/FileUploadException.php [new file with mode: 0644]
app/Exceptions/Handler.php
app/File.php [new file with mode: 0644]
app/Http/Controllers/Auth/ForgotPasswordController.php [new file with mode: 0644]
app/Http/Controllers/Auth/LoginController.php [new file with mode: 0644]
app/Http/Controllers/Auth/PasswordController.php [deleted file]
app/Http/Controllers/Auth/RegisterController.php [moved from app/Http/Controllers/Auth/AuthController.php with 66% similarity]
app/Http/Controllers/Auth/ResetPasswordController.php [new file with mode: 0644]
app/Http/Controllers/ChapterController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/FileController.php [new file with mode: 0644]
app/Http/Controllers/PageController.php
app/Http/Controllers/SettingController.php
app/Http/Controllers/UserController.php
app/Http/Kernel.php
app/Http/Middleware/Authenticate.php
app/Http/Middleware/RedirectIfAuthenticated.php
app/Jobs/Job.php [deleted file]
app/Listeners/.gitkeep [deleted file]
app/Notifications/ConfirmEmail.php [new file with mode: 0644]
app/Notifications/ResetPassword.php [new file with mode: 0644]
app/Page.php
app/PageRevision.php
app/Providers/BroadcastServiceProvider.php [new file with mode: 0644]
app/Providers/EventServiceProvider.php
app/Providers/PaginationServiceProvider.php
app/Providers/RouteServiceProvider.php
app/Repos/BookRepo.php
app/Repos/ChapterRepo.php
app/Repos/EntityRepo.php
app/Repos/PageRepo.php
app/Repos/PermissionsRepo.php
app/Repos/UserRepo.php
app/Role.php
app/Services/ActivityService.php
app/Services/EmailConfirmationService.php
app/Services/FileService.php [new file with mode: 0644]
app/Services/ImageService.php
app/Services/PermissionService.php
app/Services/SocialAuthService.php
app/Services/UploadService.php [new file with mode: 0644]
app/Services/ViewService.php
app/User.php
app/helpers.php
composer.json
composer.lock
config/app.php
config/filesystems.php
config/setting-defaults.php
database/migrations/2015_08_29_105422_add_roles_and_permissions.php
database/migrations/2016_09_29_101449_remove_hidden_roles.php [new file with mode: 0644]
database/migrations/2016_10_09_142037_create_files_table.php [new file with mode: 0644]
gulpfile.js
package.json
phpspec.yml [deleted file]
phpunit.xml
readme.md
resources/assets/js/components/drop-zone.html [deleted file]
resources/assets/js/components/image-picker.html [deleted file]
resources/assets/js/components/toggle-switch.html [deleted file]
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/js/pages/page-form.js
resources/assets/sass/_components.scss
resources/assets/sass/_lists.scss
resources/assets/sass/_pages.scss [changed mode: 0644->0755]
resources/assets/sass/_tables.scss
resources/assets/sass/_text.scss
resources/lang/en/auth.php [new file with mode: 0644]
resources/lang/en/settings.php [new file with mode: 0644]
resources/views/auth/passwords/email.blade.php [moved from resources/views/auth/password.blade.php with 100% similarity]
resources/views/auth/passwords/reset.blade.php [moved from resources/views/auth/reset.blade.php with 100% similarity]
resources/views/base.blade.php
resources/views/books/list-item.blade.php
resources/views/chapters/list-item.blade.php
resources/views/emails/email-confirmation.blade.php [deleted file]
resources/views/emails/password.blade.php [deleted file]
resources/views/home.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/form-toolbox.blade.php
resources/views/pages/form.blade.php
resources/views/pages/guest-create.blade.php [new file with mode: 0644]
resources/views/pages/list-item.blade.php
resources/views/pages/page-display.blade.php
resources/views/pages/revisions.blade.php
resources/views/pages/show.blade.php
resources/views/pages/sidebar-tree-list.blade.php
resources/views/partials/custom-styles.blade.php
resources/views/public.blade.php
resources/views/settings/index.blade.php
resources/views/settings/roles/form.blade.php
resources/views/users/edit.blade.php
resources/views/users/forms/system.blade.php [new file with mode: 0644]
resources/views/users/index.blade.php
resources/views/vendor/.gitkeep [deleted file]
resources/views/vendor/notifications/email-plain.blade.php [new file with mode: 0644]
resources/views/vendor/notifications/email.blade.php [new file with mode: 0644]
routes/web.php [moved from app/Http/routes.php with 77% similarity]
storage/uploads/files/.gitignore [moved from public/build/.gitignore with 100% similarity, mode: 0755]
tests/AttachmentTest.php [new file with mode: 0644]
tests/Auth/AuthTest.php
tests/Auth/LdapTest.php
tests/Entity/EntityTest.php
tests/Entity/PageDraftTest.php
tests/ImageTest.php
tests/Permissions/RolesTest.php
tests/PublicActionTest.php [new file with mode: 0644]
tests/PublicViewTest.php [deleted file]
tests/TestCase.php
tests/UserProfileTest.php
tests/test-data/test-file.txt [new file with mode: 0644]
tests/test-data/test-image.jpg [moved from tests/test-image.jpg with 100% similarity]
version [new file with mode: 0644]

index 69f0e2909a00e186026d2f62c46286459d74e675..362df57e13a1f29f4daf103ff8d5bf7e63267ace 100644 (file)
@@ -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/EmailConfirmation.php b/app/EmailConfirmation.php
deleted file mode 100644 (file)
index e77b754..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php namespace BookStack;
-
-class EmailConfirmation extends Model
-{
-    protected $fillable = ['user_id', 'token'];
-
-    /**
-     * Get the user that this confirmation is attached to.
-     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
-     */
-    public function user()
-    {
-        return $this->belongsTo(User::class);
-    }
-    
-}
index 2c447814f4657da11151f3cc1daf8e842bcd7658..8a15b5aa4d85a94c75d40e447248de1312eafb14 100644 (file)
@@ -162,17 +162,19 @@ 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('/&quot;.*?&quot;/', $safeTerm) || is_numeric($safeTerm)) {
-                $safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
-                $exactTerms[] = '%' . $safeTerm . '%';
+            $term = htmlentities($term, ENT_QUOTES);
+            $term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
+            if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
+                $term = str_replace('&quot;', '', $term);
+                $exactTerms[] = '%' . $term . '%';
             } else {
-                $safeTerm = '' . $safeTerm . '*';
-                if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
+                $term = '' . $term . '*';
+                if ($term !== '*') $fuzzyTerms[] = $term;
             }
         }
+
         $isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
 
         // Perform fulltext search if relevant terms exist.
@@ -193,6 +195,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 (file)
index dfe1738..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?php
-
-namespace BookStack\Events;
-
-abstract class Event
-{
-    //
-}
diff --git a/app/Exceptions/FileUploadException.php b/app/Exceptions/FileUploadException.php
new file mode 100644 (file)
index 0000000..af97607
--- /dev/null
@@ -0,0 +1,4 @@
+<?php namespace BookStack\Exceptions;
+
+
+class FileUploadException extends PrettyException {}
\ No newline at end of file
index 57e807db0e14d20014ebf13dea9469cdce87ea43..c64f0cd1fc7941ac15bba50cb7d16b0ce1817d6c 100644 (file)
@@ -87,4 +87,20 @@ class Handler extends ExceptionHandler
         } while ($e = $e->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/File.php b/app/File.php
new file mode 100644 (file)
index 0000000..e9b77d2
--- /dev/null
@@ -0,0 +1,36 @@
+<?php namespace BookStack;
+
+
+class File extends Ownable
+{
+    protected $fillable = ['name', 'order'];
+
+    /**
+     * Get the downloadable file name for this upload.
+     * @return mixed|string
+     */
+    public function getFileName()
+    {
+        if (str_contains($this->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('/files/' . $this->id);
+    }
+
+}
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
new file mode 100644 (file)
index 0000000..45e40e6
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Http\Controllers\Controller;
+use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
+use Illuminate\Http\Request;
+use Password;
+
+class ForgotPasswordController extends Controller
+{
+    /*
+    |--------------------------------------------------------------------------
+    | Password Reset Controller
+    |--------------------------------------------------------------------------
+    |
+    | This controller is responsible for handling password reset emails and
+    | includes a trait which assists in sending these notifications from
+    | your application to your users. Feel free to explore this trait.
+    |
+    */
+
+    use SendsPasswordResetEmails;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->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 (file)
index 0000000..0de4a82
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Http\Controllers\Controller;
+use BookStack\Repos\UserRepo;
+use BookStack\Services\SocialAuthService;
+use Illuminate\Contracts\Auth\Authenticatable;
+use Illuminate\Foundation\Auth\AuthenticatesUsers;
+use Illuminate\Http\Request;
+
+class LoginController extends Controller
+{
+    /*
+    |--------------------------------------------------------------------------
+    | Login Controller
+    |--------------------------------------------------------------------------
+    |
+    | This controller handles authenticating users for the application and
+    | redirecting them to your home screen. The controller uses a trait
+    | to conveniently provide its functionality to your applications.
+    |
+    */
+
+    use AuthenticatesUsers;
+
+    /**
+     * Where to redirect users after login.
+     *
+     * @var string
+     */
+    protected $redirectTo = '/';
+
+    protected $redirectPath = '/';
+    protected $redirectAfterLogout = '/login';
+
+    protected $socialAuthService;
+    protected $userRepo;
+
+    /**
+     * Create a new controller instance.
+     *
+     * @param SocialAuthService $socialAuthService
+     * @param UserRepo $userRepo
+     */
+    public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
+    {
+        $this->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 (file)
index 4dc6583..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-namespace BookStack\Http\Controllers\Auth;
-
-use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\ResetsPasswords;
-use Illuminate\Http\Request;
-use Password;
-
-class PasswordController extends Controller
-{
-    /*
-    |--------------------------------------------------------------------------
-    | Password Reset Controller
-    |--------------------------------------------------------------------------
-    |
-    | This controller is responsible for handling password reset requests
-    | and uses a simple trait to include this behavior. You're free to
-    | explore this trait and override any methods you wish to tweak.
-    |
-    */
-
-    use ResetsPasswords;
-
-    protected $redirectTo = '/';
-
-    /**
-     * Create a new password controller instance.
-     */
-    public function __construct()
-    {
-        $this->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));
-    }
-}
similarity index 66%
rename from app/Http/Controllers/Auth/AuthController.php
rename to app/Http/Controllers/Auth/RegisterController.php
index f2d3b274108616163cf55413ff77be90bb0934b8..6bba6de045f8d371e3f5b5d92d59b093ddd90df5 100644 (file)
@@ -1,62 +1,68 @@
-<?php namespace BookStack\Http\Controllers\Auth;
+<?php
 
-use BookStack\Exceptions\AuthException;
-use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Http\Request;
-use BookStack\Exceptions\SocialSignInException;
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Exceptions\ConfirmationEmailException;
 use BookStack\Exceptions\UserRegistrationException;
 use BookStack\Repos\UserRepo;
 use BookStack\Services\EmailConfirmationService;
 use BookStack\Services\SocialAuthService;
-use BookStack\SocialAccount;
+use BookStack\User;
+use Exception;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
 use Validator;
 use BookStack\Http\Controllers\Controller;
-use Illuminate\Foundation\Auth\ThrottlesLogins;
-use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
+use Illuminate\Foundation\Auth\RegistersUsers;
 
-class AuthController extends Controller
+class RegisterController extends Controller
 {
     /*
     |--------------------------------------------------------------------------
-    | Registration & Login Controller
+    | Register Controller
     |--------------------------------------------------------------------------
     |
-    | This controller handles the registration of new users, as well as the
-    | authentication of existing users. By default, this controller uses
-    | a simple trait to add these behaviors. Why don't you explore it?
+    | This controller handles the registration of new users as well as their
+    | validation and creation. By default this controller uses a trait to
+    | provide this functionality without requiring any additional code.
     |
     */
 
-    use AuthenticatesAndRegistersUsers, ThrottlesLogins;
-
-    protected $redirectPath = '/';
-    protected $redirectAfterLogout = '/login';
-    protected $username = 'email';
+    use RegistersUsers;
 
     protected $socialAuthService;
     protected $emailConfirmationService;
     protected $userRepo;
 
     /**
-     * Create a new authentication controller instance.
+     * Where to redirect users after login / registration.
+     *
+     * @var string
+     */
+    protected $redirectTo = '/';
+    protected $redirectPath = '/';
+
+    /**
+     * Create a new controller instance.
+     *
      * @param SocialAuthService $socialAuthService
      * @param EmailConfirmationService $emailConfirmationService
      * @param UserRepo $userRepo
      */
     public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
     {
-        $this->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,31 +221,17 @@ class AuthController extends Controller
             'email' => 'required|email|exists:users,email'
         ]);
         $user = $this->userRepo->getByEmail($request->get('email'));
-        $this->emailConfirmationService->sendConfirmation($user);
-        session()->flash('success', 'Confirmation email resent, Please check your inbox.');
-        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]);
-    }
+        try {
+            $this->emailConfirmationService->sendConfirmation($user);
+        } catch (Exception $e) {
+            session()->flash('error', trans('auth.email_confirm_send_error'));
+            return redirect('/register/confirm');
+        }
 
-    /**
-     * 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);
+        $this->emailConfirmationService->sendConfirmation($user);
+        session()->flash('success', trans('auth.email_confirm_resent'));
+        return redirect('/register/confirm');
     }
 
     /**
@@ -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 (file)
index 0000000..bd64793
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Http\Controllers\Controller;
+use Illuminate\Foundation\Auth\ResetsPasswords;
+
+class ResetPasswordController extends Controller
+{
+    /*
+    |--------------------------------------------------------------------------
+    | Password Reset Controller
+    |--------------------------------------------------------------------------
+    |
+    | This controller is responsible for handling password reset requests
+    | and uses a simple trait to include this behavior. You're free to
+    | explore this trait and override any methods you wish to tweak.
+    |
+    */
+
+    use ResetsPasswords;
+
+    protected $redirectTo = '/';
+
+    /**
+     * Create a new controller instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->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
index 03ec2c1109973ed3013fb0d579e65a6abfdbb558..57ca58beba9750d9ba3082b57f9202a20f1a2506 100644 (file)
@@ -117,7 +117,7 @@ class ChapterController extends Controller
         $this->checkOwnablePermission('chapter-update', $chapter);
         $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());
index 26eeb3002aeebecd9ec74796f428fe9eb2132a5a..2b6c88fe0b73748ab5cbdf61a64138dcdc4d5c49 100644 (file)
@@ -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/FileController.php b/app/Http/Controllers/FileController.php
new file mode 100644 (file)
index 0000000..668e9ec
--- /dev/null
@@ -0,0 +1,214 @@
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Exceptions\FileUploadException;
+use BookStack\File;
+use BookStack\Repos\PageRepo;
+use BookStack\Services\FileService;
+use Illuminate\Http\Request;
+
+use BookStack\Http\Requests;
+
+class FileController extends Controller
+{
+    protected $fileService;
+    protected $file;
+    protected $pageRepo;
+
+    /**
+     * FileController constructor.
+     * @param FileService $fileService
+     * @param File $file
+     * @param PageRepo $pageRepo
+     */
+    public function __construct(FileService $fileService, File $file, PageRepo $pageRepo)
+    {
+        $this->fileService = $fileService;
+        $this->file = $file;
+        $this->pageRepo = $pageRepo;
+    }
+
+
+    /**
+     * Endpoint at which files are uploaded to.
+     * @param Request $request
+     */
+    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);
+
+        $this->checkPermission('file-create-all');
+        $this->checkOwnablePermission('page-update', $page);
+
+        $uploadedFile = $request->file('file');
+
+        try {
+            $file = $this->fileService->saveNewUpload($uploadedFile, $pageId);
+        } catch (FileUploadException $e) {
+            return response($e->getMessage(), 500);
+        }
+
+        return response()->json($file);
+    }
+
+    /**
+     * Update an uploaded file.
+     * @param int $fileId
+     * @param Request $request
+     * @return mixed
+     */
+    public function uploadUpdate($fileId, 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);
+        $file = $this->file->findOrFail($fileId);
+
+        $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('file-create', $file);
+        
+        if (intval($pageId) !== intval($file->uploaded_to)) {
+            return $this->jsonError('Page mismatch during attached file update');
+        }
+
+        $uploadedFile = $request->file('file');
+
+        try {
+            $file = $this->fileService->saveUpdatedUpload($uploadedFile, $file);
+        } catch (FileUploadException $e) {
+            return response($e->getMessage(), 500);
+        }
+
+        return response()->json($file);
+    }
+
+    /**
+     * Update the details of an existing file.
+     * @param $fileId
+     * @param Request $request
+     * @return File|mixed
+     */
+    public function update($fileId, 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);
+        $file = $this->file->findOrFail($fileId);
+
+        $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('file-create', $file);
+
+        if (intval($pageId) !== intval($file->uploaded_to)) {
+            return $this->jsonError('Page mismatch during attachment update');
+        }
+
+        $file = $this->fileService->updateFile($file, $request->all());
+        return $file;
+    }
+
+    /**
+     * Attach a link to a page as a file.
+     * @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);
+
+        $this->checkPermission('file-create-all');
+        $this->checkOwnablePermission('page-update', $page);
+
+        $fileName = $request->get('name');
+        $link = $request->get('link');
+        $file = $this->fileService->saveNewFromLink($fileName, $link, $pageId);
+
+        return response()->json($file);
+    }
+
+    /**
+     * Get the files for a specific page.
+     * @param $pageId
+     * @return mixed
+     */
+    public function listForPage($pageId)
+    {
+        $page = $this->pageRepo->getById($pageId);
+        $this->checkOwnablePermission('page-view', $page);
+        return response()->json($page->files);
+    }
+
+    /**
+     * Update the file 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);
+
+        $files = $request->get('files');
+        $this->fileService->updateFileOrderWithinPage($files, $pageId);
+        return response()->json(['message' => 'Attachment order updated']);
+    }
+
+    /**
+     * Get a file from storage.
+     * @param $fileId
+     */
+    public function get($fileId)
+    {
+        $file = $this->file->findOrFail($fileId);
+        $page = $this->pageRepo->getById($file->uploaded_to);
+        $this->checkOwnablePermission('page-view', $page);
+
+        if ($file->external) {
+            return redirect($file->path);
+        }
+
+        $fileContents = $this->fileService->getFile($file);
+        return response($fileContents, 200, [
+            'Content-Type' => 'application/octet-stream',
+            'Content-Disposition' => 'attachment; filename="'. $file->getFileName() .'"'
+        ]);
+    }
+
+    /**
+     * Delete a specific file in the system.
+     * @param $fileId
+     * @return mixed
+     */
+    public function delete($fileId)
+    {
+        $file = $this->file->findOrFail($fileId);
+        $this->checkOwnablePermission('file-delete', $file);
+        $this->fileService->deleteFile($file);
+        return response()->json(['message' => 'Attachment deleted']);
+    }
+}
index 1509ace9563b7aee65fe3b8c167f5e07117bacf6..c2d8e257cb33297e28b49c448075017de8a0ad70 100644 (file)
@@ -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);
+
+        // 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);
 
-        $draft = $this->pageRepo->getDraftPage($book, $chapter);
-        return redirect($draft->getUrl());
+        $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
      */
index 61ce55fa9405921c65cd5684c33fb4b42cb9d0d1..65135eda3816079eea164c229acd89d9e57fe9ad 100644 (file)
@@ -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]);
     }
index 053d9ebd57345017cb9f8faf22986de52335cf63..18ef1a671844fc4943e7df9673f62de42bcc5cea 100644 (file)
@@ -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');
 
index a1f2a581fb6c5038179dfa0be03cfc494150adbf..f1d95f5c070da381114dad0f3c41a4b69495ec90 100644 (file)
@@ -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,
index 372f30bf64f545957cd3a8fb2a63e27416ec441f..8461ed0ba18365574911168e9a3e091a1504f2ac 100644 (file)
@@ -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')) {
index ab8ce4d3a3243620902476957fca28bed55343a3..2b3c64695735d44daf3c6f47f76d4aa59584f4c2 100644 (file)
@@ -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 (file)
index 780af74..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-namespace BookStack\Jobs;
-
-use Illuminate\Bus\Queueable;
-
-abstract class Job
-{
-    /*
-    |--------------------------------------------------------------------------
-    | Queueable Jobs
-    |--------------------------------------------------------------------------
-    |
-    | This job base class provides a central location to place any logic that
-    | is shared across all of your jobs. The trait included with the class
-    | provides access to the "queueOn" and "delay" queue helper methods.
-    |
-    */
-
-    use Queueable;
-}
diff --git a/app/Listeners/.gitkeep b/app/Listeners/.gitkeep
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/app/Notifications/ConfirmEmail.php b/app/Notifications/ConfirmEmail.php
new file mode 100644 (file)
index 0000000..64d9bb9
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+namespace BookStack\Notifications;
+
+use Illuminate\Notifications\Notification;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class ConfirmEmail extends Notification
+{
+
+    public $token;
+
+    /**
+     * Create a new notification instance.
+     * @param string $token
+     */
+    public function __construct($token)
+    {
+        $this->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 (file)
index 0000000..646030a
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+namespace BookStack\Notifications;
+
+use Illuminate\Notifications\Notification;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class ResetPassword extends Notification
+{
+    /**
+     * The password reset token.
+     *
+     * @var string
+     */
+    public $token;
+
+    /**
+     * Create a notification instance.
+     *
+     * @param  string  $token
+     */
+    public function __construct($token)
+    {
+        $this->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.');
+    }
+}
index 1961a4f7f6bc2ec0e66824397df51c2ec8652f8e..94bcce2b590d104a905b0da0be36e934d8156258 100644 (file)
@@ -54,6 +54,15 @@ class Page extends Entity
         return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
     }
 
+    /**
+     * Get the files attached to this page.
+     * @return \Illuminate\Database\Eloquent\Relations\HasMany
+     */
+    public function files()
+    {
+        return $this->hasMany(File::class, 'uploaded_to')->orderBy('order', 'asc');
+    }
+
     /**
      * Get the url for this page.
      * @param string|bool $path
index 1ffd63dbd584700376c983e39a4f4c1c8ce36800..ff469f0ed3a78b3d3750c3ff6d07b6b2d787c226 100644 (file)
@@ -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 (file)
index 0000000..11e3cc6
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace BookStack\Providers;
+
+use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Broadcast;
+
+class BroadcastServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap any application services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+//        Broadcast::routes();
+//
+//        /*
+//         * Authenticate the user's personal channel...
+//         */
+//        Broadcast::channel('BookStack.User.*', function ($user, $userId) {
+//            return (int) $user->id === (int) $userId;
+//        });
+    }
+}
index f754b87a9c43c6f7bfd66ff67a3e1ebcb84fd668..3802f20c0de0ba7c4c9697c8736b4598feed3538 100644 (file)
@@ -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();
     }
 }
index a0e97f70db42a508f1fcf1e1114e0ad5681654f7..ec41267a4ba24f99b45be4fe8d984ed112c0bdd7 100644 (file)
@@ -1,11 +1,12 @@
 <?php namespace BookStack\Providers;
 
 
-use Illuminate\Support\ServiceProvider;
+use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
 use Illuminate\Pagination\Paginator;
 
-class PaginationServiceProvider extends ServiceProvider
+class PaginationServiceProvider extends IlluminatePaginationServiceProvider
 {
+
     /**
      * Register the service provider.
      *
@@ -13,6 +14,10 @@ class PaginationServiceProvider extends ServiceProvider
      */
     public function register()
     {
+        Paginator::viewFactoryResolver(function () {
+            return $this->app['view'];
+        });
+
         Paginator::currentPathResolver(function () {
             return baseUrl($this->app['request']->path());
         });
index 2d9cd3b85daaec24a8e18f9e45c64819ed2d9d58..88ab23526e6c54aae08b992909859109e3e31e5b 100644 (file)
@@ -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()
+    {
+        $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()
     {
-        $router->group(['namespace' => $this->namespace], function ($router) {
-            require app_path('Http/routes.php');
+        Route::group([
+            'middleware' => 'api',
+            'namespace' => $this->namespace,
+            'prefix' => 'api',
+        ], function ($router) {
+            require base_path('routes/api.php');
         });
     }
 }
index fdc4dd8d47fc9c2611e6dba07f4c1fb6bf470da2..1af02035dd1797c75797bef710e1cf76307a104e 100644 (file)
@@ -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;
@@ -149,7 +149,7 @@ class BookRepo extends EntityRepo
     {
         $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;
index c12a9f0f206811eec5b14dd1e27e04f20ccf5275..96f5b2d1ea75d9c47858988b77d4489f3351be11 100644 (file)
@@ -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;
index c94601738dc2f926b8ea0775982a8c3467efbba6..42b0b6b7bef89aecfbecc0f914fde8266ec7eb5e 100644 (file)
@@ -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();
     }
index 235246f823ef9696e1e97e934da79605dffe31b2..8cd5c35a9d6ac87ae1eaac6ff70914ac64889b69 100644 (file)
@@ -5,8 +5,10 @@ use BookStack\Book;
 use BookStack\Chapter;
 use BookStack\Entity;
 use BookStack\Exceptions\NotFoundException;
+use BookStack\Services\FileService;
 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)
     {
@@ -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)
     {
@@ -626,6 +634,13 @@ class PageRepo extends EntityRepo
         $page->revisions()->delete();
         $page->permissions()->delete();
         $this->permissionService->deleteJointPermissionsForEntity($page);
+
+        // Delete AttachedFiles
+        $fileService = app(FileService::class);
+        foreach ($page->files as $file) {
+            $fileService->deleteFile($file);
+        }
+
         $page->delete();
     }
 
index e026d83e8e3fa8e88f8dab8acdf6422e3e39d333..24497c91135fa036825bcb180e88df65ba099727 100644 (file)
@@ -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);
 
index 0926f630471c24e1583b937e91ce74f2414d5da9..ab3716fca027b857de43f239691b931f6cb95b05 100644 (file)
@@ -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
index 8d0a79e753316e8070255da2181c072eaafdf6bc..bf9685ee25d597f35edb3403f82a31d33c220651 100644 (file)
@@ -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)
     {
index f6fea33a191ee2d1e02b7e63d394653d8b53322e..e4103623835af2d9c3c170a1a66e962cafb9743e 100644 (file)
@@ -19,7 +19,7 @@ class ActivityService
     {
         $this->activity = $activity;
         $this->permissionService = $permissionService;
-        $this->user = auth()->user();
+        $this->user = user();
     }
 
     /**
index c3096c654fb0fb0e1853d6ec5961b8f9df78d07a..d4ec1e9760242a4b10b889c506a8690633eebfe0 100644 (file)
@@ -1,30 +1,27 @@
 <?php namespace BookStack\Services;
 
-
+use BookStack\Notifications\ConfirmEmail;
+use BookStack\Repos\UserRepo;
 use Carbon\Carbon;
-use Illuminate\Contracts\Mail\Mailer;
-use Illuminate\Mail\Message;
-use BookStack\EmailConfirmation;
 use BookStack\Exceptions\ConfirmationEmailException;
 use BookStack\Exceptions\UserRegistrationException;
-use BookStack\Repos\UserRepo;
-use BookStack\Setting;
 use BookStack\User;
+use Illuminate\Database\Connection as Database;
 
 class EmailConfirmationService
 {
-    protected $mailer;
-    protected $emailConfirmation;
+    protected $db;
+    protected $users;
 
     /**
      * EmailConfirmationService constructor.
-     * @param Mailer            $mailer
-     * @param EmailConfirmation $emailConfirmation
+     * @param Database $db
+     * @param UserRepo $users
      */
-    public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
+    public function __construct(Database $db, UserRepo $users)
     {
-        $this->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/FileService.php b/app/Services/FileService.php
new file mode 100644 (file)
index 0000000..261695e
--- /dev/null
@@ -0,0 +1,204 @@
+<?php namespace BookStack\Services;
+
+
+use BookStack\Exceptions\FileUploadException;
+use BookStack\File;
+use Exception;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
+use Illuminate\Support\Collection;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+class FileService extends UploadService
+{
+
+    /**
+     * Get a file from storage.
+     * @param File $file
+     * @return string
+     */
+    public function getFile(File $file)
+    {
+        $filePath = $this->getStorageBasePath() . $file->path;
+        return $this->getStorage()->get($filePath);
+    }
+
+    /**
+     * Store a new file upon user upload.
+     * @param UploadedFile $uploadedFile
+     * @param int $page_id
+     * @return File
+     * @throws FileUploadException
+     */
+    public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
+    {
+        $fileName = $uploadedFile->getClientOriginalName();
+        $filePath = $this->putFileInStorage($fileName, $uploadedFile);
+        $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order');
+
+        $file = File::forceCreate([
+            'name' => $fileName,
+            'path' => $filePath,
+            'extension' => $uploadedFile->getClientOriginalExtension(),
+            'uploaded_to' => $page_id,
+            'created_by' => user()->id,
+            'updated_by' => user()->id,
+            'order' => $largestExistingOrder + 1
+        ]);
+
+        return $file;
+    }
+
+    /**
+     * Store a upload, saving to a file and deleting any existing uploads
+     * attached to that file.
+     * @param UploadedFile $uploadedFile
+     * @param File $file
+     * @return File
+     * @throws FileUploadException
+     */
+    public function saveUpdatedUpload(UploadedFile $uploadedFile, File $file)
+    {
+        if (!$file->external) {
+            $this->deleteFileInStorage($file);
+        }
+
+        $fileName = $uploadedFile->getClientOriginalName();
+        $filePath = $this->putFileInStorage($fileName, $uploadedFile);
+
+        $file->name = $fileName;
+        $file->path = $filePath;
+        $file->external = false;
+        $file->extension = $uploadedFile->getClientOriginalExtension();
+        $file->save();
+        return $file;
+    }
+
+    /**
+     * Save a new File attachment from a given link and name.
+     * @param string $name
+     * @param string $link
+     * @param int $page_id
+     * @return File
+     */
+    public function saveNewFromLink($name, $link, $page_id)
+    {
+        $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order');
+        return File::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 $fileList
+     * @param $pageId
+     */
+    public function updateFileOrderWithinPage($fileList, $pageId)
+    {
+        foreach ($fileList as $index => $file) {
+            File::where('uploaded_to', '=', $pageId)->where('id', '=', $file['id'])->update(['order' => $index]);
+        }
+    }
+
+
+    /**
+     * Update the details of a file.
+     * @param File $file
+     * @param $requestData
+     * @return File
+     */
+    public function updateFile(File $file, $requestData)
+    {
+        $file->name = $requestData['name'];
+        if (isset($requestData['link']) && trim($requestData['link']) !== '') {
+            $file->path = $requestData['link'];
+            if (!$file->external) {
+                $this->deleteFileInStorage($file);
+                $file->external = true;
+            }
+        }
+        $file->save();
+        return $file;
+    }
+
+    /**
+     * Delete a File from the database and storage.
+     * @param File $file
+     */
+    public function deleteFile(File $file)
+    {
+        if ($file->external) {
+            $file->delete();
+            return;
+        }
+        
+        $this->deleteFileInStorage($file);
+        $file->delete();
+    }
+
+    /**
+     * Delete a file from the filesystem it sits on.
+     * Cleans any empty leftover folders.
+     * @param File $file
+     */
+    protected function deleteFileInStorage(File $file)
+    {
+        $storedFilePath = $this->getStorageBasePath() . $file->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 $fileName
+     * @param UploadedFile $uploadedFile
+     * @return string
+     * @throws FileUploadException
+     */
+    protected function putFileInStorage($fileName, UploadedFile $uploadedFile)
+    {
+        $fileData = file_get_contents($uploadedFile->getRealPath());
+
+        $storage = $this->getStorage();
+        $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
+        $storageBasePath = $this->getStorageBasePath() . $fileBasePath;
+
+        $uploadFileName = $fileName;
+        while ($storage->exists($storageBasePath . $uploadFileName)) {
+            $uploadFileName = str_random(3) . $uploadFileName;
+        }
+
+        $filePath = $fileBasePath . $uploadFileName;
+        $fileStoragePath = $this->getStorageBasePath() . $filePath;
+
+        try {
+            $storage->put($fileStoragePath, $fileData);
+        } catch (Exception $e) {
+            throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
+        }
+        return $filePath;
+    }
+
+}
\ No newline at end of file
index 4401cb230a8b4105ba5458be1e4e88f596803ccb..dfe2cf453705e07a7b311b73b0f2bffaa28bfb6a 100644 (file)
@@ -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;
     }
 
index cee074cd7cd09b1f36278cef042443cb05166181..bb78f0b0a2c7481cdd9be119d6d2436eb6e16c90 100644 (file)
@@ -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
index b28a97ea43660b7cd3829cd6958e3090e20dde68..d76a7231b8049396c1be994a16eea4b5c7500fb6 100644 (file)
@@ -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 (file)
index 0000000..44d4bb4
--- /dev/null
@@ -0,0 +1,64 @@
+<?php namespace BookStack\Services;
+
+use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
+
+class UploadService
+{
+
+    /**
+     * @var FileSystem
+     */
+    protected $fileSystem;
+
+    /**
+     * @var FileSystemInstance
+     */
+    protected $storageInstance;
+
+
+    /**
+     * FileService constructor.
+     * @param $fileSystem
+     */
+    public function __construct(FileSystem $fileSystem)
+    {
+        $this->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
index aac9831f74cac2ffee9c84c19f6e94e18934bb34..1a9ee5f707fa8a2eae3264ad284d5a77c96df3f3 100644 (file)
@@ -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');
index 32449971d7847f8e4d0dc880d3868fd8eb2c528a..09b189cbb55e084419309c3b6f154e77ed28d4ee 100644 (file)
@@ -1,13 +1,16 @@
 <?php namespace BookStack;
 
+use BookStack\Notifications\ResetPassword;
 use Illuminate\Auth\Authenticatable;
 use Illuminate\Auth\Passwords\CanResetPassword;
 use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
 use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Notifications\Notifiable;
 
 class User extends Model implements AuthenticatableContract, CanResetPasswordContract
 {
-    use Authenticatable, CanResetPassword;
+    use Authenticatable, CanResetPassword, Notifiable;
 
     /**
      * The database table used by the model.
@@ -34,21 +37,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
     protected $permissions;
 
     /**
-     * Returns a default guest user.
+     * Returns the default public user.
+     * @return User
      */
     public static function getDefault()
     {
-        return new static([
-            'email' => '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));
+    }
 }
index c12708877c4aa73266ceedde18127e62763983c0..b5be0fd11bae196ccd085eb8e127ea3f0045d466 100644 (file)
@@ -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 $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);
+    static $version = null;
+
+    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
+}
index 5c77a68c4afd3de50cd34e2ffd1d5f51d2cd0cbb..7d4b5e62b03a1132b39efe3d3f39fe9b2f47bf59 100644 (file)
@@ -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": [
         ]
     },
     "scripts": {
+        "post-root-package-install": [
+            "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+        ],
+        "post-create-project-cmd": [
+            "php artisan key:generate"
+        ],
         "post-install-cmd": [
-            "php artisan clear-compiled",
+            "Illuminate\\Foundation\\ComposerScripts::postInstall",
             "php artisan optimize"
         ],
-        "pre-update-cmd": [
-            "php artisan clear-compiled"
-        ],
         "post-update-cmd": [
+            "Illuminate\\Foundation\\ComposerScripts::postUpdate",
             "php artisan optimize"
-        ],
-        "post-root-package-install": [
-            "php -r \"copy('.env.example', '.env');\""
-        ],
-        "post-create-project-cmd": [
-            "php artisan key:generate"
         ]
     },
     "config": {
index 63d378753f569263af1c35c402ea43168822dbc1..74a0902882376a9d16fc1a8ac18f5398a0c1c33f 100644 (file)
@@ -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"
             },
                 "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": {
                 "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": {
                 "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)"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.1-dev"
+                    "dev-master": "2.2-dev"
                 }
             },
             "autoload": {
                 "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": "[email protected]"
+                }
+            ],
+            "time": "2016-06-13 19:28:20"
         },
         {
             "name": "classpreloader/classpreloader",
             ],
             "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": "[email protected]"
+                },
+                {
+                    "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",
         },
         {
             "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": [
                 {
                 {
                     "name": "Brian Sweeney",
                     "email": "[email protected]"
+                },
+                {
+                    "name": "Gabriel Bull",
+                    "email": "[email protected]"
                 }
             ],
             "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/",
             ],
             "authors": [
                 {
-                    "name": "Michael Dowling",
-                    "email": "[email protected]",
-                    "homepage": "https://github.com/mtdowling"
+                    "name": "Andrew Cairns",
+                    "email": "[email protected]"
                 },
                 {
-                    "name": "Guzzle Community",
-                    "homepage": "https://github.com/guzzle/guzzle/contributors"
+                    "name": "Mathew Chapman",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Peter Legierski",
+                    "email": "[email protected]"
                 }
             ],
-            "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": {
                 "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": {
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "1.4-dev"
                 }
             },
             "autoload": {
                 "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": {
                 "thumbnail",
                 "watermark"
             ],
-            "time": "2016-02-26 18:18:19"
+            "time": "2016-09-01 17:04:03"
         },
         {
             "name": "jakub-onderka/php-console-color",
         },
         {
             "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": {
                 "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": {
                 "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",
                 "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).",
                 "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"
             "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": {
             },
             "require-dev": {
                 "mockery/mockery": "~0.9",
-                "phpunit/phpunit": "~4.0"
+                "phpunit/phpunit": "~4.0|~5.0"
             },
             "type": "library",
             "extra": {
                 "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": {
                 "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",
                 "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": {
                 }
             ],
             "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": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.11-dev"
+                    "dev-master": "1.13-dev"
                 }
             },
             "autoload": {
                 "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": {
                 "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": {
                 "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": {
                 "logging",
                 "psr-3"
             ],
-            "time": "2016-04-02 13:12:58"
+            "time": "2016-07-29 03:23:52"
         },
         {
             "name": "mtdowling/cron-expression",
         },
         {
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0-dev"
+                    "dev-master": "2.1-dev"
                 }
             },
             "autoload": {
                 "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": {
                 "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": [
                 {
             ],
             "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",
                     "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": {
                 }
             ],
             "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
             "keywords": [
                 "http",
                 "http-message",
                 "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/",
                 }
             ],
             "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",
             ],
             "time": "2016-03-09 05:03:14"
         },
+        {
+            "name": "ramsey/uuid",
+            "version": "3.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ramsey/uuid.git",
+                "reference": "a6d15c8618ea3951fd54d34e326b68d3d0bc0786"
+            },
+            "dist": {
+                "type": "zip",
+                "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": "[email protected]"
+                },
+                {
+                    "name": "Thibaud Fabre",
+                    "email": "[email protected]"
+                },
+                {
+                    "name": "Ben Ramsey",
+                    "email": "[email protected]",
+                    "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.1",
+            "version": "v5.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/swiftmailer/swiftmailer.git",
-                "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421"
+                "reference": "4cc92842069c2bbc1f28daaaf1d2576ec4dfe153"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/0697e6aa65c83edf97bb0f23d8763f94e3f11421",
-                "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421",
+                "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": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
                 "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"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1-dev"
+                    "dev-master": "1.2-dev"
                 }
             },
             "autoload": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1-dev"
+                    "dev-master": "1.2-dev"
                 }
             },
             "autoload": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.1-dev"
+                    "dev-master": "1.2-dev"
                 }
             },
             "autoload": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
                 "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
                 "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": {
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
-                "BSD"
+                "BSD-3-Clause-Attribution"
             ],
             "authors": [
                 {
                 }
             ],
             "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": [
         },
         {
             "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": {
                 "faker",
                 "fixtures"
             ],
-            "time": "2015-05-29 06:29:14"
+            "time": "2016-04-29 12:21:54"
         },
         {
             "name": "hamcrest/hamcrest-php",
         },
         {
             "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": {
                 "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"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "homepage": "https://github.com/myclabs/DeepCopy",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "time": "2016-09-16 13:37:59"
+        },
+        {
+            "name": "phpdocumentor/reflection-common",
+            "version": "1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+                "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^4.6"
+            },
+            "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": "Chris Boulton",
-                    "homepage": "http://github.com/chrisboulton",
-                    "role": "Original developer"
+                    "name": "Jaap van Otterdijk",
+                    "email": "[email protected]"
                 }
             ],
-            "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).",
-            "time": "2013-11-01 13:02:21"
+            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+            "homepage": "http://www.phpdoc.org",
+            "keywords": [
+                "FQSEN",
+                "phpDocumentor",
+                "phpdoc",
+                "reflection",
+                "static analysis"
+            ],
+            "time": "2015-12-27 11:43:31"
         },
         {
-            "name": "phpspec/phpspec",
-            "version": "2.5.0",
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
-                "url": "https://github.com/phpspec/phpspec.git",
-                "reference": "385ecb015e97c13818074f1517928b24d4a26067"
+                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+                "reference": "9270140b940ff02e58ec577c237274e92cd40cdd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/phpspec/zipball/385ecb015e97c13818074f1517928b24d4a26067",
-                "reference": "385ecb015e97c13818074f1517928b24d4a26067",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9270140b940ff02e58ec577c237274e92cd40cdd",
+                "reference": "9270140b940ff02e58ec577c237274e92cd40cdd",
                 "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",
+                "phpdocumentor/reflection-common": "^1.0@dev",
+                "phpdocumentor/type-resolver": "^0.2.0",
+                "webmozart/assert": "^1.0"
             },
             "require-dev": {
-                "behat/behat": "^3.0.11",
-                "bossa/phpspec2-expect": "~1.0",
-                "phpunit/phpunit": "~4.4",
-                "symfony/filesystem": "~2.1|~3.0"
+                "mockery/mockery": "^0.9.4",
+                "phpunit/phpunit": "^4.4"
             },
-            "suggest": {
-                "phpspec/nyan-formatters": "~1.0 â€“ Adds Nyan formatters"
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": [
+                        "src/"
+                    ]
+                }
             },
-            "bin": [
-                "bin/phpspec"
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
             ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
+                }
+            ],
+            "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": "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/",
             ],
             "authors": [
                 {
-                    "name": "Konstantin Kudryashov",
-                    "email": "[email protected]",
-                    "homepage": "http://everzet.com"
-                },
-                {
-                    "name": "Marcello Duarte",
-                    "homepage": "http://marcelloduarte.net/"
+                    "name": "Mike van Riel",
+                    "email": "[email protected]"
                 }
             ],
-            "description": "Specification-oriented BDD framework for PHP 5.3+",
-            "homepage": "http://phpspec.net/",
-            "keywords": [
-                "BDD",
-                "SpecBDD",
-                "TDD",
-                "spec",
-                "specification",
-                "testing",
-                "tests"
-            ],
-            "time": "2016-03-20 20:34:32"
+            "time": "2016-06-10 07:14:17"
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.6.0",
+            "version": "v1.6.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972"
+                "reference": "58a8137754bc24b25740d4281399a4a3596058e0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972",
-                "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972",
+                "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": {
                 "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": {
                 "testing",
                 "xunit"
             ],
-            "time": "2015-10-06 15:47:00"
+            "time": "2016-07-26 14:39:29"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "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": [
             "keywords": [
                 "timer"
             ],
-            "time": "2015-06-21 08:01:12"
+            "time": "2016-05-12 18:03:57"
         },
         {
             "name": "phpunit/php-token-stream",
         },
         {
             "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": [
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.8.x-dev"
+                    "dev-master": "5.5.x-dev"
                 }
             },
             "autoload": {
                 "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": "*"
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.3.x-dev"
+                    "dev-master": "3.2.x-dev"
                 }
             },
             "autoload": {
                 "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": "[email protected]"
+                }
+            ],
+            "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",
         },
         {
             "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": {
                 "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": {
                 "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": {
                 "export",
                 "exporter"
             ],
-            "time": "2015-06-21 07:55:53"
+            "time": "2016-06-17 09:04:28"
         },
         {
             "name": "sebastian/global-state",
             ],
             "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": "[email protected]"
+                }
+            ],
+            "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",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
             "time": "2015-11-11 19:50:13"
         },
+        {
+            "name": "sebastian/resource-operations",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/resource-operations.git",
+                "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+            },
+            "dist": {
+                "type": "zip",
+                "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": "[email protected]"
+                }
+            ],
+            "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": "1.0.6",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/version.git",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6"
+                "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
-                "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6",
+                "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/"
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.0-dev"
+                    "dev-master": "3.1-dev"
                 }
             },
             "autoload": {
             ],
             "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": "[email protected]"
+                }
+            ],
+            "description": "Assertions to validate method input/output with nice error messages.",
+            "keywords": [
+                "assert",
+                "check",
+                "validate"
+            ],
+            "time": "2016-08-09 15:02:57"
         }
     ],
     "aliases": [],
     "prefer-stable": false,
     "prefer-lowest": false,
     "platform": {
-        "php": ">=5.5.9"
+        "php": ">=5.6.4",
+        "ext-tidy": "*"
     },
     "platform-dev": []
 }
index 0d6f6f2b0744dd8ad37eda1196ac6de239263337..a5b0d2fe0e325399440d6e2250035fd1412f7e6f 100644 (file)
@@ -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,
index dbcb03db12d4d3b13d941529370bf729ad412b20..836f68d3d4224ece88983a5c4d4b7b859bc45b50 100644 (file)
@@ -56,7 +56,7 @@ return [
 
         'local' => [
             'driver' => 'local',
-            'root'   => public_path(),
+            'root'   => base_path(),
         ],
 
         'ftp' => [
index deafceb29c2e774cee42900e8cc47739a4fffec8..c681bb7f55ddbe97e6ba73b29154373e18d2db55 100644 (file)
@@ -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)',
index 763a33fec4b46b96a621730220331bf7d066919c..47a77e29f95a20e59c830b70ba98b260637fda31 100644 (file)
@@ -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 (file)
index 0000000..f666cad
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class RemoveHiddenRoles extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        // Remove the hidden property from roles
+        Schema::table('roles', function(Blueprint $table) {
+            $table->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' => '[email protected]',
+            '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_files_table.php b/database/migrations/2016_10_09_142037_create_files_table.php
new file mode 100644 (file)
index 0000000..49433f1
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class CreateFilesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('files', function (Blueprint $table) {
+            $table->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 = 'File';
+        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('files');
+
+        // Create & attach new entity permissions
+        $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
+        $entity = 'File';
+        foreach ($ops as $op) {
+            $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
+            DB::table('role_permissions')->where('name', '=', $permName)->delete();
+        }
+    }
+}
index 7deefc71ac2f99b3c27b8c1039b0178ddff98f66..9d789d9b4c40c425cd0e198b504e0aa616abc55a 100644 (file)
@@ -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');
 });
index fde090beb73afa28bfadf2d89d6884c908981bad..30f288d451b132ec4944a4d2238e3aa5c3c484b3 100644 (file)
@@ -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 (file)
index 58f1d98..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-suites:
-    main:
-        namespace: BookStack
-        psr4_prefix: BookStack
-        src_path: app
\ No newline at end of file
index a2b26d4132f87c11ba63d470009ae15d6f559eaf..72e06a3fc53f0df5c07d39cf2ff2cd465681f6d9 100644 (file)
@@ -30,6 +30,7 @@
         <env name="AUTH_METHOD" value="standard"/>
         <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
         <env name="LDAP_VERSION" value="3"/>
+        <env name="STORAGE_TYPE" value="local"/>
         <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
         <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
         <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
index 3a745beb11ad293919f4db1096253f27b36f48e6..5d3e79a2e079b3673210e3c5c33d96cfe1c257fb 100644 (file)
--- 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: `[email protected]`. Password: `password`)*
+* [Demo Instance](https://demo.bookstackapp.com)
+  * *Username: `[email protected]`*
+  * *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 (file)
index 26e0ee2..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="dropzone-container">
-    <div class="dz-message">Drop files or click here to upload</div>
-</div>
\ 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 (file)
index 1a07b92..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-
-<div class="image-picker">
-    <div>
-        <img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
-        <img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
-    </div>
-    <button class="button" type="button" ng-click="showImageManager()">Select Image</button>
-    <br>
-
-    <button class="text-button" ng-click="reset()" type="button">Reset</button>
-    <span ng-show="showRemove" class="sep">|</span>
-    <button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
-
-    <input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
-</div>
\ 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 (file)
index 455969a..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
-    <input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
-    <div class="switch-handle"></div>
-</div>
\ No newline at end of file
index fcaba2914b8557bac98a33fe5c5ce5c32bb37355..f4f1f3e3926a0e576cd9460fbecc0cd7497e2de0 100644 (file)
@@ -1,6 +1,8 @@
 "use strict";
 
-const moment = require('moment');
+import moment from 'moment';
+import 'moment/locale/en-gb';
+moment.locale('en-gb');
 
 module.exports = function (ngApp, events) {
 
@@ -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
@@ -300,6 +302,7 @@ 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;
 
@@ -317,7 +320,7 @@ module.exports = function (ngApp, events) {
             html: false
         };
 
-        if (isEdit) {
+        if (isEdit && $scope.draftsEnabled) {
             setTimeout(() => {
                 startAutoSave();
             }, 1000);
@@ -336,6 +339,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 +350,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 +364,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 +378,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 +439,7 @@ module.exports = function (ngApp, events) {
 
             const pageId = Number($attrs.pageId);
             $scope.tags = [];
-            
+
             $scope.sortOptions = {
                 handle: '.handle',
                 items: '> tr',
@@ -447,7 +462,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 +531,203 @@ 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(`/files/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(`/files/upload${suffix}`);
+            };
 
+            /**
+             * Get files for the current page from the server.
+             */
+            function getFiles() {
+                let url = window.baseUrl(`/files/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(`/files/${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 fileName
+             * @param fileLink
+             */
+            $scope.attachLinkSubmit = function(file) {
+                file.uploaded_to = pageId;
+                $http.post('/files/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 fileId
+             */
+            $scope.startEdit = function(file) {
+                console.log(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(`/files/${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('/files/' + 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 response
+             */
+            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])
+                    }
+                }
+            }
+
+        }]);
+
+};
index 933bbf5ff5e96c0ad9631a39f72d5f558e880b49..44d1a14e1a139a5bf794aef36a93062d8da9aa14 100644 (file)
@@ -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: `
+            <div class="toggle-switch" ng-click="switch()" ng-class="{'active': isActive}">
+                <input type="hidden" ng-attr-name="{{name}}" ng-attr-value="{{value}}"/>
+                <div class="switch-handle"></div>
+            </div>
+            `,
             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: `
+            <div class="image-picker">
+                <div>
+                    <img ng-if="image && image !== 'none'" ng-src="{{image}}" ng-class="{{imageClass}}" alt="Image Preview">
+                    <img ng-if="image === '' && defaultImage" ng-src="{{defaultImage}}" ng-class="{{imageClass}}" alt="Image Preview">
+                </div>
+                <button class="button" type="button" ng-click="showImageManager()">Select Image</button>
+                <br>
+
+                <button class="text-button" ng-click="reset()" type="button">Reset</button>
+                <span ng-show="showRemove" class="sep">|</span>
+                <button ng-show="showRemove" class="text-button neg" ng-click="remove()" type="button">Remove</button>
+
+                <input type="hidden" ng-attr-name="{{name}}" ng-attr-id="{{name}}" ng-attr-value="{{value}}">
+            </div>
+            `,
             scope: {
                 name: '@',
                 resizeHeight: '@',
@@ -108,7 +177,11 @@ module.exports = function (ngApp, events) {
     ngApp.directive('dropZone', [function () {
         return {
             restrict: 'E',
-            template: dropZoneTemplate,
+            template: `
+            <div class="dropzone-container">
+                <div class="dz-message">Drop files or click here to upload</div>
+            </div>
+            `,
             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) {
         };
     }]);
 };
-
-
-
-
-
-
-
-
-
-
-
-
-
-
index 755d558e83ffcde90d3f349e6568fc5da163f3c2..c1e6a92df26941dc683b92e3990f38aabb8a2bf0 100644 (file)
@@ -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"},
index ccb69b44e3f91ddef14344a8290678b504cb51d3..2f9051a5258e94d9a821fd71c73aac30569c1501 100644 (file)
   }
 }
 
-//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
index 2658c46891bf75ebc99ec9553897be9e93bfa60b..54fd55dff6f075637a2d943a431e05a05a6458fc 100644 (file)
@@ -1,5 +1,5 @@
 .page-list {
-  h3 {
+  h4 {
     margin: $-l 0 $-xs 0;
     font-size: 1.666em;
   }
     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;
     }
   }
 }
 
-.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;
+    font-size: 0.95em;
   }
-  li a {
-    color: #555;
+  .h1 {
+    margin-left: -2px;
   }
-  .nav-H2 {
+  .h2 {
+    margin-left: -2px;
+  }
+  .h3 {
     margin-left: $nav-indent;
-    font-size: 0.95em;
   }
-  .nav-H3 {
+  .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 {
old mode 100644 (file)
new mode 100755 (executable)
index 42ca0a2..c7d3e03
     max-width: 100%;
     height: auto !important;
   }
+
+  // diffs
+  ins,
+  del {
+    text-decoration: none;
+  }
+  ins {
+    background: #dbffdb;
+  }
+  del {
+    background: #FFECEC;
+  }
 }
 
 // Page content pointers
   background-color: #FFF;
   border: 1px solid #DDD;
   right: $-xl*2;
-  z-index: 99;
   width: 48px;
   overflow: hidden;
   align-items: stretch;
     color: #444;
     background-color: rgba(0, 0, 0, 0.1);
   }
-  div[tab-content] {
+  div[toolbox-tab-content] {
     padding-bottom: 45px;
     display: flex;
     flex: 1;
     min-height: 0px;
     overflow-y: scroll;
   }
-  div[tab-content] .padded {
+  div[toolbox-tab-content] .padded {
     flex: 1;
     padding-top: 0;
   }
     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;
   }
 }
 
-[tab-content] {
+[toolbox-tab-content] {
   display: none;
 }
 
index 1fc8e11c2266a14ec2edfe70f2fa19fb87832ede..37c61159db077488d7b7d9ed3988bf81a20aa7f2 100644 (file)
@@ -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
index 8bf09a626f01528c17d6f337ae60ed26c2a6b316..e81061685a1ab3afc1d37a8fdb4fab78a4fe0987 100644 (file)
@@ -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
  */
diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php
new file mode 100644 (file)
index 0000000..ffdb1cf
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+return [
+    /*
+    |--------------------------------------------------------------------------
+    | Authentication Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are used during authentication for various
+    | messages that we need to display to the user. You are free to modify
+    | these language lines according to your application's requirements.
+    |
+    */
+    'failed' => '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 (file)
index 0000000..1b0bcad
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+return [
+    
+    /**
+     * Settings text strings
+     * Contains all text strings used in the general settings sections of BookStack
+     * including users and roles.
+     */
+    
+    'settings' => '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 <head> 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. <br>Large images will be scaled down.',
+    'app_primary_color' => 'Application primary color',
+    'app_primary_color_desc' => 'This should be a hex value. <br>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. <br> 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
index 961ead251bfdd79f9f886ed43639228c7e0d9da2..08acf725d95ea013b54fcd0742060d87e9d571ed 100644 (file)
@@ -39,7 +39,9 @@
                         @if(setting('app-logo', '') !== 'none')
                             <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
                         @endif
-                        <span class="logo-text">{{ setting('app-name') }}</span>
+                        @if (setting('app-name-header'))
+                            <span class="logo-text">{{ setting('app-name') }}</span>
+                        @endif
                     </a>
                 </div>
                 <div class="col-lg-4 col-sm-3 text-center">
index 2eefdfbf531c0a9917cc2606a344e32402863ffd..605841f7f0ae31ada7c4c9f39d5b96757b7e9e0b 100644 (file)
@@ -1,5 +1,5 @@
 <div class="book entity-list-item"  data-entity-type="book" data-entity-id="{{$book->id}}">
-    <h3 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h3>
+    <h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h4>
     @if(isset($book->searchSnippet))
         <p class="text-muted">{!! $book->searchSnippet !!}</p>
     @else
index 35d3a7589d927b313e8e8488c8ff2490cc82a289..f70e592448f00068e554a9332f5ae5a3faf7f903 100644 (file)
@@ -1,5 +1,5 @@
 <div class="chapter entity-list-item" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
-    <h3>
+    <h4>
         @if (isset($showPath) && $showPath)
             <a href="{{ $chapter->book->getUrl() }}" class="text-book">
                 <i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
@@ -9,7 +9,7 @@
         <a href="{{ $chapter->getUrl() }}" class="text-chapter entity-list-item-link">
             <i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
         </a>
-    </h3>
+    </h4>
     @if(isset($chapter->searchSnippet))
         <p class="text-muted">{!! $chapter->searchSnippet !!}</p>
     @else
@@ -20,7 +20,7 @@
         <p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ count($chapter->pages) }} Pages</span></p>
         <div class="inset-list">
             @foreach($chapter->pages as $page)
-                <h4 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h4>
+                <h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>
             @endforeach
         </div>
     @endif
diff --git a/resources/views/emails/email-confirmation.blade.php b/resources/views/emails/email-confirmation.blade.php
deleted file mode 100644 (file)
index 0a211c3..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
-        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml"
-      style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-
-<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-    <meta name="viewport" content="width=device-width"
-          style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"
-          style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>
-    <title>Confirm Your Email At {{ setting('app-name')}}</title>
-    <style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-        * {
-            margin: 0;
-            padding: 0;
-            font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
-            font-size: 100%;
-            line-height: 1.6;
-        }
-
-        img {
-            max-width: 100%;
-        }
-
-        body {
-            -webkit-font-smoothing: antialiased;
-            -webkit-text-size-adjust: none;
-            width: 100% !important;
-            height: 100%;
-        }
-
-        a {
-            color: #348eda;
-        }
-
-        .btn-primary {
-            text-decoration: none;
-            color: #FFF;
-            background-color: #348eda;
-            border: solid #348eda;
-            border-width: 10px 20px;
-            line-height: 2;
-            font-weight: bold;
-            margin-right: 10px;
-            text-align: center;
-            cursor: pointer;
-            display: inline-block;
-            border-radius: 4px;
-        }
-
-        .btn-secondary {
-            text-decoration: none;
-            color: #FFF;
-            background-color: #aaa;
-            border: solid #aaa;
-            border-width: 10px 20px;
-            line-height: 2;
-            font-weight: bold;
-            margin-right: 10px;
-            text-align: center;
-            cursor: pointer;
-            display: inline-block;
-            border-radius: 25px;
-        }
-
-        .last {
-            margin-bottom: 0;
-        }
-
-        .first {
-            margin-top: 0;
-        }
-
-        .padding {
-            padding: 10px 0;
-        }
-
-        table.body-wrap {
-            width: 100%;
-            padding: 20px;
-        }
-
-        table.body-wrap .container {
-            border: 1px solid #f0f0f0;
-        }
-
-        h1,
-        h2,
-        h3 {
-            font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
-            color: #444;
-            margin: 10px 0 10px;
-            line-height: 1.2;
-            font-weight: 200;
-        }
-
-        h1 {
-            font-size: 36px;
-        }
-
-        h2 {
-            font-size: 28px;
-        }
-
-        h3 {
-            font-size: 22px;
-        }
-
-        p,
-        ul,
-        ol {
-            margin-bottom: 10px;
-            font-weight: normal;
-            font-size: 14px;
-            color: #888888;
-        }
-
-        ul li,
-        ol li {
-            margin-left: 5px;
-            list-style-position: inside;
-        }
-
-        .container {
-            display: block !important;
-            max-width: 600px !important;
-            margin: 0 auto !important;
-            clear: both !important;
-        }
-
-        .body-wrap .container {
-            padding: 20px;
-        }
-
-        .content {
-            max-width: 600px;
-            margin: 0 auto;
-            display: block;
-        }
-
-        .content table {
-            width: 100%;
-        }
-    </style>
-</head>
-
-<body bgcolor="#f6f6f6"
-      style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;">
-<!-- body -->
-<table class="body-wrap" bgcolor="#f6f6f6"
-       style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;">
-    <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
-        <td class="container" bgcolor="#FFFFFF"
-            style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;">
-            <!-- content -->
-            <div class="content"
-                 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;">
-                <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
-                    <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-                        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-                            <h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;">
-                                Email Confirmation</h1>
-                            <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
-                                Thanks for joining <a href="{{ baseUrl('/', true) }}">{{ setting('app-name')}}</a>. <br/>
-                                Please confirm your email address by clicking the button below.</p>
-                            <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">
-                                <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">
-                                    <td class="padding"
-                                        style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;">
-                                        <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">
-                                            <a class="btn-primary" href="{{ baseUrl('/register/confirm/' . $token, true) }}"
-                                               style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Confirm
-                                                Email</a></p>
-                                    </td>
-                                </tr>
-                            </table>
-                        </td>
-                    </tr>
-                </table>
-            </div>
-            <!-- /content -->
-        </td>
-        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>
-    </tr>
-</table>
-<!-- /body -->
-</body>
-
-</html>
diff --git a/resources/views/emails/password.blade.php b/resources/views/emails/password.blade.php
deleted file mode 100644 (file)
index a8f7911..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"\r        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r<html xmlns="http://www.w3.org/1999/xhtml"\r      style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r\r<head style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r    <meta name="viewport" content="width=device-width"\r          style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>\r    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"\r          style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"/>\r    <title>Password Reset From {{ setting('app-name')}}</title>\r    <style style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r        * {\r            margin: 0;\r            padding: 0;\r            font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;\r            font-size: 100%;\r            line-height: 1.6;\r        }\r\r        img {\r            max-width: 100%;\r        }\r\r        body {\r            -webkit-font-smoothing: antialiased;\r            -webkit-text-size-adjust: none;\r            width: 100% !important;\r            height: 100%;\r        }\r\r        a {\r            color: #348eda;\r        }\r\r        .btn-primary {\r            text-decoration: none;\r            color: #FFF;\r            background-color: #348eda;\r            border: solid #348eda;\r            border-width: 10px 20px;\r            line-height: 2;\r            font-weight: bold;\r            margin-right: 10px;\r            text-align: center;\r            cursor: pointer;\r            display: inline-block;\r            border-radius: 4px;\r        }\r\r        .btn-secondary {\r            text-decoration: none;\r            color: #FFF;\r            background-color: #aaa;\r            border: solid #aaa;\r            border-width: 10px 20px;\r            line-height: 2;\r            font-weight: bold;\r            margin-right: 10px;\r            text-align: center;\r            cursor: pointer;\r            display: inline-block;\r            border-radius: 25px;\r        }\r\r        .last {\r            margin-bottom: 0;\r        }\r\r        .first {\r            margin-top: 0;\r        }\r\r        .padding {\r            padding: 10px 0;\r        }\r\r        table.body-wrap {\r            width: 100%;\r            padding: 20px;\r        }\r\r        table.body-wrap .container {\r            border: 1px solid #f0f0f0;\r        }\r\r        h1,\r        h2,\r        h3 {\r            font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;\r            color: #444;\r            margin: 10px 0 10px;\r            line-height: 1.2;\r            font-weight: 200;\r        }\r\r        h1 {\r            font-size: 36px;\r        }\r\r        h2 {\r            font-size: 28px;\r        }\r\r        h3 {\r            font-size: 22px;\r        }\r\r        p,\r        ul,\r        ol {\r            margin-bottom: 10px;\r            font-weight: normal;\r            font-size: 14px;\r            color: #888888;\r        }\r\r        ul li,\r        ol li {\r            margin-left: 5px;\r            list-style-position: inside;\r        }\r\r        .container {\r            display: block !important;\r            max-width: 600px !important;\r            margin: 0 auto !important;\r            clear: both !important;\r        }\r\r        .body-wrap .container {\r            padding: 20px;\r        }\r\r        .content {\r            max-width: 600px;\r            margin: 0 auto;\r            display: block;\r        }\r\r        .content table {\r            width: 100%;\r        }\r    </style>\r</head>\r\r<body bgcolor="#f6f6f6"\r      style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;width:100%!important;height:100%;">\r<!-- body -->\r<table class="body-wrap" bgcolor="#f6f6f6"\r       style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;">\r    <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>\r        <td class="container" bgcolor="#FFFFFF"\r            style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;display:block!important;max-width:600px!important;margin-top:0 !important;margin-bottom:0 !important;margin-right:auto !important;margin-left:auto !important;clear:both!important;padding-top:20px;padding-bottom:20px;padding-right:20px;padding-left:20px;border-width:1px;border-style:solid;border-color:#f0f0f0;">\r            <!-- content -->\r            <div class="content"\r                 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;max-width:600px;margin-top:0;margin-bottom:0;margin-right:auto;margin-left:auto;display:block;">\r                <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">\r                    <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r                        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r                            <h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;color:#444;margin-top:10px;margin-bottom:10px;margin-right:0;margin-left:0;line-height:1.2;font-weight:200;font-size:36px;">\r                                Password Reset</h1>\r                            <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">\r                                A password reset was requested for this email address on <a\r                                        href="{{ baseUrl('/', true) }}">{{ setting('app-name')}}</a>. If you did not request\r                                a password change please ignore this email.</p>\r                            <table style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;width:100%;">\r                                <tr style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;">\r                                    <td class="padding"\r                                        style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;padding-top:10px;padding-bottom:10px;padding-right:0;padding-left:0;">\r                                        <p style="margin-top:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;line-height:1.6;margin-bottom:10px;font-weight:normal;font-size:14px;color:#888888;">\r                                            <a class="btn-primary" href="{{ baseUrl('/password/reset/' . $token, true) }}"\r                                               style="margin-top:0;margin-bottom:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;text-decoration:none;color:#FFF;background-color:#348eda;border-style:solid;border-color:#348eda;border-width:10px 20px;line-height:2;font-weight:bold;margin-right:10px;text-align:center;cursor:pointer;display:inline-block;border-radius:4px;">Click\r                                                here to reset your password</a></p>\r                                    </td>\r                                </tr>\r                            </table>\r                        </td>\r                    </tr>\r                </table>\r            </div>\r            <!-- /content -->\r        </td>\r        <td style="margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6;"></td>\r    </tr>\r</table>\r<!-- /body -->\r</body>\r\r</html>\r
\ No newline at end of file
index 2529c39c7f77fb81b5b3cc082ade0b2ec422bd28..2fb4ac85597cca57cc0f26d6e86bde9a55efb881 100644 (file)
             <div class="col-sm-4">
                 <div id="recent-drafts">
                     @if(count($draftPages) > 0)
-                        <h3>My Recent Drafts</h3>
+                        <h4>My Recent Drafts</h4>
                         @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
                     @endif
                 </div>
                 @if($signedIn)
-                    <h3>My Recently Viewed</h3>
+                    <h4>My Recently Viewed</h4>
                 @else
-                    <h3>Recent Books</h3>
+                    <h4>Recent Books</h4>
                 @endif
                 @include('partials/entity-list', [
                 'entities' => $recents,
@@ -42,7 +42,7 @@
             </div>
 
             <div class="col-sm-4">
-                <h3><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h3>
+                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-created") }}">Recently Created Pages</a></h4>
                 <div id="recently-created-pages">
                     @include('partials/entity-list', [
                     'entities' => $recentlyCreatedPages,
@@ -51,7 +51,7 @@
                     ])
                 </div>
 
-                <h3><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h3>
+                <h4><a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">Recently Updated Pages</a></h4>
                 <div id="recently-updated-pages">
                     @include('partials/entity-list', [
                     'entities' => $recentlyUpdatedPages,
@@ -62,7 +62,7 @@
             </div>
 
             <div class="col-sm-4" id="recent-activity">
-                <h3>Recent Activity</h3>
+                <h4>Recent Activity</h4>
                 @include('partials/activity-list', ['activity' => $activity])
             </div>
 
index d39e24e920aaca0927493b6b05457c5e60275fdf..e50cc7c5bb2dea098b3d1c2f7f5685e62fd2da30 100644 (file)
     @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
     @include('partials/entity-selector-popup')
 
-    <script>
-        (function() {
-
-        })();
-    </script>
-
 @stop
\ No newline at end of file
index a03a208b6539440cb2cdfa4b8adad76db3b63904..78e485eabffdf54467733ed7271f5b5a09f79967 100644 (file)
@@ -3,10 +3,13 @@
 
     <div class="tabs primary-background-light">
         <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
-        <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
+        <span toolbox-tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
+        @if(userCan('file-create-all'))
+            <span toolbox-tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
+        @endif
     </div>
 
-    <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
+    <div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
         <h4>Page Tags</h4>
         <div class="padded tags">
             <p class="muted small">Add some tags to better categorise your content. <br> You can assign a value to a tag for more in-depth organisation.</p>
         </div>
     </div>
 
+    @if(userCan('file-create-all'))
+        <div toolbox-tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
+            <h4>Attachments</h4>
+            <div class="padded files">
+
+                <div id="file-list" ng-show="!editFile">
+                    <p class="muted small">Upload some files or attach some link to display on your page. This are visible in the page sidebar.</p>
+
+                    <div tab-container>
+                        <div class="nav-tabs">
+                            <div tab-button="list" class="tab-item">File List</div>
+                            <div tab-button="file" class="tab-item">Upload File</div>
+                            <div tab-button="link" class="tab-item">Attach Link</div>
+                        </div>
+                        <div tab-content="list">
+                            <table class="file-table" style="width: 100%;">
+                                <tbody ui-sortable="sortOptions" ng-model="files" >
+                                <tr ng-repeat="file in files track by $index">
+                                    <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
+                                    <td>
+                                        <a ng-href="@{{getFileUrl(file)}}" target="_blank" ng-bind="file.name"></a>
+                                        <div ng-if="file.deleting">
+                                            <span class="neg small">Click delete again to confirm you want to delete this attachment.</span>
+                                            <br>
+                                            <span class="text-primary small" ng-click="file.deleting=false;">Cancel</span>
+                                        </div>
+                                    </td>
+                                    <td width="10" ng-click="startEdit(file)" class="text-center text-primary" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
+                                    <td width="5"></td>
+                                    <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
+                                </tr>
+                                </tbody>
+                            </table>
+                            <p class="small muted" ng-if="files.length == 0">
+                                No files have been uploaded.
+                            </p>
+                        </div>
+                        <div tab-content="file">
+                            <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
+                        </div>
+                        <div tab-content="link" sub-form="attachLinkSubmit(file)">
+                            <p class="muted small">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.</p>
+                            <div class="form-group">
+                                <label for="attachment-via-link">Link Name</label>
+                                <input type="text" placeholder="Link name" ng-model="file.name">
+                                <p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
+                            </div>
+                            <div class="form-group">
+                                <label for="attachment-via-link">Link to file</label>
+                                <input type="text" placeholder="Url of site or file" ng-model="file.link">
+                                <p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
+                            </div>
+                            <button type="submit" class="button pos">Attach</button>
+
+                        </div>
+                    </div>
+
+                </div>
+
+                <div id="file-edit" ng-if="editFile" sub-form="updateFile(editFile)">
+                    <h5>Edit File</h5>
+
+                    <div class="form-group">
+                        <label for="attachment-name-edit">File Name</label>
+                        <input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
+                        <p class="small neg" ng-repeat="error in errors.edit.name" ng-bind="error"></p>
+                    </div>
+
+                    <div tab-container="@{{ editFile.external ? 'link' : 'file' }}">
+                        <div class="nav-tabs">
+                            <div tab-button="file" class="tab-item">Upload File</div>
+                            <div tab-button="link" class="tab-item">Set Link</div>
+                        </div>
+                        <div tab-content="file">
+                            <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
+                            <br>
+                        </div>
+                        <div tab-content="link">
+                            <div class="form-group">
+                                <label for="attachment-link-edit">Link to file</label>
+                                <input type="text" id="attachment-link-edit" placeholder="Attachment link" ng-model="editFile.link">
+                                <p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
+                            </div>
+                        </div>
+                    </div>
+
+                    <button type="button" class="button" ng-click="cancelEdit()">Back</button>
+                    <button type="submit" class="button pos">Save</button>
+                </div>
+
+            </div>
+        </div>
+    @endif
+
 </div>
\ No newline at end of file
index 0e0c3672e9df6f25c8518a4258a97cec22319b00..c4baf38f75b352046eb8181b4873c1e624c82169 100644 (file)
@@ -1,7 +1,9 @@
 
-<div class="page-editor flex-fill flex" ng-controller="PageEditController" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
+<div class="page-editor flex-fill flex" ng-controller="PageEditController" drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" editor-type="{{ setting('app-editor') }}" page-id="{{ $model->id or 0 }}" page-new-draft="{{ $model->draft or 0 }}" page-update-draft="{{ $model->isDraft or 0 }}">
 
     {{ csrf_field() }}
+
+    {{--Header Bar--}}
     <div class="faded-small toolbar">
         <div class="container">
             <div class="row">
@@ -13,7 +15,7 @@
                 </div>
                 <div class="col-sm-4 faded text-center">
 
-                    <div dropdown class="dropdown-container draft-display">
+                    <div ng-show="draftsEnabled" dropdown class="dropdown-container draft-display">
                         <a dropdown-toggle class="text-primary text-button"><span class="faded-text" ng-bind="draftText"></span>&nbsp; <i class="zmdi zmdi-more-vert"></i></a>
                         <i class="zmdi zmdi-check-circle text-pos draft-notification" ng-class="{visible: draftUpdated}"></i>
                         <ul>
         </div>
     </div>
 
+    {{--Title input--}}
     <div class="title-input page-title clearfix" ng-non-bindable>
         <div class="input">
             @include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
         </div>
     </div>
 
+    {{--Editors--}}
     <div class="edit-area flex-fill flex">
+
+        {{--WYSIWYG Editor--}}
         @if(setting('app-editor') === 'wysiwyg')
             <div tinymce="editorOptions" mce-change="editorChange" mce-model="editContent" class="flex-fill flex">
                 <textarea id="html-editor"   name="html" rows="5" ng-non-bindable
@@ -66,6 +72,7 @@
             @endif
         @endif
 
+        {{--Markdown Editor--}}
         @if(setting('app-editor') === 'markdown')
             <div id="markdown-editor" markdown-editor class="flex-fill flex">
 
             @if($errors->has('markdown'))
                 <div class="text-neg text-small">{{ $errors->first('markdown') }}</div>
             @endif
-
         @endif
+
     </div>
 </div>
\ No newline at end of file
diff --git a/resources/views/pages/guest-create.blade.php b/resources/views/pages/guest-create.blade.php
new file mode 100644 (file)
index 0000000..00d9f55
--- /dev/null
@@ -0,0 +1,25 @@
+@extends('base')
+
+@section('content')
+
+    <div class="container small" ng-non-bindable>
+        <h1>Create Page</h1>
+        <form action="{{  $parent->getUrl('/page/create/guest') }}" method="POST">
+
+            {!! csrf_field() !!}
+
+            <div class="form-group title-input">
+                <label for="name">Page Name</label>
+                @include('form/text', ['name' => 'name'])
+            </div>
+
+            <div class="form-group">
+                <a href="{{ $parent->getUrl() }}" class="button muted">Cancel</a>
+                <button type="submit" class="button pos">Continue</button>
+            </div>
+
+        </form>
+    </div>
+
+
+@stop
\ No newline at end of file
index 98243f6fa88ba63a2d09c20af659917025348186..7aa5d7933fb6661968fdb38aa12e0f1fea2259fe 100644 (file)
@@ -1,7 +1,7 @@
 <div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
-    <h3>
+    <h4>
         <a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
-    </h3>
+    </h4>
 
     @if(isset($page->searchSnippet))
         <p class="text-muted">{!! $page->searchSnippet !!}</p>
index 2ddd4e0d8e38830fd0ce9a7375bac5b947e7f358..6ffe4b50237209d6f07e5683be265a1e3f4fd0ce 100644 (file)
@@ -24,5 +24,9 @@
 
     <div style="clear:left;"></div>
 
-    {!! $page->html !!}
+    @if (isset($diff) && $diff)
+        {!! $diff !!}
+    @else
+        {!! $page->html !!}
+    @endif
 </div>
\ No newline at end of file
index 926affffc3824d969518ae57383ed19a53726b00..720e34fea4efd76c4187e4f5b37b29e9b65e3a57 100644 (file)
 
             <table class="table">
                 <tr>
-                    <th width="25%">Name</th>
-                    <th colspan="2" width="10%">Created By</th>
+                    <th width="23%">Name</th>
+                    <th colspan="2" width="8%">Created By</th>
                     <th width="15%">Revision Date</th>
                     <th width="25%">Changelog</th>
-                    <th width="15%">Actions</th>
+                    <th width="20%">Actions</th>
                 </tr>
                 @foreach($page->revisions as $index => $revision)
                     <tr>
                         <td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif</td>
                         <td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
                         <td>{{ $revision->summary }}</td>
-                        @if ($index !== 0)
-                            <td>
+                        <td>
+                            <a href="{{ $revision->getUrl('changes') }}" target="_blank">Changes</a>
+                            <span class="text-muted">&nbsp;|&nbsp;</span>
+
+                            @if ($index === 0)
+                                <a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a>
+                            @else
                                 <a href="{{ $revision->getUrl() }}" target="_blank">Preview</a>
                                 <span class="text-muted">&nbsp;|&nbsp;</span>
-                                <a href="{{ $revision->getUrl() }}/restore">Restore</a>
-                            </td>
-                        @else
-                            <td><a target="_blank" href="{{ $page->getUrl() }}"><i>Current Version</i></a></td>
-                        @endif
+                                <a href="{{ $revision->getUrl('restore') }}" target="_blank">Restore</a>
+                            @endif
+                        </td>
                     </tr>
                 @endforeach
             </table>
index 2e6e35476d9773d568f1aff4a7325a09d47a1b6c..af85075a204981b84e2644d5f93ea6eec1a3046b 100644 (file)
                         @endif
                     </div>
                 @endif
-                @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
+
+                @include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav])
             </div>
 
         </div>
index e40fdbf0fcaa28d673aeea356246a05894ea6fc3..8e7db85ac21c7f0523677c2b4dc0b37cea9cfce8 100644 (file)
@@ -1,5 +1,26 @@
 
 <div class="book-tree" ng-non-bindable>
+
+    @if (isset($page) && $page->files->count() > 0)
+        <h6 class="text-muted">Attachments</h6>
+        @foreach($page->files as $file)
+            <div class="attachment">
+                <a href="{{ $file->getUrl() }}" @if($file->external) target="_blank" @endif><i class="zmdi zmdi-{{ $file->external ? 'open-in-new' : 'file' }}"></i> {{ $file->name }}</a>
+            </div>
+        @endforeach
+    @endif
+
+    @if (isset($pageNav) && $pageNav)
+        <h6 class="text-muted">Page Navigation</h6>
+        <div class="sidebar-page-nav menu">
+            @foreach($pageNav as $navItem)
+                <li class="page-nav-item {{ $navItem['nodeName'] }}">
+                    <a href="{{ $navItem['link'] }}">{{ $navItem['text'] }}</a>
+                </li>
+            @endforeach
+        </div>
+    @endif
+
     <h6 class="text-muted">Book Navigation</h6>
     <ul class="sidebar-page-list menu">
         <li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
index bf7dde1d4a34de2344ee1a32d49ba6b5101e166a..885cc2729c9c1a2386fe65e69a89c37a14be4ee8 100644 (file)
@@ -14,7 +14,7 @@
     .nav-tabs a.selected, .nav-tabs .tab-item.selected {
         border-bottom-color: {{ setting('app-color') }};
     }
-    p.primary:hover, p .primary:hover, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
+    .text-primary, p.primary, p .primary, span.primary:hover, .text-primary:hover, a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
         color: {{ setting('app-color') }};
     }
 </style>
\ No newline at end of file
index 2bce4157028a8ca2b1595021dfaa582cb3b67ae0..16aebe2bb74fcede0efea22af753c41ec920b46b 100644 (file)
@@ -36,7 +36,9 @@
                     @if(setting('app-logo', '') !== 'none')
                         <img class="logo-image" src="{{ setting('app-logo', '') === '' ? baseUrl('/logo.png') : baseUrl(setting('app-logo', '')) }}" alt="Logo">
                     @endif
-                    <span class="logo-text">{{ setting('app-name') }}</span>
+                    @if (setting('app-name-header'))
+                        <span class="logo-text">{{ setting('app-name') }}</span>
+                    @endif
                 </a>
             </div>
             <div class="col-md-6">
index 222ff0f690492dddcd8c0c91365ba18a96c68cd1..ac25eb3b55e8a40bc33f885fb86cf5fcef16947c 100644 (file)
@@ -6,7 +6,7 @@
 
 <div class="container small settings-container">
 
-    <h1>Settings</h1>
+    <h1>{{ trans('settings.settings') }}</h1>
 
     <form action="{{ baseUrl("/settings") }}" method="POST" ng-cloak>
         {!! csrf_field() !!}
         <h3>App Settings</h3>
 
         <div class="row">
+
             <div class="col-md-6">
                 <div class="form-group">
-                    <label for="setting-app-name">Application name</label>
+                    <label for="setting-app-name">{{ trans('settings.app_name') }}</label>
+                    <p class="small">{{ trans('settings.app_name_desc') }}</p>
                     <input type="text" value="{{ setting('app-name', 'BookStack') }}" name="setting-app-name" id="setting-app-name">
                 </div>
                 <div class="form-group">
-                    <label>Allow public viewing?</label>
+                    <label>{{ trans('settings.app_name_header') }}</label>
+                    <div toggle-switch name="setting-app-name-header" value="{{ setting('app-name-header') }}"></div>
+                </div>
+                <div class="form-group">
+                    <label for="setting-app-public">{{ trans('settings.app_public_viewing') }}</label>
                     <div toggle-switch name="setting-app-public" value="{{ setting('app-public') }}"></div>
                 </div>
                 <div class="form-group">
-                    <label>Enable higher security image uploads?</label>
-                    <p class="small">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.</p>
+                    <label>{{ trans('settings.app_secure_images') }}</label>
+                    <p class="small">{{ trans('settings.app_secure_images_desc') }}</p>
                     <div toggle-switch name="setting-app-secure-images" value="{{ setting('app-secure-images') }}"></div>
                 </div>
                 <div class="form-group">
-                    <label for="setting-app-editor">Page editor</label>
-                    <p class="small">Select which editor will be used by all users to edit pages.</p>
+                    <label for="setting-app-editor">{{ trans('settings.app_editor') }}</label>
+                    <p class="small">{{ trans('settings.app_editor_desc') }}</p>
                     <select name="setting-app-editor" id="setting-app-editor">
                         <option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
                         <option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
                     </select>
                 </div>
             </div>
+
             <div class="col-md-6">
                 <div class="form-group" id="logo-control">
-                    <label for="setting-app-logo">Application logo</label>
-                    <p class="small">This image should be 43px in height. <br>Large images will be scaled down.</p>
+                    <label for="setting-app-logo">{{ trans('settings.app_logo') }}</label>
+                    <p class="small">{!! trans('settings.app_logo_desc') !!}</p>
                     <image-picker resize-height="43" show-remove="true" resize-width="200" current-image="{{ setting('app-logo', '') }}" default-image="{{ baseUrl('/logo.png') }}" name="setting-app-logo" image-class="logo-image"></image-picker>
                 </div>
                 <div class="form-group" id="color-control">
-                    <label for="setting-app-color">Application primary color</label>
-                    <p class="small">This should be a hex value. <br> Leave empty to reset to the default color.</p>
+                    <label for="setting-app-color">{{ trans('settings.app_primary_color') }}</label>
+                    <p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
                     <input  type="text" value="{{ setting('app-color', '') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
                     <input  type="hidden" value="{{ setting('app-color-light', '') }}" name="setting-app-color-light" id="setting-app-color-light" placeholder="rgba(21, 101, 192, 0.15)">
                 </div>
             </div>
+
         </div>
+
         <div class="form-group">
-            <label for="setting-app-custom-head">Custom HTML head content</label>
-            <p class="small">Any content added here will be inserted into the bottom of the &lt;head&gt; section of every page. This is handy for overriding styles or adding analytics code.</p>
+            <label for="setting-app-custom-head">{{ trans('settings.app_custom_html') }}</label>
+            <p class="small">{{ trans('settings.app_custom_html_desc') }}</p>
             <textarea name="setting-app-custom-head" id="setting-app-custom-head">{{ setting('app-custom-head', '') }}</textarea>
         </div>
 
         <hr class="margin-top">
 
-        <h3>Registration Settings</h3>
+        <h3>{{ trans('settings.reg_settings') }}</h3>
 
         <div class="row">
             <div class="col-md-6">
                 <div class="form-group">
-                    <label for="setting-registration-enabled">Allow registration?</label>
+                    <label for="setting-registration-enabled">{{ trans('settings.reg_allow') }}</label>
                     <div toggle-switch name="setting-registration-enabled" value="{{ setting('registration-enabled') }}"></div>
                 </div>
                 <div class="form-group">
-                    <label for="setting-registration-role">Default user role after registration</label>
+                    <label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
                     <select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
-                        @foreach(\BookStack\Role::visible() as $role)
+                        @foreach(\BookStack\Role::all() as $role)
                             <option value="{{$role->id}}" data-role-name="{{ $role->name }}"
                                     @if(setting('registration-role', \BookStack\Role::first()->id) == $role->id) selected @endif
                                     >
                     </select>
                 </div>
                 <div class="form-group">
-                    <label for="setting-registration-confirmation">Require email confirmation?</label>
-                    <p class="small">If domain restriction is used then email confirmation will be required and the below value will be ignored.</p>
+                    <label for="setting-registration-confirmation">{{ trans('settings.reg_confirm_email') }}</label>
+                    <p class="small">{{ trans('settings.reg_confirm_email_desc') }}</p>
                     <div toggle-switch name="setting-registration-confirmation" value="{{ setting('registration-confirmation') }}"></div>
                 </div>
             </div>
             <div class="col-md-6">
                 <div class="form-group">
-                    <label for="setting-registration-restrict">Restrict registration to domain</label>
-                    <p class="small">Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
-                        <br> Note that users will be able to change their email addresses after successful registration.</p>
-                    <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="No restriction set" value="{{ setting('registration-restrict', '') }}">
+                    <label for="setting-registration-restrict">{{ trans('settings.reg_confirm_restrict_domain') }}</label>
+                    <p class="small">{!! trans('settings.reg_confirm_restrict_domain_desc') !!}</p>
+                    <input type="text" id="setting-registration-restrict" name="setting-registration-restrict" placeholder="{{ trans('settings.reg_confirm_restrict_domain_placeholder') }}" value="{{ setting('registration-restrict', '') }}">
                 </div>
             </div>
         </div>
             <span class="float right muted">
                 BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
             </span>
-            <button type="submit" class="button pos">Save Settings</button>
+            <button type="submit" class="button pos">{{ trans('settings.settings_save') }}</button>
         </div>
     </form>
 
index 5e653f8de0525975e1f375243fed25c9bf9d0fe9..4f1987c0387def5b9488404405693be3eb91bd71 100644 (file)
                             <label>@include('settings/roles/checkbox', ['permission' => 'image-delete-all']) All</label>
                         </td>
                     </tr>
+                    <tr>
+                        <td>Attached <br>Files</td>
+                        <td>@include('settings/roles/checkbox', ['permission' => 'file-create-all'])</td>
+                        <td style="line-height:1.2;"><small class="faded">Controlled by the asset they are uploaded to</small></td>
+                        <td>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-update-own']) Own</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-update-all']) All</label>
+                        </td>
+                        <td>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-own']) Own</label>
+                            <label>@include('settings/roles/checkbox', ['permission' => 'file-delete-all']) All</label>
+                        </td>
+                    </tr>
                 </table>
             </div>
         </div>
index d06ec09bc3a3fcde92179766b0eed4b647185120..6cbbdb7f7ec879a776e3ec0087f13618c8a836c0 100644 (file)
@@ -15,7 +15,9 @@
                 </div>
                 <div class="col-sm-4">
                     <p></p>
-                    <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
+                    @if($authMethod !== 'system')
+                        <a href="{{ baseUrl("/settings/users/{$user->id}/delete") }}" class="neg button float right">Delete User</a>
+                    @endif
                 </div>
             </div>
             <div class="row">
diff --git a/resources/views/users/forms/system.blade.php b/resources/views/users/forms/system.blade.php
new file mode 100644 (file)
index 0000000..3ee5f64
--- /dev/null
@@ -0,0 +1,25 @@
+@if($user->system_name == 'public')
+    <p>This user represents any guest users that visit your instance. It cannot be used for logins but is assigned&nbsp;automatically.</p>
+@endif
+
+<div class="form-group">
+    <label for="name">Name</label>
+    @include('form.text', ['name' => 'name'])
+</div>
+
+<div class="form-group">
+    <label for="email">Email</label>
+    @include('form.text', ['name' => 'email'])
+</div>
+
+@if(userCan('users-manage'))
+    <div class="form-group">
+        <label for="role">User Role</label>
+        @include('form/role-checkboxes', ['name' => 'roles', 'roles' => $roles])
+    </div>
+@endif
+
+<div class="form-group">
+    <a href="{{ baseUrl("/settings/users") }}" class="button muted">Cancel</a>
+    <button class="button pos" type="submit">Save</button>
+</div>
index e72a9e61a5df113d041190cde8707ccbd6d56749..105fddb5ba55d232cd69fd684006dd6070dab0ae 100644 (file)
@@ -22,7 +22,7 @@
         <div class="row">
             <div class="col-sm-8">
                 <div class="compact">
-                    {!! $users->links() !!}
+                    {{ $users->links() }}
                 </div>
             </div>
             <div class="col-sm-4">
@@ -76,7 +76,7 @@
         </table>
 
         <div>
-            {!! $users->links() !!}
+            {{ $users->links() }}
         </div>
     </div>
 
diff --git a/resources/views/vendor/.gitkeep b/resources/views/vendor/.gitkeep
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/resources/views/vendor/notifications/email-plain.blade.php b/resources/views/vendor/notifications/email-plain.blade.php
new file mode 100644 (file)
index 0000000..acefa65
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+if (! empty($greeting)) {
+    echo $greeting, "\n\n";
+} else {
+    echo $level == 'error' ? 'Whoops!' : 'Hello!', "\n\n";
+}
+
+if (! empty($introLines)) {
+    echo implode("\n", $introLines), "\n\n";
+}
+
+if (isset($actionText)) {
+    echo "{$actionText}: {$actionUrl}", "\n\n";
+}
+
+if (! empty($outroLines)) {
+    echo implode("\n", $outroLines), "\n\n";
+}
+
+echo 'Regards,', "\n";
+echo config('app.name'), "\n";
diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php
new file mode 100644 (file)
index 0000000..297c6be
--- /dev/null
@@ -0,0 +1,206 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+
+    <style type="text/css" rel="stylesheet" media="all">
+        /* Media Queries */
+        @media only screen and (max-width: 500px) {
+            .button {
+                width: 100% !important;
+            }
+        }
+
+        @media only screen and (max-width: 600px) {
+            .button {
+                width: 100% !important;
+            }
+            .mobile {
+                max-width: 100%;
+                display: block;
+                width: 100%;
+            }
+        }
+    </style>
+</head>
+
+<?php
+
+$style = [
+    /* Layout ------------------------------ */
+
+    'body' => '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').';',
+];
+?>
+
+<?php $fontFamily = 'font-family: Arial, \'Helvetica Neue\', Helvetica, sans-serif;'; ?>
+
+<body style="{{ $style['body'] }}">
+    <table width="100%" cellpadding="0" cellspacing="0">
+        <tr>
+            <td align="center" class="mobile">
+                <table width="600" style="max-width: 100%; padding: 12px;text-align: left;" cellpadding="0" cellspacing="0" class="mobile">
+                    <tr>
+                        <td style="{{ $style['email-wrapper'] }}" align="center">
+                            <table width="100%" cellpadding="0" cellspacing="0">
+                                <!-- Logo -->
+                                <tr>
+                                    <td style="{{ $style['email-masthead'] }}">
+                                        <a style="{{ $fontFamily }} {{ $style['email-masthead_name'] }}" href="{{ baseUrl('/') }}" target="_blank">
+                                            {{ setting('app-name') }}
+                                        </a>
+                                    </td>
+                                </tr>
+
+                                <!-- Email Body -->
+                                <tr>
+                                    <td style="{{ $style['email-body'] }}" width="100%">
+                                        <table style="{{ $style['email-body_inner'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
+                                            <tr>
+                                                <td style="{{ $fontFamily }} {{ $style['email-body_cell'] }}">
+
+                                                    <!-- Greeting -->
+                                                    @if (!empty($greeting) || $level == 'error')
+                                                    <h1 style="{{ $style['header-1'] }}">
+                                                        @if (! empty($greeting))
+                                                            {{ $greeting }}
+                                                        @else
+                                                            @if ($level == 'error')
+                                                                Whoops!
+                                                            @endif
+                                                        @endif
+                                                    </h1>
+                                                    @endif
+
+                                                    <!-- Intro -->
+                                                    @foreach ($introLines as $line)
+                                                        <p style="{{ $style['paragraph'] }}">
+                                                            {{ $line }}
+                                                        </p>
+                                                    @endforeach
+
+                                                    <!-- Action Button -->
+                                                    @if (isset($actionText))
+                                                        <table style="{{ $style['body_action'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
+                                                            <tr>
+                                                                <td align="center">
+                                                                    <?php
+                                                                    switch ($level) {
+                                                                        case 'success':
+                                                                            $actionColor = 'button--green';
+                                                                            break;
+                                                                        case 'error':
+                                                                            $actionColor = 'button--red';
+                                                                            break;
+                                                                        default:
+                                                                            $actionColor = 'button--blue';
+                                                                    }
+                                                                    ?>
+
+                                                                    <a href="{{ $actionUrl }}"
+                                                                       style="{{ $fontFamily }} {{ $style['button'] }} {{ $style[$actionColor] }}"
+                                                                       class="button"
+                                                                       target="_blank">
+                                                                        {{ $actionText }}
+                                                                    </a>
+                                                                </td>
+                                                            </tr>
+                                                        </table>
+                                                    @endif
+
+                                                    <!-- Outro -->
+                                                    @foreach ($outroLines as $line)
+                                                        <p style="{{ $style['paragraph'] }}">
+                                                            {{ $line }}
+                                                        </p>
+                                                    @endforeach
+
+
+                                                    <!-- Sub Copy -->
+                                                    @if (isset($actionText))
+                                                        <table style="{{ $style['body_sub'] }}">
+                                                            <tr>
+                                                                <td style="{{ $fontFamily }}">
+                                                                    <p style="{{ $style['paragraph-sub'] }}">
+                                                                        If you’re having trouble clicking the "{{ $actionText }}" button,
+                                                                        copy and paste the URL below into your web browser:
+                                                                    </p>
+
+                                                                    <p style="{{ $style['paragraph-sub'] }}">
+                                                                        <a style="{{ $style['anchor'] }}" href="{{ $actionUrl }}" target="_blank">
+                                                                            {{ $actionUrl }}
+                                                                        </a>
+                                                                    </p>
+                                                                </td>
+                                                            </tr>
+                                                        </table>
+                                                    @endif
+
+                                                </td>
+                                            </tr>
+                                        </table>
+                                    </td>
+                                </tr>
+
+                                <!-- Footer -->
+                                <tr>
+                                    <td>
+                                        <table style="{{ $style['email-footer'] }}" align="center" width="100%" cellpadding="0" cellspacing="0">
+                                            <tr>
+                                                <td style="{{ $fontFamily }} {{ $style['email-footer_cell'] }}">
+                                                    <p style="{{ $style['paragraph-sub'] }}">
+                                                        &copy; {{ date('Y') }}
+                                                        <a style="{{ $style['anchor'] }}" href="{{ baseUrl('/') }}" target="_blank">{{ setting('app-name') }}</a>.
+                                                        All rights reserved.
+                                                    </p>
+                                                </td>
+                                            </tr>
+                                        </table>
+                                    </td>
+                                </tr>
+                            </table>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+</body>
+</html>
similarity index 77%
rename from app/Http/routes.php
rename to routes/web.php
index eb35f2a119846bd87836b3ff4b7ff156f242c89a..45957ac626e2e1cb34bdb71ddefbe2278b24bcb5 100644 (file)
@@ -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');
     });
 
+    // File routes
+    Route::get('/files/{id}', 'FileController@get');
+    Route::post('/files/upload', 'FileController@upload');
+    Route::post('/files/upload/{id}', 'FileController@uploadUpdate');
+    Route::post('/files/link', 'FileController@attachLink');
+    Route::put('/files/{id}', 'FileController@update');
+    Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
+    Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
+    Route::delete('/files/{id}', 'FileController@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
old mode 100644 (file)
new mode 100755 (executable)
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 (file)
index 0000000..f22faa7
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+
+class AttachmentTest extends TestCase
+{
+    /**
+     * Get a test file that can be uploaded
+     * @param $fileName
+     * @return \Illuminate\Http\UploadedFile
+     */
+    protected function getTestFile($fileName)
+    {
+        return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
+    }
+
+    /**
+     * Uploads a file with the given name.
+     * @param $name
+     * @param int $uploadedTo
+     * @return string
+     */
+    protected function uploadFile($name, $uploadedTo = 0)
+    {
+        $file = $this->getTestFile($name);
+        return $this->call('POST', '/files/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\FileService::class);
+        foreach (\BookStack\File::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('files', $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', 'files/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('files', $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', 'files/link', [
+            'link' => 'https://example.com',
+            'name' => 'Example Attachment Link',
+            'uploaded_to' => $page->id,
+        ]);
+
+        $attachmentId = \BookStack\File::first()->id;
+
+        $this->call('PUT', 'files/' . $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('files', $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\File::first()->id;
+        $this->call('DELETE', 'files/' . $attachmentId);
+
+        $this->dontSeeInDatabase('files', [
+            '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('files', [
+            'name' => $fileName
+        ]);
+
+        $this->call('DELETE', $page->getUrl());
+
+        $this->dontSeeInDatabase('files', [
+            'name' => $fileName
+        ]);
+        $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
+
+        $this->deleteUploads();
+    }
+}
index 99885d552c7a0dcdf9b4b24e2d680502d614cad3..0d2e4ac170a660e3ed0925f65df404e915a1c398 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
-use BookStack\EmailConfirmation;
+use BookStack\Notifications\ConfirmEmail;
+use Illuminate\Support\Facades\Notification;
 
 class AuthTest extends TestCase
 {
@@ -57,15 +58,13 @@ class AuthTest extends TestCase
 
     public function test_confirmed_registration()
     {
+        // Fake notifications
+        Notification::fake();
+
         // Set settings and get user instance
         $this->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)
index 76fbc662ab89fab61bc50b6e8c79aa8e8d421b68..9573321fba98e5c3bb6fbf06630ddafbfb5a0221 100644 (file)
@@ -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');
     }
index 296aa72edc1491ae703e3fab90a92b15db85cf7e..20721968f9a88c933d41cfa9efbb60c5cc652437 100644 (file)
@@ -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);
     }
 
index 108b7459f79c860e7d12c8d4649b87bea6042ab6..1a46e30bc6b03c2e26361b9563b33b6a16a1ba1f 100644 (file)
@@ -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')
index 23373419f21792413a97706aee439787c13dc6aa..031517cdb019d1d9380cf4161fc4dd44346d4399 100644 (file)
@@ -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'
         ]);
 
index b64f40dc6af325bc5ebfdbeacf8e24b60cc8c9e6..7a0515fd939795ea5732326cfc016a770a37d009 100644 (file)
@@ -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 (file)
index 0000000..6851464
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+class PublicActionTest extends TestCase
+{
+
+    public function test_app_not_public()
+    {
+        $this->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 (file)
index 58e39df..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-
-class PublicViewTest extends TestCase
-{
-
-    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());
-    }
-
-}
\ No newline at end of file
index 6a8c2d732b65dc123de08569c6f4c2da7c2ac3bc..d3620eae0b9d1d985d8baaa988ca69c49c52a5f8 100644 (file)
@@ -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
index 40ae004e981681b66e6fbea20b891c8a8e5fb248..9543adc1d3bc3f6752bffa1eadd27eb26387f064 100644 (file)
@@ -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 (file)
index 0000000..4c1f41a
--- /dev/null
@@ -0,0 +1 @@
+Hi, This is a test file for testing the upload process.
\ No newline at end of file
diff --git a/version b/version
new file mode 100644 (file)
index 0000000..8287f28
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+v0.13-dev