From: Dan Brown
Date: Sat, 16 Nov 2019 12:42:45 +0000 (+0000)
Subject: Merge branch 'feature/saml' of git://github.com/Xiphoseer/BookStack into Xiphoseer...
X-Git-Tag: v0.28.0~1^2~47^2~8
X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/bb1f43cbd882b51550007020f72667fd0fb10171?hp=8e723f10dc3db49df9dc66ea5a90e3153eda54e8
Merge branch 'feature/saml' of git://github.com/Xiphoseer/BookStack into Xiphoseer-feature/saml
---
diff --git a/.env.example.complete b/.env.example.complete
index 829a7509b..c4c3f0b85 100644
--- a/.env.example.complete
+++ b/.env.example.complete
@@ -89,7 +89,7 @@ REDIS_SERVERS=127.0.0.1:6379:0
# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
-QUEUE_DRIVER=sync
+QUEUE_CONNECTION=sync
# Storage system to use
# Can be 'local', 'local_secure' or 's3'
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 000000000..922aa5067
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,26 @@
+name: phpunit
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: [7.2, 7.3]
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup Database
+ run: |
+ mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
+ mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
+ mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
+ mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
+ - name: Install composer dependencies & Test
+ run: composer install --prefer-dist --no-interaction --ansi
+ - name: Migrate and seed the database
+ run: |
+ php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
+ php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
+ - name: phpunit
+ run: php${{ matrix.php }} ./vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
index 1b53cbe7a..e5579e4a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,5 @@ nbproject
.buildpath
.project
.settings/
-webpack-stats.json
\ No newline at end of file
+webpack-stats.json
+.phpunit.result.cache
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 29727f488..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-dist: trusty
-sudo: false
-language: php
-php:
- - 7.0.20
- - 7.1.9
-
-cache:
- directories:
- - $HOME/.composer/cache
-
-before_script:
- - mysql -u root -e 'create database `bookstack-test`;'
- - mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- - mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- - mysql -u root -e "FLUSH PRIVILEGES;"
- - phpenv config-rm xdebug.ini
- - composer install --prefer-dist --no-interaction
- - php artisan clear-compiled -n
- - php artisan optimize -n
- - php artisan migrate --force -n --database=mysql_testing
- - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
-
-after_failure:
- - cat storage/logs/laravel.log
-
-script:
- - phpunit
diff --git a/app/Actions/Activity.php b/app/Actions/Activity.php
index 1ae1811e1..05f0129dd 100644
--- a/app/Actions/Activity.php
+++ b/app/Actions/Activity.php
@@ -3,13 +3,18 @@
namespace BookStack\Actions;
use BookStack\Auth\User;
+use BookStack\Entities\Entity;
use BookStack\Model;
/**
- * @property string key
- * @property \User user
- * @property \Entity entity
- * @property string extra
+ * @property string $key
+ * @property User $user
+ * @property Entity $entity
+ * @property string $extra
+ * @property string $entity_type
+ * @property int $entity_id
+ * @property int $user_id
+ * @property int $book_id
*/
class Activity extends Model
{
diff --git a/app/Actions/ActivityService.php b/app/Actions/ActivityService.php
index f4f82a6f4..f56f1ca57 100644
--- a/app/Actions/ActivityService.php
+++ b/app/Actions/ActivityService.php
@@ -1,8 +1,8 @@
activity->newInstance();
- $activity->user_id = $this->user->id;
- $activity->book_id = $bookId;
- $activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $activity->extra = $extra;
- }
+ $activity = $this->newActivityForUser($activityKey, $bookId);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
}
/**
- * Adds a activity history with a message & without binding to a entity.
- * @param $activityKey
+ * Adds a activity history with a message, without binding to a entity.
+ * @param string $activityKey
+ * @param string $message
* @param int $bookId
- * @param bool|false $extra
*/
- public function addMessage($activityKey, $bookId = 0, $extra = false)
+ public function addMessage(string $activityKey, string $message, int $bookId = null)
{
- $this->activity->user_id = $this->user->id;
- $this->activity->book_id = $bookId;
- $this->activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $this->activity->extra = $extra;
- }
- $this->activity->save();
+ $this->newActivityForUser($activityKey, $bookId)->forceFill([
+ 'extra' => $message
+ ])->save();
+
$this->setNotification($activityKey);
}
+ /**
+ * Get a new activity instance for the current user.
+ * @param string $key
+ * @param int|null $bookId
+ * @return Activity
+ */
+ protected function newActivityForUser(string $key, int $bookId = null)
+ {
+ return $this->activity->newInstance()->forceFill([
+ 'key' => strtolower($key),
+ 'user_id' => $this->user->id,
+ 'book_id' => $bookId ?? 0,
+ ]);
+ }
/**
* Removes the entity attachment from each of its activities
* and instead uses the 'extra' field with the entities name.
* Used when an entity is deleted.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @return mixed
*/
public function removeEntity(Entity $entity)
{
+ // TODO - Rewrite to db query.
$activities = $entity->activity;
foreach ($activities as $activity) {
$activity->extra = $entity->name;
@@ -90,7 +95,11 @@ class ActivityService
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
- ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
+ ->orderBy('created_at', 'desc')
+ ->with('user', 'entity')
+ ->skip($count * $page)
+ ->take($count)
+ ->get();
return $this->filterSimilar($activityList);
}
@@ -98,7 +107,7 @@ class ActivityService
/**
* Gets the latest activity for an entity, Filtering out similar
* items to prevent a message activity list.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @param int $count
* @param int $page
* @return array
@@ -171,7 +180,7 @@ class ActivityService
$notificationTextKey = 'activities.' . $activityKey . '_notification';
if (trans()->has($notificationTextKey)) {
$message = trans($notificationTextKey);
- Session::flash('success', $message);
+ session()->flash('success', $message);
}
}
}
diff --git a/app/Actions/ViewService.php b/app/Actions/ViewService.php
index 532f31c42..324bfaa4e 100644
--- a/app/Actions/ViewService.php
+++ b/app/Actions/ViewService.php
@@ -1,8 +1,10 @@
views()->save($this->view->create([
+ $entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
@@ -59,12 +61,12 @@ class ViewService
* @param string $action - used for permission checking
* @return Collection
*/
- public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
+ public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
- ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
+ ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
diff --git a/app/Application.php b/app/Application.php
index 8c56e9dac..499fdeaa6 100644
--- a/app/Application.php
+++ b/app/Application.php
@@ -13,7 +13,11 @@ class Application extends \Illuminate\Foundation\Application
*/
public function configPath($path = '')
{
- return $this->basePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
+ return $this->basePath
+ . DIRECTORY_SEPARATOR
+ . 'app'
+ . DIRECTORY_SEPARATOR
+ . 'Config'
+ . ($path ? DIRECTORY_SEPARATOR.$path : $path);
}
-
-}
\ No newline at end of file
+}
diff --git a/app/Auth/Access/EmailConfirmationService.php b/app/Auth/Access/EmailConfirmationService.php
index 4df014116..9aa3b9b98 100644
--- a/app/Auth/Access/EmailConfirmationService.php
+++ b/app/Auth/Access/EmailConfirmationService.php
@@ -1,33 +1,18 @@
db = $db;
- $this->users = $users;
- }
+ protected $tokenTable = 'email_confirmations';
+ protected $expiryTime = 24;
/**
* Create new confirmation for a user,
* Also removes any existing old ones.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @throws ConfirmationEmailException
*/
public function sendConfirmation(User $user)
@@ -36,76 +21,19 @@ class EmailConfirmationService
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
}
- $this->deleteConfirmationsByUser($user);
- $token = $this->createEmailConfirmation($user);
+ $this->deleteByUser($user);
+ $token = $this->createTokenForUser($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->db->table('email_confirmations')->insert([
- 'user_id' => $user->id,
- 'token' => $token,
- 'created_at' => Carbon::now(),
- 'updated_at' => Carbon::now()
- ]);
- return $token;
- }
-
- /**
- * Gets an email confirmation by looking up the token,
- * Ensures the token has not expired.
- * @param string $token
- * @return array|null|\stdClass
- * @throws UserRegistrationException
- */
- public function getEmailConfirmationFromToken($token)
- {
- $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
-
- // If not found show error
- if ($emailConfirmation === null) {
- throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
- }
-
- // If more than a day old
- if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
- $user = $this->users->getById($emailConfirmation->user_id);
- $this->sendConfirmation($user);
- throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
- }
-
- $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
- return $emailConfirmation;
- }
-
- /**
- * Delete all email confirmations that belong to a user.
- * @param \BookStack\Auth\User $user
- * @return mixed
+ * Check if confirmation is required in this instance.
+ * @return bool
*/
- public function deleteConfirmationsByUser(User $user)
+ public function confirmationRequired() : bool
{
- return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
- }
-
- /**
- * Creates a unique token within the email confirmation database.
- * @return string
- */
- protected function getToken()
- {
- $token = str_random(24);
- while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
- $token = str_random(25);
- }
- return $token;
+ return setting('registration-confirmation')
+ || setting('registration-restrict');
}
}
diff --git a/app/Auth/Access/SocialAuthService.php b/app/Auth/Access/SocialAuthService.php
index 0d46b9f88..9c8d1a81f 100644
--- a/app/Auth/Access/SocialAuthService.php
+++ b/app/Auth/Access/SocialAuthService.php
@@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
+use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\User as SocialUser;
@@ -104,6 +105,7 @@ class SocialAuthService
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$isLoggedIn = auth()->check();
$currentUser = user();
+ $titleCaseDriver = Str::title($socialDriver);
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
@@ -117,26 +119,26 @@ class SocialAuthService
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
- session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
- session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in, A social account exists but the users do not match.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
- session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// Otherwise let the user know this social account is not used by anyone.
- $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
+ $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
if (setting('registration-enabled')) {
- $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
+ $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
}
throw new SocialSignInAccountNotUsed($message, '/login');
@@ -157,7 +159,7 @@ class SocialAuthService
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
- throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
+ throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
}
return $driver;
@@ -244,7 +246,7 @@ class SocialAuthService
public function detachSocialAccount($socialDriver)
{
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
- session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
return redirect(user()->getEditUrl());
}
diff --git a/app/Auth/Access/UserInviteService.php b/app/Auth/Access/UserInviteService.php
new file mode 100644
index 000000000..20519fc7d
--- /dev/null
+++ b/app/Auth/Access/UserInviteService.php
@@ -0,0 +1,22 @@
+deleteByUser($user);
+ $token = $this->createTokenForUser($user);
+ $user->notify(new UserInvite($token));
+ }
+}
diff --git a/app/Auth/Access/UserTokenService.php b/app/Auth/Access/UserTokenService.php
new file mode 100644
index 000000000..a1defbf62
--- /dev/null
+++ b/app/Auth/Access/UserTokenService.php
@@ -0,0 +1,134 @@
+db = $db;
+ }
+
+ /**
+ * Delete all email confirmations that belong to a user.
+ * @param User $user
+ * @return mixed
+ */
+ public function deleteByUser(User $user)
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('user_id', '=', $user->id)
+ ->delete();
+ }
+
+ /**
+ * Get the user id from a token, while check the token exists and has not expired.
+ * @param string $token
+ * @return int
+ * @throws UserTokenNotFoundException
+ * @throws UserTokenExpiredException
+ */
+ public function checkTokenAndGetUserId(string $token) : int
+ {
+ $entry = $this->getEntryByToken($token);
+
+ if (is_null($entry)) {
+ throw new UserTokenNotFoundException('Token "' . $token . '" not found');
+ }
+
+ if ($this->entryExpired($entry)) {
+ throw new UserTokenExpiredException("Token of id {$entry->id} has expired.", $entry->user_id);
+ }
+
+ return $entry->user_id;
+ }
+
+ /**
+ * Creates a unique token within the email confirmation database.
+ * @return string
+ */
+ protected function generateToken() : string
+ {
+ $token = Str::random(24);
+ while ($this->tokenExists($token)) {
+ $token = Str::random(25);
+ }
+ return $token;
+ }
+
+ /**
+ * Generate and store a token for the given user.
+ * @param User $user
+ * @return string
+ */
+ protected function createTokenForUser(User $user) : string
+ {
+ $token = $this->generateToken();
+ $this->db->table($this->tokenTable)->insert([
+ 'user_id' => $user->id,
+ 'token' => $token,
+ 'created_at' => Carbon::now(),
+ 'updated_at' => Carbon::now()
+ ]);
+ return $token;
+ }
+
+ /**
+ * Check if the given token exists.
+ * @param string $token
+ * @return bool
+ */
+ protected function tokenExists(string $token) : bool
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('token', '=', $token)->exists();
+ }
+
+ /**
+ * Get a token entry for the given token.
+ * @param string $token
+ * @return object|null
+ */
+ protected function getEntryByToken(string $token)
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('token', '=', $token)
+ ->first();
+ }
+
+ /**
+ * Check if the given token entry has expired.
+ * @param stdClass $tokenEntry
+ * @return bool
+ */
+ protected function entryExpired(stdClass $tokenEntry) : bool
+ {
+ return Carbon::now()->subHours($this->expiryTime)
+ ->gt(new Carbon($tokenEntry->created_at));
+ }
+}
diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php
index a5ab4ea9a..97cc1ca24 100644
--- a/app/Auth/Permissions/PermissionService.php
+++ b/app/Auth/Permissions/PermissionService.php
@@ -215,7 +215,6 @@ class PermissionService
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
- * @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
@@ -634,42 +633,40 @@ class PermissionService
}
/**
- * Get the children of a book in an efficient single query, Filtered by the permission system.
- * @param integer $book_id
- * @param bool $filterDrafts
- * @param bool $fetchPageContent
- * @return QueryBuilder
+ * Limited the given entity query so that the query will only
+ * return items that the user has permission for the given ability.
*/
- public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
+ public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
{
- $entities = $this->entityProvider;
- $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
- ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
- $query->where('draft', '=', 0);
- if (!$filterDrafts) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
+ $this->clean();
+ return $query->where(function (Builder $parentQuery) use ($ability) {
+ $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
+ $permissionQuery->whereIn('role_id', $this->getRoles())
+ ->where('action', '=', $ability)
+ ->where(function (Builder $query) {
+ $query->where('has_permission', '=', true)
+ ->orWhere(function (Builder $query) {
+ $query->where('has_permission_own', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
+ });
});
- }
- });
- $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
- $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
- ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
-
- // Add joint permission filter
- $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
- ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
- ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
- ->where(function ($query) {
- $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
- $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
- });
});
- $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
+ });
+ }
- $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
- $this->clean();
- return $query;
+ /**
+ * Extend the given page query to ensure draft items are not visible
+ * unless created by the given user.
+ */
+ public function enforceDraftVisiblityOnQuery(Builder $query): Builder
+ {
+ return $query->where(function (Builder $query) {
+ $query->where('draft', '=', false)
+ ->orWhere(function (Builder $query) {
+ $query->where('draft', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
+ });
+ });
}
/**
@@ -684,12 +681,11 @@ class PermissionService
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
- $query->where('draft', '=', false);
- if ($this->currentUser()) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+ $query->where('draft', '=', false)
+ ->orWhere(function ($query) {
+ $query->where('draft', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
});
- }
});
}
diff --git a/app/Auth/Permissions/PermissionsRepo.php b/app/Auth/Permissions/PermissionsRepo.php
index 18d5089be..56ef19301 100644
--- a/app/Auth/Permissions/PermissionsRepo.php
+++ b/app/Auth/Permissions/PermissionsRepo.php
@@ -3,6 +3,7 @@
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
+use Illuminate\Support\Str;
class PermissionsRepo
{
@@ -66,7 +67,7 @@ class PermissionsRepo
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
- $role->name .= strtolower(str_random(2));
+ $role->name .= strtolower(Str::random(2));
}
$role->save();
@@ -136,7 +137,7 @@ class PermissionsRepo
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
- } else if ($role->id == setting('registration-role')) {
+ } else if ($role->id === intval(setting('registration-role'))) {
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
diff --git a/app/Auth/Role.php b/app/Auth/Role.php
index 917d8aa26..712f5299b 100644
--- a/app/Auth/Role.php
+++ b/app/Auth/Role.php
@@ -75,7 +75,7 @@ class Role extends Model
*/
public static function getRole($roleName)
{
- return static::where('name', '=', $roleName)->first();
+ return static::query()->where('name', '=', $roleName)->first();
}
/**
@@ -85,7 +85,7 @@ class Role extends Model
*/
public static function getSystemRole($roleName)
{
- return static::where('system_name', '=', $roleName)->first();
+ return static::query()->where('system_name', '=', $roleName)->first();
}
/**
@@ -94,6 +94,15 @@ class Role extends Model
*/
public static function visible()
{
- return static::where('hidden', '=', false)->orderBy('name')->get();
+ return static::query()->where('hidden', '=', false)->orderBy('name')->get();
+ }
+
+ /**
+ * Get the roles that can be restricted.
+ * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
+ */
+ public static function restrictable()
+ {
+ return static::query()->where('system_name', '!=', 'admin')->get();
}
}
diff --git a/app/Auth/User.php b/app/Auth/User.php
index e5a8a3931..bce418a74 100644
--- a/app/Auth/User.php
+++ b/app/Auth/User.php
@@ -3,6 +3,7 @@
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
+use Carbon\Carbon;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@@ -10,6 +11,20 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
+/**
+ * Class User
+ * @package BookStack\Auth
+ * @property string $id
+ * @property string $name
+ * @property string $email
+ * @property string $password
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property bool $email_confirmed
+ * @property int $image_id
+ * @property string $external_auth_id
+ * @property string $system_name
+ */
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword, Notifiable;
@@ -38,13 +53,24 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $permissions;
+ /**
+ * This holds the default user when loaded.
+ * @var null|User
+ */
+ protected static $defaultUser = null;
+
/**
* Returns the default public user.
* @return User
*/
public static function getDefault()
{
- return static::where('system_name', '=', 'public')->first();
+ if (!is_null(static::$defaultUser)) {
+ return static::$defaultUser;
+ }
+
+ static::$defaultUser = static::where('system_name', '=', 'public')->first();
+ return static::$defaultUser;
}
/**
diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php
index dec973f6c..a903e2c38 100644
--- a/app/Auth/UserRepo.php
+++ b/app/Auth/UserRepo.php
@@ -1,32 +1,31 @@
user = $user;
$this->role = $role;
- $this->entityRepo = $entityRepo;
}
/**
@@ -81,7 +80,7 @@ class UserRepo
* Creates a new user and attaches a role to them.
* @param array $data
* @param boolean $verifyEmail
- * @return \BookStack\Auth\User
+ * @return User
*/
public function registerNew(array $data, $verifyEmail = false)
{
@@ -121,7 +120,7 @@ class UserRepo
/**
* Checks if the give user is the only admin.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @return bool
*/
public function isOnlyAdmin(User $user)
@@ -175,7 +174,7 @@ class UserRepo
* Create a new basic instance of user.
* @param array $data
* @param boolean $verifyEmail
- * @return \BookStack\Auth\User
+ * @return User
*/
public function create(array $data, $verifyEmail = false)
{
@@ -189,7 +188,7 @@ class UserRepo
/**
* Remove the given user from storage, Delete all related content.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @throws Exception
*/
public function destroy(User $user)
@@ -206,7 +205,7 @@ class UserRepo
/**
* Get the latest activity for a user.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @param int $count
* @param int $page
* @return array
@@ -218,36 +217,35 @@ class UserRepo
/**
* Get the recently created content for this given user.
- * @param \BookStack\Auth\User $user
- * @param int $count
- * @return mixed
*/
- public function getRecentlyCreated(User $user, $count = 20)
+ public function getRecentlyCreated(User $user, int $count = 20): array
{
- $createdByUserQuery = function (Builder $query) use ($user) {
- $query->where('created_by', '=', $user->id);
+ $query = function (Builder $query) use ($user, $count) {
+ return $query->orderBy('created_at', 'desc')
+ ->where('created_by', '=', $user->id)
+ ->take($count)
+ ->get();
};
return [
- 'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
- 'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
- 'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
- 'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
+ 'pages' => $query(Page::visible()->where('draft', '=', false)),
+ 'chapters' => $query(Chapter::visible()),
+ 'books' => $query(Book::visible()),
+ 'shelves' => $query(Bookshelf::visible()),
];
}
/**
* Get asset created counts for the give user.
- * @param \BookStack\Auth\User $user
- * @return array
*/
- public function getAssetCounts(User $user)
+ public function getAssetCounts(User $user): array
{
+ $createdBy = ['created_by' => $user->id];
return [
- 'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
- 'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
- 'books' => $this->entityRepo->getUserTotalCreated('book', $user),
- 'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
+ 'pages' => Page::visible()->where($createdBy)->count(),
+ 'chapters' => Chapter::visible()->where($createdBy)->count(),
+ 'books' => Book::visible()->where($createdBy)->count(),
+ 'shelves' => Bookshelf::visible()->where($createdBy)->count(),
];
}
@@ -260,16 +258,6 @@ class UserRepo
return $this->role->newQuery()->orderBy('name', 'asc')->get();
}
- /**
- * Get all the roles which can be given restricted access to
- * other entities in the system.
- * @return mixed
- */
- public function getRestrictableRoles()
- {
- return $this->role->where('system_name', '!=', 'admin')->get();
- }
-
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
@@ -288,7 +276,7 @@ class UserRepo
$user->save();
return true;
} catch (Exception $e) {
- \Log::error('Failed to save user avatar image');
+ Log::error('Failed to save user avatar image');
return false;
}
}
diff --git a/app/Config/app.php b/app/Config/app.php
index 23025a6c4..9dae697da 100755
--- a/app/Config/app.php
+++ b/app/Config/app.php
@@ -52,11 +52,14 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
- 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
+ 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'ko', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW', 'tr'],
// Application Fallback Locale
'fallback_locale' => 'en',
+ // Faker Locale
+ 'faker_locale' => 'en_GB',
+
// Enable right-to-left text control.
'rtl' => false,
@@ -72,10 +75,6 @@ return [
// Encryption cipher
'cipher' => 'AES-256-CBC',
- // Logging configuration
- // Options: single, daily, syslog, errorlog
- 'log' => env('APP_LOGGING', 'single'),
-
// Application Services Provides
'providers' => [
@@ -108,7 +107,6 @@ return [
Barryvdh\Snappy\ServiceProvider::class,
Aacotroneo\Saml2\Saml2ServiceProvider::class,
-
// BookStack replacement service providers (Extends Laravel)
BookStack\Providers\PaginationServiceProvider::class,
BookStack\Providers\TranslationServiceProvider::class,
@@ -138,6 +136,7 @@ return [
// Laravel
'App' => Illuminate\Support\Facades\App::class,
+ 'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
@@ -167,6 +166,7 @@ return [
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
+ 'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
@@ -182,6 +182,7 @@ return [
'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
+ 'Permissions' => BookStack\Facades\Permissions::class,
],
diff --git a/app/Config/auth.php b/app/Config/auth.php
index 7bf1ae772..5535a6f9c 100644
--- a/app/Config/auth.php
+++ b/app/Config/auth.php
@@ -36,6 +36,7 @@ return [
'api' => [
'driver' => 'token',
'provider' => 'users',
+ 'hash' => false,
],
],
@@ -69,4 +70,4 @@ return [
],
],
-];
\ No newline at end of file
+];
diff --git a/app/Config/broadcasting.php b/app/Config/broadcasting.php
index 3d9eb78f9..7aaaa5693 100644
--- a/app/Config/broadcasting.php
+++ b/app/Config/broadcasting.php
@@ -24,9 +24,13 @@ return [
'pusher' => [
'driver' => 'pusher',
- 'key' => env('PUSHER_KEY'),
- 'secret' => env('PUSHER_SECRET'),
+ 'key' => env('PUSHER_APP_KEY'),
+ 'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
+ 'options' => [
+ 'cluster' => env('PUSHER_APP_CLUSTER'),
+ 'useTLS' => true,
+ ],
],
'redis' => [
@@ -38,6 +42,11 @@ return [
'driver' => 'log',
],
+ 'null' => [
+ 'driver' => 'null',
+ ],
+
+
],
];
diff --git a/app/Config/cache.php b/app/Config/cache.php
index 43f420457..33d3a1a0b 100644
--- a/app/Config/cache.php
+++ b/app/Config/cache.php
@@ -14,8 +14,12 @@ if (env('CACHE_DRIVER') === 'memcached') {
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
foreach ($memcachedServers as $index => $memcachedServer) {
$memcachedServerDetails = explode(':', $memcachedServer);
- if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
- if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
+ if (count($memcachedServerDetails) < 2) {
+ $memcachedServerDetails[] = '11211';
+ }
+ if (count($memcachedServerDetails) < 3) {
+ $memcachedServerDetails[] = '100';
+ }
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
}
}
@@ -62,6 +66,6 @@ return [
// Cache key prefix
// Used to prevent collisions in shared cache systems.
- 'prefix' => env('CACHE_PREFIX', 'bookstack'),
+ 'prefix' => env('CACHE_PREFIX', 'bookstack_cache'),
];
diff --git a/app/Config/database.php b/app/Config/database.php
index 93a44854f..ed654ffb9 100644
--- a/app/Config/database.php
+++ b/app/Config/database.php
@@ -11,10 +11,9 @@
// REDIS
// Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
-
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
- $redisConfig = [];
+ $redisConfig = ['client' => 'predis'];
$cluster = count($redisServers) > 1;
if ($cluster) {
@@ -59,14 +58,9 @@ return [
// Many of those shown here are unsupported by BookStack.
'connections' => [
- 'sqlite' => [
- 'driver' => 'sqlite',
- 'database' => storage_path('database.sqlite'),
- 'prefix' => '',
- ],
-
'mysql' => [
'driver' => 'mysql',
+ 'url' => env('DATABASE_URL'),
'host' => $mysql_host,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
@@ -76,43 +70,28 @@ return [
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
+ 'prefix_indexes' => true,
'strict' => false,
'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
],
'mysql_testing' => [
'driver' => 'mysql',
+ 'url' => env('TEST_DATABASE_URL'),
'host' => '127.0.0.1',
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
- 'charset' => 'utf8',
- 'collation' => 'utf8_unicode_ci',
+ 'charset' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
+ 'prefix_indexes' => true,
'strict' => false,
],
- 'pgsql' => [
- 'driver' => 'pgsql',
- 'host' => env('DB_HOST', 'localhost'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- 'schema' => 'public',
- ],
-
- 'sqlsrv' => [
- 'driver' => 'sqlsrv',
- 'host' => env('DB_HOST', 'localhost'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- ],
-
],
// Migration Repository Table
diff --git a/app/Config/debugbar.php b/app/Config/debugbar.php
index ec942dcd3..fe624eb7d 100644
--- a/app/Config/debugbar.php
+++ b/app/Config/debugbar.php
@@ -79,6 +79,7 @@ return [
'files' => false, // Show the included files
'config' => false, // Display config settings
'cache' => false, // Display cache events
+ 'models' => true, // Display models
],
// Configure some DataCollectors
diff --git a/app/Config/dompdf.php b/app/Config/dompdf.php
index 77f0cff9c..87be53df5 100644
--- a/app/Config/dompdf.php
+++ b/app/Config/dompdf.php
@@ -69,7 +69,7 @@ return [
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
- * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
+ * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
"DOMPDF_CHROOT" => realpath(base_path()),
diff --git a/app/Config/hashing.php b/app/Config/hashing.php
new file mode 100644
index 000000000..756718ce2
--- /dev/null
+++ b/app/Config/hashing.php
@@ -0,0 +1,37 @@
+ 'bcrypt',
+
+ // Bcrypt Options
+ // Here you may specify the configuration options that should be used when
+ // passwords are hashed using the Bcrypt algorithm. This will allow you
+ // to control the amount of time it takes to hash the given password.
+ 'bcrypt' => [
+ 'rounds' => env('BCRYPT_ROUNDS', 10),
+ ],
+
+ // Argon Options
+ // Here you may specify the configuration options that should be used when
+ // passwords are hashed using the Argon algorithm. These will allow you
+ // to control the amount of time it takes to hash the given password.
+ 'argon' => [
+ 'memory' => 1024,
+ 'threads' => 2,
+ 'time' => 2,
+ ],
+
+];
diff --git a/app/Config/logging.php b/app/Config/logging.php
new file mode 100644
index 000000000..0b55dc24d
--- /dev/null
+++ b/app/Config/logging.php
@@ -0,0 +1,82 @@
+ env('LOG_CHANNEL', 'single'),
+
+ // Log Channels
+ // Here you may configure the log channels for your application. Out of
+ // the box, Laravel uses the Monolog PHP logging library. This gives
+ // you a variety of powerful log handlers / formatters to utilize.
+ // Available Drivers: "single", "daily", "slack", "syslog",
+ // "errorlog", "monolog",
+ // "custom", "stack"
+ 'channels' => [
+ 'stack' => [
+ 'driver' => 'stack',
+ 'channels' => ['daily'],
+ 'ignore_exceptions' => false,
+ ],
+
+ 'single' => [
+ 'driver' => 'single',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => 'debug',
+ 'days' => 14,
+ ],
+
+ 'daily' => [
+ 'driver' => 'daily',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => 'debug',
+ 'days' => 7,
+ ],
+
+ 'slack' => [
+ 'driver' => 'slack',
+ 'url' => env('LOG_SLACK_WEBHOOK_URL'),
+ 'username' => 'Laravel Log',
+ 'emoji' => ':boom:',
+ 'level' => 'critical',
+ ],
+
+ 'stderr' => [
+ 'driver' => 'monolog',
+ 'handler' => StreamHandler::class,
+ 'with' => [
+ 'stream' => 'php://stderr',
+ ],
+ ],
+
+ 'syslog' => [
+ 'driver' => 'syslog',
+ 'level' => 'debug',
+ ],
+
+ 'errorlog' => [
+ 'driver' => 'errorlog',
+ 'level' => 'debug',
+ ],
+
+ 'null' => [
+ 'driver' => 'monolog',
+ 'handler' => NullHandler::class,
+ ],
+ ],
+
+];
diff --git a/app/Config/mail.php b/app/Config/mail.php
index 49407bd8e..a91bdf237 100644
--- a/app/Config/mail.php
+++ b/app/Config/mail.php
@@ -23,7 +23,7 @@ return [
// Global "From" address & name
'from' => [
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
- 'name' => env('MAIL_FROM_NAME','BookStack')
+ 'name' => env('MAIL_FROM_NAME', 'BookStack')
],
// Email encryption protocol
@@ -46,4 +46,10 @@ return [
],
],
+ // Log Channel
+ // If you are using the "log" driver, you may specify the logging channel
+ // if you prefer to keep mail messages separate from other log entries
+ // for simpler reading. Otherwise, the default channel will be used.
+ 'log_channel' => env('MAIL_LOG_CHANNEL'),
+
];
diff --git a/app/Config/queue.php b/app/Config/queue.php
index 721eac136..46f6962c5 100644
--- a/app/Config/queue.php
+++ b/app/Config/queue.php
@@ -12,11 +12,12 @@ return [
// Default driver to use for the queue
// Options: null, sync, redis
- 'default' => env('QUEUE_DRIVER', 'sync'),
+ 'default' => env('QUEUE_CONNECTION', 'sync'),
// Queue connection configuration
'connections' => [
+
'sync' => [
'driver' => 'sync',
],
@@ -25,38 +26,15 @@ return [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
- 'expire' => 60,
- ],
-
- 'beanstalkd' => [
- 'driver' => 'beanstalkd',
- 'host' => 'localhost',
- 'queue' => 'default',
- 'ttr' => 60,
- ],
-
- 'sqs' => [
- 'driver' => 'sqs',
- 'key' => 'your-public-key',
- 'secret' => 'your-secret-key',
- 'queue' => 'your-queue-url',
- 'region' => 'us-east-1',
- ],
-
- 'iron' => [
- 'driver' => 'iron',
- 'host' => 'mq-aws-us-east-1.iron.io',
- 'token' => 'your-token',
- 'project' => 'your-project-id',
- 'queue' => 'your-queue-name',
- 'encrypt' => true,
+ 'retry_after' => 90,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
- 'queue' => 'default',
- 'expire' => 60,
+ 'queue' => env('REDIS_QUEUE', 'default'),
+ 'retry_after' => 90,
+ 'block_for' => null,
],
],
diff --git a/app/Config/services.php b/app/Config/services.php
index b3dc9f087..0f80a9fc1 100644
--- a/app/Config/services.php
+++ b/app/Config/services.php
@@ -22,23 +22,6 @@ return [
// Callback URL for social authentication methods
'callback_url' => env('APP_URL', false),
- 'mailgun' => [
- 'domain' => '',
- 'secret' => '',
- ],
-
- 'ses' => [
- 'key' => '',
- 'secret' => '',
- 'region' => 'us-east-1',
- ],
-
- 'stripe' => [
- 'model' => \BookStack\Auth\User::class,
- 'key' => '',
- 'secret' => '',
- ],
-
'github' => [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),
@@ -143,9 +126,9 @@ return [
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
- 'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
+ 'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
- 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
+ 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
],
diff --git a/app/Config/session.php b/app/Config/session.php
index bdb5e554b..37f1627bb 100644
--- a/app/Config/session.php
+++ b/app/Config/session.php
@@ -35,13 +35,18 @@ return [
// Session database table, if database driver is in use
'table' => 'sessions',
+ // Session Cache Store
+ // When using the "apc" or "memcached" session drivers, you may specify a
+ // cache store that should be used for these sessions. This value must
+ // correspond with one of the application's configured cache stores.
+ 'store' => null,
+
// Session Sweeping Lottery
// Some session drivers must manually sweep their storage location to get
// rid of old sessions from storage. Here are the chances that it will
// happen on a given request. By default, the odds are 2 out of 100.
'lottery' => [2, 100],
-
// Session Cookie Name
// Here you may change the name of the cookie used to identify a session
// instance by ID. The name specified here will get used every time a
diff --git a/app/Config/setting-defaults.php b/app/Config/setting-defaults.php
index b48253eb1..c6080df1d 100644
--- a/app/Config/setting-defaults.php
+++ b/app/Config/setting-defaults.php
@@ -14,9 +14,9 @@ return [
'app-logo' => '',
'app-name-header' => true,
'app-editor' => 'wysiwyg',
- 'app-color' => '#0288D1',
- 'app-color-light' => 'rgba(21, 101, 192, 0.15)',
+ 'app-color' => '#206ea7',
+ 'app-color-light' => 'rgba(32,110,167,0.15)',
'app-custom-head' => false,
'registration-enabled' => false,
-];
\ No newline at end of file
+];
diff --git a/app/Entities/Book.php b/app/Entities/Book.php
index 7d3d5e4ae..4e54457b8 100644
--- a/app/Entities/Book.php
+++ b/app/Entities/Book.php
@@ -1,22 +1,25 @@
cover ? url($this->cover->getThumb($width, $height, false)) : $default;
- } catch (\Exception $err) {
+ } catch (Exception $err) {
$cover = $default;
}
return $cover;
@@ -53,16 +56,23 @@ class Book extends Entity
/**
* Get the cover image of the book
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function cover()
+ public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
+ /**
+ * Get the type of the image model that is used when storing a cover image.
+ */
+ public function coverImageTypeKey(): string
+ {
+ return 'cover_book';
+ }
+
/**
* Get all pages within this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function pages()
{
@@ -71,7 +81,7 @@ class Book extends Entity
/**
* Get the direct child pages of this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function directPages()
{
@@ -80,7 +90,7 @@ class Book extends Entity
/**
* Get all chapters within this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function chapters()
{
@@ -89,7 +99,7 @@ class Book extends Entity
/**
* Get the shelves this book is contained within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ * @return BelongsToMany
*/
public function shelves()
{
@@ -97,22 +107,24 @@ class Book extends Entity
}
/**
- * Get an excerpt of this book's description to the specified length or less.
- * @param int $length
- * @return string
+ * Get the direct child items within this book.
+ * @return Collection
*/
- public function getExcerpt(int $length = 100)
+ public function getDirectChildren(): Collection
{
- $description = $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
+ $pages = $this->directPages()->visible()->get();
+ $chapters = $this->chapters()->visible()->get();
+ return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get an excerpt of this book's description to the specified length or less.
+ * @param int $length
* @return string
*/
- public function entityRawQuery()
+ public function getExcerpt(int $length = 100)
{
- return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ $description = $this->description;
+ return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
}
diff --git a/app/Entities/BookChild.php b/app/Entities/BookChild.php
new file mode 100644
index 000000000..6eac4375d
--- /dev/null
+++ b/app/Entities/BookChild.php
@@ -0,0 +1,60 @@
+with('book')
+ ->whereHas('book', function (Builder $query) use ($bookSlug) {
+ $query->where('slug', '=', $bookSlug);
+ })
+ ->where('slug', '=', $childSlug);
+ }
+
+ /**
+ * Get the book this page sits in.
+ * @return BelongsTo
+ */
+ public function book(): BelongsTo
+ {
+ return $this->belongsTo(Book::class);
+ }
+
+ /**
+ * Change the book that this entity belongs to.
+ */
+ public function changeBook(int $newBookId): Entity
+ {
+ $this->book_id = $newBookId;
+ $this->refreshSlug();
+ $this->save();
+ $this->refresh();
+
+ // Update related activity
+ $this->activity()->update(['book_id' => $newBookId]);
+
+ // Update all child pages if a chapter
+ if ($this instanceof Chapter) {
+ foreach ($this->pages as $page) {
+ $page->changeBook($newBookId);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/app/Entities/Bookshelf.php b/app/Entities/Bookshelf.php
index db6685688..62c7e2fe4 100644
--- a/app/Entities/Bookshelf.php
+++ b/app/Entities/Bookshelf.php
@@ -1,8 +1,10 @@
orderBy('order', 'asc');
}
+ /**
+ * Related books that are visible to the current user.
+ */
+ public function visibleBooks(): BelongsToMany
+ {
+ return $this->books()->visible();
+ }
+
/**
* Get the url for this bookshelf.
* @param string|bool $path
@@ -68,13 +69,20 @@ class Bookshelf extends Entity
/**
* Get the cover image of the shelf
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function cover()
+ public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
+ /**
+ * Get the type of the image model that is used when storing a cover image.
+ */
+ public function coverImageTypeKey(): string
+ {
+ return 'cover_shelf';
+ }
+
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
@@ -87,21 +95,26 @@ class Bookshelf extends Entity
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this shelf contains the given book.
+ * @param Book $book
+ * @return bool
*/
- public function entityRawQuery()
+ public function contains(Book $book): bool
{
- return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ return $this->books()->where('id', '=', $book->id)->count() > 0;
}
/**
- * Check if this shelf contains the given book.
+ * Add a book to the end of this shelf.
* @param Book $book
- * @return bool
*/
- public function contains(Book $book)
+ public function appendBook(Book $book)
{
- return $this->books()->where('id', '=', $book->id)->count() > 0;
+ if ($this->contains($book)) {
+ return;
+ }
+
+ $maxOrder = $this->books()->max('order');
+ $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
}
}
diff --git a/app/Entities/BreadcrumbsViewComposer.php b/app/Entities/BreadcrumbsViewComposer.php
index 97ddbc2dc..43d63d026 100644
--- a/app/Entities/BreadcrumbsViewComposer.php
+++ b/app/Entities/BreadcrumbsViewComposer.php
@@ -1,5 +1,6 @@
entityContextManager = $entityContextManager;
}
@@ -23,8 +24,9 @@ class BreadcrumbsViewComposer
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
- if (array_first($crumbs) instanceof Book) {
- $shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
+ $firstCrumb = $crumbs[0] ?? null;
+ if ($firstCrumb instanceof Book) {
+ $shelf = $this->entityContextManager->getContextualShelfForBook($firstCrumb);
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
diff --git a/app/Entities/Chapter.php b/app/Entities/Chapter.php
index b204f1903..848bc6448 100644
--- a/app/Entities/Chapter.php
+++ b/app/Entities/Chapter.php
@@ -1,29 +1,18 @@
$pages
+ * @package BookStack\Entities
+ */
+class Chapter extends BookChild
{
public $searchFactor = 1.3;
protected $fillable = ['name', 'description', 'priority', 'book_id'];
- /**
- * Get the morph class for this model.
- * @return string
- */
- public function getMorphClass()
- {
- return 'BookStack\\Chapter';
- }
-
- /**
- * Get the book this chapter is within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function book()
- {
- return $this->belongsTo(Book::class);
- }
-
/**
* Get the pages that this chapter contains.
* @param string $dir
@@ -63,20 +52,22 @@ class Chapter extends Entity
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this chapter has any child pages.
+ * @return bool
*/
- public function entityRawQuery()
+ public function hasChildren()
{
- return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ return count($this->pages) > 0;
}
/**
- * Check if this chapter has any child pages.
- * @return bool
+ * Get the visible pages in this chapter.
*/
- public function hasChildren()
+ public function getVisiblePages(): Collection
{
- return count($this->pages) > 0;
+ return $this->pages()->visible()
+ ->orderBy('draft', 'desc')
+ ->orderBy('priority', 'asc')
+ ->get();
}
}
diff --git a/app/Entities/Entity.php b/app/Entities/Entity.php
index 482d21766..5013c39cf 100644
--- a/app/Entities/Entity.php
+++ b/app/Entities/Entity.php
@@ -6,8 +6,11 @@ use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Facades\Permissions;
use BookStack\Ownable;
use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
@@ -15,7 +18,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
* The base class for book-like items such as pages, chapters & books.
* This is not a database model in itself but extended.
*
- * @property integer $id
+ * @property int $id
* @property string $name
* @property string $slug
* @property Carbon $created_at
@@ -23,6 +26,11 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
* @property int $created_by
* @property int $updated_by
* @property boolean $restricted
+ * @property Collection $tags
+ * @method static Entity|Builder visible()
+ * @method static Entity|Builder hasPermission(string $permission)
+ * @method static Builder withLastView()
+ * @method static Builder withViewCount()
*
* @package BookStack\Entities
*/
@@ -40,14 +48,45 @@ class Entity extends Ownable
public $searchFactor = 1.0;
/**
- * Get the morph class for this model.
- * Set here since, due to folder changes, the namespace used
- * in the database no longer matches the class namespace.
- * @return string
+ * Get the entities that are visible to the current user.
+ */
+ public function scopeVisible(Builder $query)
+ {
+ return $this->scopeHasPermission($query, 'view');
+ }
+
+ /**
+ * Scope the query to those entities that the current user has the given permission for.
+ */
+ public function scopeHasPermission(Builder $query, string $permission)
+ {
+ return Permissions::restrictEntityQuery($query, $permission);
+ }
+
+ /**
+ * Query scope to get the last view from the current user.
*/
- public function getMorphClass()
+ public function scopeWithLastView(Builder $query)
{
- return 'BookStack\\Entity';
+ $viewedAtQuery = View::query()->select('updated_at')
+ ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+ ->where('viewable_type', '=', $this->getMorphClass())
+ ->where('user_id', '=', user()->id)
+ ->take(1);
+
+ return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
+ }
+
+ /**
+ * Query scope to get the total view count of the entities.
+ */
+ public function scopeWithViewCount(Builder $query)
+ {
+ $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
+ ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+ ->where('viewable_type', '=', $this->getMorphClass())->take(1);
+
+ $query->addSelect(['view_count' => $viewCountQuery]);
}
/**
@@ -87,11 +126,12 @@ class Entity extends Ownable
/**
* Gets the activity objects for this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function activity()
{
- return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
+ return $this->morphMany(Activity::class, 'entity')
+ ->orderBy('created_at', 'desc');
}
/**
@@ -102,14 +142,9 @@ class Entity extends Ownable
return $this->morphMany(View::class, 'viewable');
}
- public function viewCountQuery()
- {
- return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
- }
-
/**
* Get the Tag models that have been user assigned to this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function tags()
{
@@ -129,7 +164,7 @@ class Entity extends Ownable
/**
* Get the related search terms.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function searchTerms()
{
@@ -158,7 +193,7 @@ class Entity extends Ownable
/**
* Get the entity jointPermissions this is connected to.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function jointPermissions()
{
@@ -238,21 +273,40 @@ class Entity extends Ownable
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get the url of this entity
+ * @param $path
* @return string
*/
- public function entityRawQuery()
+ public function getUrl($path = '/')
{
- return '';
+ return $path;
}
/**
- * Get the url of this entity
- * @param $path
- * @return string
+ * Rebuild the permissions for this entity.
*/
- public function getUrl($path = '/')
+ public function rebuildPermissions()
{
- return $path;
+ /** @noinspection PhpUnhandledExceptionInspection */
+ Permissions::buildJointPermissionsForEntity($this);
+ }
+
+ /**
+ * Index the current entity for search
+ */
+ public function indexForSearch()
+ {
+ $searchService = app()->make(SearchService::class);
+ $searchService->indexEntity($this);
+ }
+
+ /**
+ * Generate and set a new URL slug for this model.
+ */
+ public function refreshSlug(): string
+ {
+ $generator = new SlugGenerator($this);
+ $this->slug = $generator->generate();
+ return $this->slug;
}
}
diff --git a/app/Entities/EntityProvider.php b/app/Entities/EntityProvider.php
index d0d4a7ad6..6bf923b31 100644
--- a/app/Entities/EntityProvider.php
+++ b/app/Entities/EntityProvider.php
@@ -39,11 +39,6 @@ class EntityProvider
/**
* EntityProvider constructor.
- * @param Bookshelf $bookshelf
- * @param Book $book
- * @param Chapter $chapter
- * @param Page $page
- * @param PageRevision $pageRevision
*/
public function __construct(
Bookshelf $bookshelf,
@@ -62,9 +57,8 @@ class EntityProvider
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
- * @return Entity[]
*/
- public function all()
+ public function all(): array
{
return [
'bookshelf' => $this->bookshelf,
@@ -76,10 +70,8 @@ class EntityProvider
/**
* Get an entity instance by it's basic name.
- * @param string $type
- * @return Entity
*/
- public function get(string $type)
+ public function get(string $type): Entity
{
$type = strtolower($type);
return $this->all()[$type];
@@ -87,15 +79,9 @@ class EntityProvider
/**
* Get the morph classes, as an array, for a single or multiple types.
- * @param string|array $types
- * @return array
*/
- public function getMorphClasses($types)
+ public function getMorphClasses(array $types): array
{
- if (is_string($types)) {
- $types = [$types];
- }
-
$morphClasses = [];
foreach ($types as $type) {
$model = $this->get($type);
diff --git a/app/Entities/ExportService.php b/app/Entities/ExportService.php
index 09635aa21..3ec867959 100644
--- a/app/Entities/ExportService.php
+++ b/app/Entities/ExportService.php
@@ -1,35 +1,34 @@
entityRepo = $entityRepo;
$this->imageService = $imageService;
}
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
- * @param \BookStack\Entities\Page $page
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function pageToContainedHtml(Page $page)
{
- $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
$pageHtml = view('pages/export', [
'page' => $page
])->render();
@@ -38,15 +37,13 @@ class ExportService
/**
* Convert a chapter to a self-contained HTML file.
- * @param \BookStack\Entities\Chapter $chapter
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function chapterToContainedHtml(Chapter $chapter)
{
- $pages = $this->entityRepo->getChapterChildren($chapter);
+ $pages = $chapter->getVisiblePages();
$pages->each(function ($page) {
- $page->html = $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
});
$html = view('chapters/export', [
'chapter' => $chapter,
@@ -57,13 +54,11 @@ class ExportService
/**
* Convert a book to a self-contained HTML file.
- * @param Book $book
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function bookToContainedHtml(Book $book)
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
@@ -73,13 +68,11 @@ class ExportService
/**
* Convert a page to a PDF file.
- * @param Page $page
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function pageToPdf(Page $page)
{
- $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
$html = view('pages/pdf', [
'page' => $page
])->render();
@@ -88,32 +81,30 @@ class ExportService
/**
* Convert a chapter to a PDF file.
- * @param \BookStack\Entities\Chapter $chapter
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function chapterToPdf(Chapter $chapter)
{
- $pages = $this->entityRepo->getChapterChildren($chapter);
+ $pages = $chapter->getVisiblePages();
$pages->each(function ($page) {
- $page->html = $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
});
+
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
+
return $this->htmlToPdf($html);
}
/**
- * Convert a book to a PDF file
- * @param \BookStack\Entities\Book $book
- * @return string
- * @throws \Throwable
+ * Convert a book to a PDF file.
+ * @throws Throwable
*/
public function bookToPdf(Book $book)
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
@@ -122,31 +113,27 @@ class ExportService
}
/**
- * Convert normal webpage HTML to a PDF.
- * @param $html
- * @return string
- * @throws \Exception
+ * Convert normal web-page HTML to a PDF.
+ * @throws Exception
*/
- protected function htmlToPdf($html)
+ protected function htmlToPdf(string $html): string
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
- $pdf = \SnappyPDF::loadHTML($containedHtml);
+ $pdf = SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
- $pdf = \DomPDF::loadHTML($containedHtml);
+ $pdf = DomPDF::loadHTML($containedHtml);
}
return $pdf->output();
}
/**
* Bundle of the contents of a html file to be self-contained.
- * @param $htmlContent
- * @return mixed|string
- * @throws \Exception
+ * @throws Exception
*/
- protected function containHtml($htmlContent)
+ protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
preg_match_all("/\/i", $htmlContent, $imageTagsOutput);
@@ -188,12 +175,10 @@ class ExportService
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to provide a nice final output.
- * @param Page $page
- * @return mixed
*/
- public function pageToPlainText(Page $page)
+ public function pageToPlainText(Page $page): string
{
- $html = $this->entityRepo->renderPage($page);
+ $html = (new PageContent($page))->render();
$text = strip_tags($html);
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
@@ -207,10 +192,8 @@ class ExportService
/**
* Convert a chapter into a plain text string.
- * @param \BookStack\Entities\Chapter $chapter
- * @return string
*/
- public function chapterToPlainText(Chapter $chapter)
+ public function chapterToPlainText(Chapter $chapter): string
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
@@ -222,12 +205,10 @@ class ExportService
/**
* Convert a book into a plain text string.
- * @param Book $book
- * @return string
*/
- public function bookToPlainText(Book $book)
+ public function bookToPlainText(Book $book): string
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
diff --git a/app/Entities/HasCoverImage.php b/app/Entities/HasCoverImage.php
new file mode 100644
index 000000000..31277f4b6
--- /dev/null
+++ b/app/Entities/HasCoverImage.php
@@ -0,0 +1,20 @@
+book = $book;
+ }
+
+ /**
+ * Get the current priority of the last item
+ * at the top-level of the book.
+ */
+ public function getLastPriority(): int
+ {
+ $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
+ ->where('draft', '=', false)
+ ->where('chapter_id', '=', 0)->max('priority');
+ $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
+ ->max('priority');
+ return max($maxChapter, $maxPage, 1);
+ }
+
+ /**
+ * Get the contents as a sorted collection tree.
+ * TODO - Support $renderPages option
+ */
+ public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
+ {
+ $pages = $this->getPages($showDrafts);
+ $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
+ $all = collect()->concat($pages)->concat($chapters);
+ $chapterMap = $chapters->keyBy('id');
+ $lonePages = collect();
+
+ $pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
+ $chapter = $chapterMap->get($chapter_id);
+ if ($chapter) {
+ $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
+ } else {
+ $lonePages = $lonePages->concat($pages);
+ }
+ });
+
+ $all->each(function (Entity $entity) {
+ $entity->setRelation('book', $this->book);
+ });
+
+ return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
+ }
+
+ /**
+ * Function for providing a sorting score for an entity in relation to the
+ * other items within the book.
+ */
+ protected function bookChildSortFunc(): callable
+ {
+ return function (Entity $entity) {
+ if (isset($entity['draft']) && $entity['draft']) {
+ return -100;
+ }
+ return $entity['priority'] ?? 0;
+ };
+ }
+
+ /**
+ * Get the visible pages within this book.
+ */
+ protected function getPages(bool $showDrafts = false): Collection
+ {
+ $query = Page::visible()->where('book_id', '=', $this->book->id);
+
+ if (!$showDrafts) {
+ $query->where('draft', '=', false);
+ }
+
+ return $query->get();
+ }
+
+ /**
+ * Sort the books content using the given map.
+ * The map is a single-dimension collection of objects in the following format:
+ * {
+ * +"id": "294" (ID of item)
+ * +"sort": 1 (Sort order index)
+ * +"parentChapter": false (ID of parent chapter, as string, or false)
+ * +"type": "page" (Entity type of item)
+ * +"book": "1" (Id of book to place item in)
+ * }
+ *
+ * Returns a list of books that were involved in the operation.
+ * @throws SortOperationException
+ */
+ public function sortUsingMap(Collection $sortMap): Collection
+ {
+ // Load models into map
+ $this->loadModelsIntoSortMap($sortMap);
+ $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
+
+ // Perform the sort
+ $sortMap->each(function ($mapItem) {
+ $this->applySortUpdates($mapItem);
+ });
+
+ // Update permissions and activity.
+ $booksInvolved->each(function (Book $book) {
+ $book->rebuildPermissions();
+ });
+
+ return $booksInvolved;
+ }
+
+ /**
+ * Using the given sort map item, detect changes for the related model
+ * and update it if required.
+ */
+ protected function applySortUpdates(\stdClass $sortMapItem)
+ {
+ /** @var BookChild $model */
+ $model = $sortMapItem->model;
+
+ $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
+ $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
+ $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
+
+ if ($bookChanged) {
+ $model->changeBook($sortMapItem->book);
+ }
+
+ if ($chapterChanged) {
+ $model->chapter_id = intval($sortMapItem->parentChapter);
+ $model->save();
+ }
+
+ if ($priorityChanged) {
+ $model->priority = intval($sortMapItem->sort);
+ $model->save();
+ }
+ }
+
+ /**
+ * Load models from the database into the given sort map.
+ */
+ protected function loadModelsIntoSortMap(Collection $sortMap): void
+ {
+ $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
+ return $sortMapItem->type . ':' . $sortMapItem->id;
+ });
+ $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
+ $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
+
+ $pages = Page::visible()->whereIn('id', $pageIds)->get();
+ $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
+
+ foreach ($pages as $page) {
+ $sortItem = $keyMap->get('page:' . $page->id);
+ $sortItem->model = $page;
+ }
+
+ foreach ($chapters as $chapter) {
+ $sortItem = $keyMap->get('chapter:' . $chapter->id);
+ $sortItem->model = $chapter;
+ }
+ }
+
+ /**
+ * Get the books involved in a sort.
+ * The given sort map should have its models loaded first.
+ * @throws SortOperationException
+ */
+ protected function getBooksInvolvedInSort(Collection $sortMap): Collection
+ {
+ $bookIdsInvolved = collect([$this->book->id]);
+ $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
+ $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
+ $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
+
+ $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
+
+ if (count($books) !== count($bookIdsInvolved)) {
+ throw new SortOperationException("Could not find all books requested in sort operation");
+ }
+
+ return $books;
+ }
+}
diff --git a/app/Entities/EntityContextManager.php b/app/Entities/Managers/EntityContext.php
similarity index 53%
rename from app/Entities/EntityContextManager.php
rename to app/Entities/Managers/EntityContext.php
index 20be0de2b..551cd1a10 100644
--- a/app/Entities/EntityContextManager.php
+++ b/app/Entities/Managers/EntityContext.php
@@ -1,44 +1,38 @@
-session = $session;
- $this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
- * @param Book $book
- * @return Bookshelf|null
*/
- public function getContextualShelfForBook(Book $book)
+ public function getContextualShelfForBook(Book $book): ?Bookshelf
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
- if (is_int($contextBookshelfId)) {
- /** @var Bookshelf $shelf */
- $shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
-
- if ($shelf && $shelf->contains($book)) {
- return $shelf;
- }
+ if (!is_int($contextBookshelfId)) {
+ return null;
}
- return null;
+
+ $shelf = Bookshelf::visible()->find($contextBookshelfId);
+ $shelfContainsBook = $shelf && $shelf->contains($book);
+
+ return $shelfContainsBook ? $shelf : null;
}
/**
diff --git a/app/Entities/Managers/PageContent.php b/app/Entities/Managers/PageContent.php
new file mode 100644
index 000000000..36bc2445c
--- /dev/null
+++ b/app/Entities/Managers/PageContent.php
@@ -0,0 +1,304 @@
+page = $page;
+ }
+
+ /**
+ * Update the content of the page with new provided HTML.
+ */
+ public function setNewHTML(string $html)
+ {
+ $this->page->html = $this->formatHtml($html);
+ $this->page->text = $this->toPlainText();
+ }
+
+ /**
+ * Formats a page's html to be tagged correctly within the system.
+ */
+ protected function formatHtml(string $htmlText): string
+ {
+ if ($htmlText == '') {
+ return $htmlText;
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
+
+ $container = $doc->documentElement;
+ $body = $container->childNodes->item(0);
+ $childNodes = $body->childNodes;
+
+ // Set ids on top-level nodes
+ $idMap = [];
+ foreach ($childNodes as $index => $childNode) {
+ $this->setUniqueId($childNode, $idMap);
+ }
+
+ // Ensure no duplicate ids within child items
+ $xPath = new DOMXPath($doc);
+ $idElems = $xPath->query('//body//*//*[@id]');
+ foreach ($idElems as $domElem) {
+ $this->setUniqueId($domElem, $idMap);
+ }
+
+ // Generate inner html as a string
+ $html = '';
+ foreach ($childNodes as $childNode) {
+ $html .= $doc->saveHTML($childNode);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Set a unique id on the given DOMElement.
+ * A map for existing ID's should be passed in to check for current existence.
+ * @param DOMElement $element
+ * @param array $idMap
+ */
+ protected function setUniqueId($element, array &$idMap)
+ {
+ if (get_class($element) !== 'DOMElement') {
+ return;
+ }
+
+ // Overwrite id if not a BookStack custom id
+ $existingId = $element->getAttribute('id');
+ if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
+ $idMap[$existingId] = true;
+ return;
+ }
+
+ // Create an unique id for the element
+ // Uses the content as a basis to ensure output is the same every time
+ // the same content is passed through.
+ $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
+ $newId = urlencode($contentId);
+ $loopIndex = 0;
+
+ while (isset($idMap[$newId])) {
+ $newId = urlencode($contentId . '-' . $loopIndex);
+ $loopIndex++;
+ }
+
+ $element->setAttribute('id', $newId);
+ $idMap[$newId] = true;
+ }
+
+ /**
+ * Get a plain-text visualisation of this page.
+ */
+ protected function toPlainText(): string
+ {
+ $html = $this->render(true);
+ return strip_tags($html);
+ }
+
+ /**
+ * Render the page for viewing
+ */
+ public function render(bool $blankIncludes = false) : string
+ {
+ $content = $this->page->html;
+
+ if (!config('app.allow_content_scripts')) {
+ $content = $this->escapeScripts($content);
+ }
+
+ if ($blankIncludes) {
+ $content = $this->blankPageIncludes($content);
+ } else {
+ $content = $this->parsePageIncludes($content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Parse the headers on the page to get a navigation menu
+ */
+ public function getNavigation(string $htmlContent): array
+ {
+ if (empty($htmlContent)) {
+ return [];
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
+ $xPath = new DOMXPath($doc);
+ $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
+
+ return $headers ? $this->headerNodesToLevelList($headers) : [];
+ }
+
+ /**
+ * Convert a DOMNodeList into an array of readable header attributes
+ * with levels normalised to the lower header level.
+ */
+ protected function headerNodesToLevelList(DOMNodeList $nodeList): array
+ {
+ $tree = collect($nodeList)->map(function ($header) {
+ $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
+ $text = mb_substr($text, 0, 100);
+
+ return [
+ 'nodeName' => strtolower($header->nodeName),
+ 'level' => intval(str_replace('h', '', $header->nodeName)),
+ 'link' => '#' . $header->getAttribute('id'),
+ 'text' => $text,
+ ];
+ })->filter(function ($header) {
+ return mb_strlen($header['text']) > 0;
+ });
+
+ // Shift headers if only smaller headers have been used
+ $levelChange = ($tree->pluck('level')->min() - 1);
+ $tree = $tree->map(function ($header) use ($levelChange) {
+ $header['level'] -= ($levelChange);
+ return $header;
+ });
+
+ return $tree->toArray();
+ }
+
+ /**
+ * Remove any page include tags within the given HTML.
+ */
+ protected function blankPageIncludes(string $html) : string
+ {
+ return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
+ }
+
+ /**
+ * Parse any include tags "{{@#section}}" to be part of the page.
+ */
+ protected function parsePageIncludes(string $html) : string
+ {
+ $matches = [];
+ preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
+
+ foreach ($matches[1] as $index => $includeId) {
+ $fullMatch = $matches[0][$index];
+ $splitInclude = explode('#', $includeId, 2);
+
+ // Get page id from reference
+ $pageId = intval($splitInclude[0]);
+ if (is_nan($pageId)) {
+ continue;
+ }
+
+ // Find page and skip this if page not found
+ $matchedPage = Page::visible()->find($pageId);
+ if ($matchedPage === null) {
+ $html = str_replace($fullMatch, '', $html);
+ continue;
+ }
+
+ // If we only have page id, just insert all page html and continue.
+ if (count($splitInclude) === 1) {
+ $html = str_replace($fullMatch, $matchedPage->html, $html);
+ continue;
+ }
+
+ // Create and load HTML into a document
+ $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
+ $html = str_replace($fullMatch, trim($innerContent), $html);
+ }
+
+ return $html;
+ }
+
+
+ /**
+ * Fetch the content from a specific section of the given page.
+ */
+ protected function fetchSectionOfPage(Page $page, string $sectionId): string
+ {
+ $topLevelTags = ['table', 'ul', 'ol'];
+ $doc = new DOMDocument();
+ libxml_use_internal_errors(true);
+ $doc->loadHTML(mb_convert_encoding(''.$page->html.'', 'HTML-ENTITIES', 'UTF-8'));
+
+ // Search included content for the id given and blank out if not exists.
+ $matchingElem = $doc->getElementById($sectionId);
+ if ($matchingElem === null) {
+ return '';
+ }
+
+ // Otherwise replace the content with the found content
+ // Checks if the top-level wrapper should be included by matching on tag types
+ $innerContent = '';
+ $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
+ if ($isTopLevel) {
+ $innerContent .= $doc->saveHTML($matchingElem);
+ } else {
+ foreach ($matchingElem->childNodes as $childNode) {
+ $innerContent .= $doc->saveHTML($childNode);
+ }
+ }
+ libxml_clear_errors();
+
+ return $innerContent;
+ }
+
+ /**
+ * Escape script tags within HTML content.
+ */
+ protected function escapeScripts(string $html) : string
+ {
+ if (empty($html)) {
+ return $html;
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+ $xPath = new DOMXPath($doc);
+
+ // Remove standard script tags
+ $scriptElems = $xPath->query('//script');
+ foreach ($scriptElems as $scriptElem) {
+ $scriptElem->parentNode->removeChild($scriptElem);
+ }
+
+ // Remove data or JavaScript iFrames
+ $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
+ foreach ($badIframes as $badIframe) {
+ $badIframe->parentNode->removeChild($badIframe);
+ }
+
+ // Remove 'on*' attributes
+ $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
+ foreach ($onAttributes as $attr) {
+ /** @var \DOMAttr $attr*/
+ $attrName = $attr->nodeName;
+ $attr->parentNode->removeAttribute($attrName);
+ }
+
+ $html = '';
+ $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
+ foreach ($topElems as $child) {
+ $html .= $doc->saveHTML($child);
+ }
+
+ return $html;
+ }
+}
diff --git a/app/Entities/Managers/PageEditActivity.php b/app/Entities/Managers/PageEditActivity.php
new file mode 100644
index 000000000..cebbf8720
--- /dev/null
+++ b/app/Entities/Managers/PageEditActivity.php
@@ -0,0 +1,74 @@
+page = $page;
+ }
+
+ /**
+ * Check if there's active editing being performed on this page.
+ * @return bool
+ */
+ public function hasActiveEditing(): bool
+ {
+ return $this->activePageEditingQuery(60)->count() > 0;
+ }
+
+ /**
+ * Get a notification message concerning the editing activity on the page.
+ */
+ public function activeEditingMessage(): string
+ {
+ $pageDraftEdits = $this->activePageEditingQuery(60)->get();
+ $count = $pageDraftEdits->count();
+
+ $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+ $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
+ return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+ }
+
+ /**
+ * Get the message to show when the user will be editing one of their drafts.
+ * @param PageRevision $draft
+ * @return string
+ */
+ public function getEditingActiveDraftMessage(PageRevision $draft): string
+ {
+ $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
+ if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
+ return $message;
+ }
+ return $message . "\n" . trans('entities.pages_draft_edited_notification');
+ }
+
+ /**
+ * A query to check for active update drafts on a particular page
+ * within the last given many minutes.
+ */
+ protected function activePageEditingQuery(int $withinMinutes): Builder
+ {
+ $checkTime = Carbon::now()->subMinutes($withinMinutes);
+ $query = PageRevision::query()
+ ->where('type', '=', 'update_draft')
+ ->where('page_id', '=', $this->page->id)
+ ->where('updated_at', '>', $this->page->updated_at)
+ ->where('created_by', '!=', user()->id)
+ ->where('updated_at', '>=', $checkTime)
+ ->with('createdBy');
+
+ return $query;
+ }
+}
diff --git a/app/Entities/Managers/TrashCan.php b/app/Entities/Managers/TrashCan.php
new file mode 100644
index 000000000..1a32294fc
--- /dev/null
+++ b/app/Entities/Managers/TrashCan.php
@@ -0,0 +1,109 @@
+destroyCommonRelations($shelf);
+ $shelf->delete();
+ }
+
+ /**
+ * Remove a book from the system.
+ * @throws NotifyException
+ * @throws BindingResolutionException
+ */
+ public function destroyBook(Book $book)
+ {
+ foreach ($book->pages as $page) {
+ $this->destroyPage($page);
+ }
+
+ foreach ($book->chapters as $chapter) {
+ $this->destroyChapter($chapter);
+ }
+
+ $this->destroyCommonRelations($book);
+ $book->delete();
+ }
+
+ /**
+ * Remove a page from the system.
+ * @throws NotifyException
+ */
+ public function destroyPage(Page $page)
+ {
+ // Check if set as custom homepage & remove setting if not used or throw error if active
+ $customHome = setting('app-homepage', '0:');
+ if (intval($page->id) === intval(explode(':', $customHome)[0])) {
+ if (setting('app-homepage-type') === 'page') {
+ throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+ }
+ setting()->remove('app-homepage');
+ }
+
+ $this->destroyCommonRelations($page);
+
+ // Delete Attached Files
+ $attachmentService = app(AttachmentService::class);
+ foreach ($page->attachments as $attachment) {
+ $attachmentService->deleteFile($attachment);
+ }
+
+ $page->delete();
+ }
+
+ /**
+ * Remove a chapter from the system.
+ * @throws Exception
+ */
+ public function destroyChapter(Chapter $chapter)
+ {
+ if (count($chapter->pages) > 0) {
+ foreach ($chapter->pages as $page) {
+ $page->chapter_id = 0;
+ $page->save();
+ }
+ }
+
+ $this->destroyCommonRelations($chapter);
+ $chapter->delete();
+ }
+
+ /**
+ * Update entity relations to remove or update outstanding connections.
+ */
+ protected function destroyCommonRelations(Entity $entity)
+ {
+ Activity::removeEntity($entity);
+ $entity->views()->delete();
+ $entity->permissions()->delete();
+ $entity->tags()->delete();
+ $entity->comments()->delete();
+ $entity->jointPermissions()->delete();
+ $entity->searchTerms()->delete();
+
+ if ($entity instanceof HasCoverImage && $entity->cover) {
+ $imageService = app()->make(ImageService::class);
+ $imageService->destroy($entity->cover);
+ }
+ }
+}
diff --git a/app/Entities/Page.php b/app/Entities/Page.php
index c32417418..76dc628fb 100644
--- a/app/Entities/Page.php
+++ b/app/Entities/Page.php
@@ -1,8 +1,25 @@
belongsTo(Book::class);
- }
-
/**
* Get the parent item
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function parent()
+ public function parent(): Entity
{
- return $this->chapter_id ? $this->chapter() : $this->book();
+ return $this->chapter_id ? $this->chapter : $this->book;
}
/**
* Get the chapter that this page is in, If applicable.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ * @return BelongsTo
*/
public function chapter()
{
@@ -72,12 +79,12 @@ class Page extends Entity
*/
public function revisions()
{
- return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
+ return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
}
/**
* Get the attachments assigned to this page.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function attachments()
{
@@ -95,27 +102,17 @@ class Page extends Entity
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
+ $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
if ($path !== false) {
- return url('/https/source.bookstackapp.com/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
+ $url .= '/' . trim($path, '/');
}
- return url('/https/source.bookstackapp.com/books/' . urlencode($bookSlug) . $midText . $idComponent);
- }
-
- /**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @param bool $withContent
- * @return string
- */
- public function entityRawQuery($withContent = false)
- {
- $htmlQuery = $withContent ? 'html' : "'' as html";
- return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
+ return url($url);
}
/**
* Get the current revision for the page if existing
- * @return \BookStack\Entities\PageRevision|null
+ * @return PageRevision|null
*/
public function getCurrentRevision()
{
diff --git a/app/Entities/PageRevision.php b/app/Entities/PageRevision.php
index d30147bfc..13dc713ba 100644
--- a/app/Entities/PageRevision.php
+++ b/app/Entities/PageRevision.php
@@ -2,7 +2,21 @@
use BookStack\Auth\User;
use BookStack\Model;
+use Carbon\Carbon;
+/**
+ * Class PageRevision
+ * @property int $page_id
+ * @property string $slug
+ * @property string $book_slug
+ * @property int $created_by
+ * @property Carbon $created_at
+ * @property string $type
+ * @property string $summary
+ * @property string $markdown
+ * @property string $html
+ * @property int $revision_number
+ */
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@@ -41,13 +55,18 @@ class PageRevision extends Model
/**
* Get the previous revision for the same page if existing
- * @return \BookStack\PageRevision|null
+ * @return \BookStack\Entities\PageRevision|null
*/
public function getPrevious()
{
- if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
- return static::find($id);
+ $id = static::newQuery()->where('page_id', '=', $this->page_id)
+ ->where('id', '<', $this->id)
+ ->max('id');
+
+ if ($id) {
+ return static::query()->find($id);
}
+
return null;
}
diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php
new file mode 100644
index 000000000..23f07f820
--- /dev/null
+++ b/app/Entities/Repos/BaseRepo.php
@@ -0,0 +1,119 @@
+tagRepo = $tagRepo;
+ $this->imageRepo = $imageRepo;
+ }
+
+ /**
+ * Create a new entity in the system
+ */
+ public function create(Entity $entity, array $input)
+ {
+ $entity->fill($input);
+ $entity->forceFill([
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ ]);
+ $entity->refreshSlug();
+ $entity->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+ }
+
+ $entity->rebuildPermissions();
+ $entity->indexForSearch();
+ }
+
+ /**
+ * Update the given entity.
+ */
+ public function update(Entity $entity, array $input)
+ {
+ $entity->fill($input);
+ $entity->updated_by = user()->id;
+
+ if ($entity->isDirty('name')) {
+ $entity->refreshSlug();
+ }
+
+ $entity->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+ }
+
+ $entity->rebuildPermissions();
+ $entity->indexForSearch();
+ }
+
+ /**
+ * Update the given items' cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws \Exception
+ */
+ public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ if ($coverImage) {
+ $this->imageRepo->destroyImage($entity->cover);
+ $image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
+ $entity->cover()->associate($image);
+ $entity->save();
+ }
+
+ if ($removeImage) {
+ $this->imageRepo->destroyImage($entity->cover);
+ $entity->image_id = 0;
+ $entity->save();
+ }
+ }
+
+ /**
+ * Update the permissions of an entity.
+ */
+ public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
+ {
+ $entity->restricted = $restricted;
+ $entity->permissions()->delete();
+
+ if (!is_null($permissions)) {
+ $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
+ return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+ return [
+ 'role_id' => $roleId,
+ 'action' => strtolower($action),
+ ] ;
+ });
+ });
+
+ $entity->permissions()->createMany($entityPermissionData);
+ }
+
+ $entity->save();
+ $entity->rebuildPermissions();
+ }
+}
diff --git a/app/Entities/Repos/BookRepo.php b/app/Entities/Repos/BookRepo.php
new file mode 100644
index 000000000..7fcc80fac
--- /dev/null
+++ b/app/Entities/Repos/BookRepo.php
@@ -0,0 +1,134 @@
+baseRepo = $baseRepo;
+ $this->tagRepo = $tagRepo;
+ $this->imageRepo = $imageRepo;
+ }
+
+ /**
+ * Get all books in a paginated format.
+ */
+ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+ {
+ return Book::visible()->orderBy($sort, $order)->paginate($count);
+ }
+
+ /**
+ * Get the books that were most recently viewed by this user.
+ */
+ public function getRecentlyViewed(int $count = 20): Collection
+ {
+ return Book::visible()->withLastView()
+ ->having('last_viewed_at', '>', 0)
+ ->orderBy('last_viewed_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most popular books in the system.
+ */
+ public function getPopular(int $count = 20): Collection
+ {
+ return Book::visible()->withViewCount()
+ ->having('view_count', '>', 0)
+ ->orderBy('view_count', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most recently created books from the system.
+ */
+ public function getRecentlyCreated(int $count = 20): Collection
+ {
+ return Book::visible()->orderBy('created_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get a book by its slug.
+ */
+ public function getBySlug(string $slug): Book
+ {
+ $book = Book::visible()->where('slug', '=', $slug)->first();
+
+ if ($book === null) {
+ throw new NotFoundException(trans('errors.book_not_found'));
+ }
+
+ return $book;
+ }
+
+ /**
+ * Create a new book in the system
+ */
+ public function create(array $input): Book
+ {
+ $book = new Book();
+ $this->baseRepo->create($book, $input);
+ return $book;
+ }
+
+ /**
+ * Update the given book.
+ */
+ public function update(Book $book, array $input): Book
+ {
+ $this->baseRepo->update($book, $input);
+ return $book;
+ }
+
+ /**
+ * Update the given book's cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws Exception
+ */
+ public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ $this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
+ }
+
+ /**
+ * Update the permissions of a book.
+ */
+ public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
+ {
+ $this->baseRepo->updatePermissions($book, $restricted, $permissions);
+ }
+
+ /**
+ * Remove a book from the system.
+ * @throws NotifyException
+ * @throws BindingResolutionException
+ */
+ public function destroy(Book $book)
+ {
+ $trashCan = new TrashCan();
+ $trashCan->destroyBook($book);
+ }
+}
diff --git a/app/Entities/Repos/BookshelfRepo.php b/app/Entities/Repos/BookshelfRepo.php
new file mode 100644
index 000000000..ab4a51805
--- /dev/null
+++ b/app/Entities/Repos/BookshelfRepo.php
@@ -0,0 +1,173 @@
+baseRepo = $baseRepo;
+ }
+
+ /**
+ * Get all bookshelves in a paginated format.
+ */
+ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+ {
+ return Bookshelf::visible()->with('visibleBooks')
+ ->orderBy($sort, $order)->paginate($count);
+ }
+
+ /**
+ * Get the bookshelves that were most recently viewed by this user.
+ */
+ public function getRecentlyViewed(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->withLastView()
+ ->having('last_viewed_at', '>', 0)
+ ->orderBy('last_viewed_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most popular bookshelves in the system.
+ */
+ public function getPopular(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->withViewCount()
+ ->having('view_count', '>', 0)
+ ->orderBy('view_count', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most recently created bookshelves from the system.
+ */
+ public function getRecentlyCreated(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->orderBy('created_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get a shelf by its slug.
+ */
+ public function getBySlug(string $slug): Bookshelf
+ {
+ $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
+
+ if ($shelf === null) {
+ throw new NotFoundException(trans('errors.bookshelf_not_found'));
+ }
+
+ return $shelf;
+ }
+
+ /**
+ * Create a new shelf in the system.
+ */
+ public function create(array $input, array $bookIds): Bookshelf
+ {
+ $shelf = new Bookshelf();
+ $this->baseRepo->create($shelf, $input);
+ $this->updateBooks($shelf, $bookIds);
+ return $shelf;
+ }
+
+ /**
+ * Create a new shelf in the system.
+ */
+ public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
+ {
+ $this->baseRepo->update($shelf, $input);
+ $this->updateBooks($shelf, $bookIds);
+ return $shelf;
+ }
+
+ /**
+ * Update which books are assigned to this shelf by
+ * syncing the given book ids.
+ * Function ensures the books are visible to the current user and existing.
+ */
+ protected function updateBooks(Bookshelf $shelf, array $bookIds)
+ {
+ $numericIDs = collect($bookIds)->map(function ($id) {
+ return intval($id);
+ });
+
+ $syncData = Book::visible()
+ ->whereIn('id', $bookIds)
+ ->get(['id'])->pluck('id')->mapWithKeys(function ($bookId) use ($numericIDs) {
+ return [$bookId => ['order' => $numericIDs->search($bookId)]];
+ });
+
+ $shelf->books()->sync($syncData);
+ }
+
+ /**
+ * Update the given shelf cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws Exception
+ */
+ public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ $this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
+ }
+
+ /**
+ * Update the permissions of a bookshelf.
+ */
+ public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
+ {
+ $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
+ }
+
+ /**
+ * Copy down the permissions of the given shelf to all child books.
+ */
+ public function copyDownPermissions(Bookshelf $shelf): int
+ {
+ $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
+ $shelfBooks = $shelf->books()->get();
+ $updatedBookCount = 0;
+
+ /** @var Book $book */
+ foreach ($shelfBooks as $book) {
+ if (!userCan('restrictions-manage', $book)) {
+ continue;
+ }
+ $book->permissions()->delete();
+ $book->restricted = $shelf->restricted;
+ $book->permissions()->createMany($shelfPermissions);
+ $book->save();
+ $book->rebuildPermissions();
+ $updatedBookCount++;
+ }
+
+ return $updatedBookCount;
+ }
+
+ /**
+ * Remove a bookshelf from the system.
+ * @throws Exception
+ */
+ public function destroy(Bookshelf $shelf)
+ {
+ $trashCan = new TrashCan();
+ $trashCan->destroyShelf($shelf);
+ }
+}
diff --git a/app/Entities/Repos/ChapterRepo.php b/app/Entities/Repos/ChapterRepo.php
new file mode 100644
index 000000000..c6f3a2d2f
--- /dev/null
+++ b/app/Entities/Repos/ChapterRepo.php
@@ -0,0 +1,108 @@
+baseRepo = $baseRepo;
+ }
+
+ /**
+ * Get a chapter via the slug.
+ * @throws NotFoundException
+ */
+ public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
+ {
+ $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
+
+ if ($chapter === null) {
+ throw new NotFoundException(trans('errors.chapter_not_found'));
+ }
+
+ return $chapter;
+ }
+
+ /**
+ * Create a new chapter in the system.
+ */
+ public function create(array $input, Book $parentBook): Chapter
+ {
+ $chapter = new Chapter();
+ $chapter->book_id = $parentBook->id;
+ $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
+ $this->baseRepo->create($chapter, $input);
+ return $chapter;
+ }
+
+ /**
+ * Update the given chapter.
+ */
+ public function update(Chapter $chapter, array $input): Chapter
+ {
+ $this->baseRepo->update($chapter, $input);
+ return $chapter;
+ }
+
+ /**
+ * Update the permissions of a chapter.
+ */
+ public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
+ {
+ $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
+ }
+
+ /**
+ * Remove a chapter from the system.
+ * @throws Exception
+ */
+ public function destroy(Chapter $chapter)
+ {
+ $trashCan = new TrashCan();
+ $trashCan->destroyChapter($chapter);
+ }
+
+ /**
+ * Move the given chapter into a new parent book.
+ * The $parentIdentifier must be a string of the following format:
+ * 'book:' (book:5)
+ * @throws MoveOperationException
+ */
+ public function move(Chapter $chapter, string $parentIdentifier): Book
+ {
+ $stringExploded = explode(':', $parentIdentifier);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
+
+ if ($entityType !== 'book') {
+ throw new MoveOperationException('Chapters can only be moved into books');
+ }
+
+ $parent = Book::visible()->where('id', '=', $entityId)->first();
+ if ($parent === null) {
+ throw new MoveOperationException('Book to move chapter into not found');
+ }
+
+ $chapter->changeBook($parent->id);
+ $chapter->rebuildPermissions();
+ return $parent;
+ }
+}
diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php
deleted file mode 100644
index aad9a1205..000000000
--- a/app/Entities/Repos/EntityRepo.php
+++ /dev/null
@@ -1,918 +0,0 @@
-entityProvider = $entityProvider;
- $this->viewService = $viewService;
- $this->permissionService = $permissionService;
- $this->tagRepo = $tagRepo;
- $this->searchService = $searchService;
- }
-
- /**
- * Base query for searching entities via permission system
- * @param string $type
- * @param bool $allowDrafts
- * @param string $permission
- * @return \Illuminate\Database\Query\Builder
- */
- protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
- {
- $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
- if (strtolower($type) === 'page' && !$allowDrafts) {
- $q = $q->where('draft', '=', false);
- }
- return $q;
- }
-
- /**
- * Check if an entity with the given id exists.
- * @param $type
- * @param $id
- * @return bool
- */
- public function exists($type, $id)
- {
- return $this->entityQuery($type)->where('id', '=', $id)->exists();
- }
-
- /**
- * Get an entity by ID
- * @param string $type
- * @param integer $id
- * @param bool $allowDrafts
- * @param bool $ignorePermissions
- * @return Entity
- */
- public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
- {
- $query = $this->entityQuery($type, $allowDrafts);
-
- if ($ignorePermissions) {
- $query = $this->entityProvider->get($type)->newQuery();
- }
-
- return $query->find($id);
- }
-
- /**
- * @param string $type
- * @param []int $ids
- * @param bool $allowDrafts
- * @param bool $ignorePermissions
- * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
- */
- public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
- {
- $query = $this->entityQuery($type, $allowDrafts);
-
- if ($ignorePermissions) {
- $query = $this->entityProvider->get($type)->newQuery();
- }
-
- return $query->whereIn('id', $ids)->get();
- }
-
- /**
- * Get an entity by its url slug.
- * @param string $type
- * @param string $slug
- * @param string|bool $bookSlug
- * @return Entity
- * @throws NotFoundException
- */
- public function getBySlug($type, $slug, $bookSlug = false)
- {
- $q = $this->entityQuery($type)->where('slug', '=', $slug);
-
- if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
- $q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
- $query->select('id')
- ->from($this->entityProvider->book->getTable())
- ->where('slug', '=', $bookSlug)->limit(1);
- });
- }
- $entity = $q->first();
- if ($entity === null) {
- throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
- }
- return $entity;
- }
-
-
- /**
- * Get all entities of a type with the given permission, limited by count unless count is false.
- * @param string $type
- * @param integer|bool $count
- * @param string $permission
- * @return Collection
- */
- public function getAll($type, $count = 20, $permission = 'view')
- {
- $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
- if ($count !== false) {
- $q = $q->take($count);
- }
- return $q->get();
- }
-
- /**
- * Get all entities in a paginated format
- * @param $type
- * @param int $count
- * @param string $sort
- * @param string $order
- * @param null|callable $queryAddition
- * @return LengthAwarePaginator
- */
- public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
- {
- $query = $this->entityQuery($type);
- $query = $this->addSortToQuery($query, $sort, $order);
- if ($queryAddition) {
- $queryAddition($query);
- }
- return $query->paginate($count);
- }
-
- /**
- * Add sorting operations to an entity query.
- * @param Builder $query
- * @param string $sort
- * @param string $order
- * @return Builder
- */
- protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
- {
- $order = ($order === 'asc') ? 'asc' : 'desc';
- $propertySorts = ['name', 'created_at', 'updated_at'];
-
- if (in_array($sort, $propertySorts)) {
- return $query->orderBy($sort, $order);
- }
-
- return $query;
- }
-
- /**
- * Get the most recently created entities of the given type.
- * @param string $type
- * @param int $count
- * @param int $page
- * @param bool|callable $additionalQuery
- * @return Collection
- */
- public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
- {
- $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
- ->orderBy('created_at', 'desc');
- if (strtolower($type) === 'page') {
- $query = $query->where('draft', '=', false);
- }
- if ($additionalQuery !== false && is_callable($additionalQuery)) {
- $additionalQuery($query);
- }
- return $query->skip($page * $count)->take($count)->get();
- }
-
- /**
- * Get the most recently updated entities of the given type.
- * @param string $type
- * @param int $count
- * @param int $page
- * @param bool|callable $additionalQuery
- * @return Collection
- */
- public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
- {
- $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
- ->orderBy('updated_at', 'desc');
- if (strtolower($type) === 'page') {
- $query = $query->where('draft', '=', false);
- }
- if ($additionalQuery !== false && is_callable($additionalQuery)) {
- $additionalQuery($query);
- }
- return $query->skip($page * $count)->take($count)->get();
- }
-
- /**
- * Get the most recently viewed entities.
- * @param string|bool $type
- * @param int $count
- * @param int $page
- * @return mixed
- */
- public function getRecentlyViewed($type, $count = 10, $page = 0)
- {
- $filter = is_bool($type) ? false : $this->entityProvider->get($type);
- return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
- }
-
- /**
- * Get the latest pages added to the system with pagination.
- * @param string $type
- * @param int $count
- * @return mixed
- */
- public function getRecentlyCreatedPaginated($type, $count = 20)
- {
- return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
- }
-
- /**
- * Get the latest pages added to the system with pagination.
- * @param string $type
- * @param int $count
- * @return mixed
- */
- public function getRecentlyUpdatedPaginated($type, $count = 20)
- {
- return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
- }
-
- /**
- * Get the most popular entities base on all views.
- * @param string $type
- * @param int $count
- * @param int $page
- * @return mixed
- */
- public function getPopular(string $type, int $count = 10, int $page = 0)
- {
- return $this->viewService->getPopular($count, $page, $type);
- }
-
- /**
- * Get draft pages owned by the current user.
- * @param int $count
- * @param int $page
- * @return Collection
- */
- public function getUserDraftPages($count = 20, $page = 0)
- {
- return $this->entityProvider->page->where('draft', '=', true)
- ->where('created_by', '=', user()->id)
- ->orderBy('updated_at', 'desc')
- ->skip($count * $page)->take($count)->get();
- }
-
- /**
- * Get the number of entities the given user has created.
- * @param string $type
- * @param User $user
- * @return int
- */
- public function getUserTotalCreated(string $type, User $user)
- {
- return $this->entityProvider->get($type)
- ->where('created_by', '=', $user->id)->count();
- }
-
- /**
- * Get the child items for a chapter sorted by priority but
- * with draft items floated to the top.
- * @param Bookshelf $bookshelf
- * @return \Illuminate\Database\Eloquent\Collection|static[]
- */
- public function getBookshelfChildren(Bookshelf $bookshelf)
- {
- return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
- }
-
- /**
- * Get the direct children of a book.
- * @param Book $book
- * @return \Illuminate\Database\Eloquent\Collection
- */
- public function getBookDirectChildren(Book $book)
- {
- $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
- $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
- return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
- }
-
- /**
- * Get all child objects of a book.
- * Returns a sorted collection of Pages and Chapters.
- * Loads the book slug onto child elements to prevent access database access for getting the slug.
- * @param Book $book
- * @param bool $filterDrafts
- * @param bool $renderPages
- * @return mixed
- */
- public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
- {
- $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
- $entities = [];
- $parents = [];
- $tree = [];
-
- foreach ($q as $index => $rawEntity) {
- if ($rawEntity->entity_type === $this->entityProvider->page->getMorphClass()) {
- $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
- if ($renderPages) {
- $entities[$index]->html = $rawEntity->html;
- $entities[$index]->html = $this->renderPage($entities[$index]);
- };
- } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
- $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
- $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
- $parents[$key] = $entities[$index];
- $parents[$key]->setAttribute('pages', collect());
- }
- if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
- $tree[] = $entities[$index];
- }
- $entities[$index]->book = $book;
- }
-
- foreach ($entities as $entity) {
- if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
- continue;
- }
- $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
- if (!isset($parents[$parentKey])) {
- $tree[] = $entity;
- continue;
- }
- $chapter = $parents[$parentKey];
- $chapter->pages->push($entity);
- }
-
- return collect($tree);
- }
-
- /**
- * Get the child items for a chapter sorted by priority but
- * with draft items floated to the top.
- * @param Chapter $chapter
- * @return \Illuminate\Database\Eloquent\Collection|static[]
- */
- public function getChapterChildren(Chapter $chapter)
- {
- return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
- ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
- }
-
-
- /**
- * Get the next sequential priority for a new child element in the given book.
- * @param Book $book
- * @return int
- */
- public function getNewBookPriority(Book $book)
- {
- $lastElem = $this->getBookChildren($book)->pop();
- return $lastElem ? $lastElem->priority + 1 : 0;
- }
-
- /**
- * Get a new priority for a new page to be added to the given chapter.
- * @param Chapter $chapter
- * @return int
- */
- public function getNewChapterPriority(Chapter $chapter)
- {
- $lastPage = $chapter->pages('DESC')->first();
- return $lastPage !== null ? $lastPage->priority + 1 : 0;
- }
-
- /**
- * Find a suitable slug for an entity.
- * @param string $type
- * @param string $name
- * @param bool|integer $currentId
- * @param bool|integer $bookId Only pass if type is not a book
- * @return string
- */
- public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
- {
- $slug = $this->nameToSlug($name);
- while ($this->slugExists($type, $slug, $currentId, $bookId)) {
- $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
- }
- return $slug;
- }
-
- /**
- * Check if a slug already exists in the database.
- * @param string $type
- * @param string $slug
- * @param bool|integer $currentId
- * @param bool|integer $bookId
- * @return bool
- */
- protected function slugExists($type, $slug, $currentId = false, $bookId = false)
- {
- $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
- if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
- $query = $query->where('book_id', '=', $bookId);
- }
- if ($currentId) {
- $query = $query->where('id', '!=', $currentId);
- }
- return $query->count() > 0;
- }
-
- /**
- * Updates entity restrictions from a request
- * @param Request $request
- * @param Entity $entity
- * @throws Throwable
- */
- public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
- {
- $entity->restricted = $request->get('restricted', '') === 'true';
- $entity->permissions()->delete();
-
- if ($request->filled('restrictions')) {
- foreach ($request->get('restrictions') as $roleId => $restrictions) {
- foreach ($restrictions as $action => $value) {
- $entity->permissions()->create([
- 'role_id' => $roleId,
- 'action' => strtolower($action)
- ]);
- }
- }
- }
-
- $entity->save();
- $this->permissionService->buildJointPermissionsForEntity($entity);
- }
-
-
-
- /**
- * Create a new entity from request input.
- * Used for books and chapters.
- * @param string $type
- * @param array $input
- * @param bool|Book $book
- * @return Entity
- */
- public function createFromInput($type, $input = [], $book = false)
- {
- $isChapter = strtolower($type) === 'chapter';
- $entityModel = $this->entityProvider->get($type)->newInstance($input);
- $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
- $entityModel->created_by = user()->id;
- $entityModel->updated_by = user()->id;
- $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $this->searchService->indexEntity($entityModel);
- return $entityModel;
- }
-
- /**
- * Update entity details from request input.
- * Used for books and chapters
- * @param string $type
- * @param Entity $entityModel
- * @param array $input
- * @return Entity
- */
- public function updateFromInput($type, Entity $entityModel, $input = [])
- {
- if ($entityModel->name !== $input['name']) {
- $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
- }
- $entityModel->fill($input);
- $entityModel->updated_by = user()->id;
- $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $this->searchService->indexEntity($entityModel);
- return $entityModel;
- }
-
- /**
- * Sync the books assigned to a shelf from a comma-separated list
- * of book IDs.
- * @param Bookshelf $shelf
- * @param string $books
- */
- public function updateShelfBooks(Bookshelf $shelf, string $books)
- {
- $ids = explode(',', $books);
-
- // Check books exist and match ordering
- $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
- $syncData = [];
- foreach ($ids as $index => $id) {
- if ($bookIds->contains($id)) {
- $syncData[$id] = ['order' => $index];
- }
- }
-
- $shelf->books()->sync($syncData);
- }
-
- /**
- * Append a Book to a BookShelf.
- * @param Bookshelf $shelf
- * @param Book $book
- */
- public function appendBookToShelf(Bookshelf $shelf, Book $book)
- {
- if ($shelf->contains($book)) {
- return;
- }
-
- $maxOrder = $shelf->books()->max('order');
- $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
- }
-
- /**
- * Change the book that an entity belongs to.
- * @param string $type
- * @param integer $newBookId
- * @param Entity $entity
- * @param bool $rebuildPermissions
- * @return Entity
- */
- public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
- {
- $entity->book_id = $newBookId;
- // Update related activity
- foreach ($entity->activity as $activity) {
- $activity->book_id = $newBookId;
- $activity->save();
- }
- $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
- $entity->save();
-
- // Update all child pages if a chapter
- if (strtolower($type) === 'chapter') {
- foreach ($entity->pages as $page) {
- $this->changeBook('page', $newBookId, $page, false);
- }
- }
-
- // Update permissions if applicable
- if ($rebuildPermissions) {
- $entity->load('book');
- $this->permissionService->buildJointPermissionsForEntity($entity->book);
- }
-
- return $entity;
- }
-
- /**
- * Alias method to update the book jointPermissions in the PermissionService.
- * @param Book $book
- */
- public function buildJointPermissionsForBook(Book $book)
- {
- $this->permissionService->buildJointPermissionsForEntity($book);
- }
-
- /**
- * Format a name as a url slug.
- * @param $name
- * @return string
- */
- protected function nameToSlug($name)
- {
- $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
- $slug = preg_replace('/\s{2,}/', ' ', $slug);
- $slug = str_replace(' ', '-', $slug);
- if ($slug === "") {
- $slug = substr(md5(rand(1, 500)), 0, 5);
- }
- return $slug;
- }
-
- /**
- * Render the page for viewing
- * @param Page $page
- * @param bool $blankIncludes
- * @return string
- */
- public function renderPage(Page $page, bool $blankIncludes = false) : string
- {
- $content = $page->html;
-
- if (!config('app.allow_content_scripts')) {
- $content = $this->escapeScripts($content);
- }
-
- if ($blankIncludes) {
- $content = $this->blankPageIncludes($content);
- } else {
- $content = $this->parsePageIncludes($content);
- }
-
- return $content;
- }
-
- /**
- * Remove any page include tags within the given HTML.
- * @param string $html
- * @return string
- */
- protected function blankPageIncludes(string $html) : string
- {
- return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
- }
-
- /**
- * Parse any include tags "{{@#section}}" to be part of the page.
- * @param string $html
- * @return mixed|string
- */
- protected function parsePageIncludes(string $html) : string
- {
- $matches = [];
- preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
-
- $topLevelTags = ['table', 'ul', 'ol'];
- foreach ($matches[1] as $index => $includeId) {
- $splitInclude = explode('#', $includeId, 2);
- $pageId = intval($splitInclude[0]);
- if (is_nan($pageId)) {
- continue;
- }
-
- $matchedPage = $this->getById('page', $pageId);
- if ($matchedPage === null) {
- $html = str_replace($matches[0][$index], '', $html);
- continue;
- }
-
- if (count($splitInclude) === 1) {
- $html = str_replace($matches[0][$index], $matchedPage->html, $html);
- continue;
- }
-
- $doc = new DOMDocument();
- libxml_use_internal_errors(true);
- $doc->loadHTML(mb_convert_encoding(''.$matchedPage->html.'', 'HTML-ENTITIES', 'UTF-8'));
- $matchingElem = $doc->getElementById($splitInclude[1]);
- if ($matchingElem === null) {
- $html = str_replace($matches[0][$index], '', $html);
- continue;
- }
- $innerContent = '';
- $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
- if ($isTopLevel) {
- $innerContent .= $doc->saveHTML($matchingElem);
- } else {
- foreach ($matchingElem->childNodes as $childNode) {
- $innerContent .= $doc->saveHTML($childNode);
- }
- }
- libxml_clear_errors();
- $html = str_replace($matches[0][$index], trim($innerContent), $html);
- }
-
- return $html;
- }
-
- /**
- * Escape script tags within HTML content.
- * @param string $html
- * @return string
- */
- protected function escapeScripts(string $html) : string
- {
- if ($html == '') {
- return $html;
- }
-
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
- $xPath = new DOMXPath($doc);
-
- // Remove standard script tags
- $scriptElems = $xPath->query('//script');
- foreach ($scriptElems as $scriptElem) {
- $scriptElem->parentNode->removeChild($scriptElem);
- }
-
- // Remove 'on*' attributes
- $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
- foreach ($onAttributes as $attr) {
- /** @var \DOMAttr $attr*/
- $attrName = $attr->nodeName;
- $attr->parentNode->removeAttribute($attrName);
- }
-
- $html = '';
- $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
- foreach ($topElems as $child) {
- $html .= $doc->saveHTML($child);
- }
-
- return $html;
- }
-
- /**
- * Search for image usage within page content.
- * @param $imageString
- * @return mixed
- */
- public function searchForImage($imageString)
- {
- $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
- foreach ($pages as $page) {
- $page->url = $page->getUrl();
- $page->html = '';
- $page->text = '';
- }
- return count($pages) > 0 ? $pages : false;
- }
-
- /**
- * Destroy a bookshelf instance
- * @param Bookshelf $shelf
- * @throws Throwable
- */
- public function destroyBookshelf(Bookshelf $shelf)
- {
- $this->destroyEntityCommonRelations($shelf);
- $shelf->delete();
- }
-
- /**
- * Destroy the provided book and all its child entities.
- * @param Book $book
- * @throws NotifyException
- * @throws Throwable
- */
- public function destroyBook(Book $book)
- {
- foreach ($book->pages as $page) {
- $this->destroyPage($page);
- }
- foreach ($book->chapters as $chapter) {
- $this->destroyChapter($chapter);
- }
- $this->destroyEntityCommonRelations($book);
- $book->delete();
- }
-
- /**
- * Destroy a chapter and its relations.
- * @param Chapter $chapter
- * @throws Throwable
- */
- public function destroyChapter(Chapter $chapter)
- {
- if (count($chapter->pages) > 0) {
- foreach ($chapter->pages as $page) {
- $page->chapter_id = 0;
- $page->save();
- }
- }
- $this->destroyEntityCommonRelations($chapter);
- $chapter->delete();
- }
-
- /**
- * Destroy a given page along with its dependencies.
- * @param Page $page
- * @throws NotifyException
- * @throws Throwable
- */
- public function destroyPage(Page $page)
- {
- // Check if set as custom homepage & remove setting if not used or throw error if active
- $customHome = setting('app-homepage', '0:');
- if (intval($page->id) === intval(explode(':', $customHome)[0])) {
- if (setting('app-homepage-type') === 'page') {
- throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
- }
- setting()->remove('app-homepage');
- }
-
- $this->destroyEntityCommonRelations($page);
-
- // Delete Attached Files
- $attachmentService = app(AttachmentService::class);
- foreach ($page->attachments as $attachment) {
- $attachmentService->deleteFile($attachment);
- }
-
- $page->delete();
- }
-
- /**
- * Destroy or handle the common relations connected to an entity.
- * @param Entity $entity
- * @throws Throwable
- */
- protected function destroyEntityCommonRelations(Entity $entity)
- {
- Activity::removeEntity($entity);
- $entity->views()->delete();
- $entity->permissions()->delete();
- $entity->tags()->delete();
- $entity->comments()->delete();
- $this->permissionService->deleteJointPermissionsForEntity($entity);
- $this->searchService->deleteEntityTerms($entity);
- }
-
- /**
- * Copy the permissions of a bookshelf to all child books.
- * Returns the number of books that had permissions updated.
- * @param Bookshelf $bookshelf
- * @return int
- * @throws Throwable
- */
- public function copyBookshelfPermissions(Bookshelf $bookshelf)
- {
- $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
- $shelfBooks = $bookshelf->books()->get();
- $updatedBookCount = 0;
-
- foreach ($shelfBooks as $book) {
- if (!userCan('restrictions-manage', $book)) {
- continue;
- }
- $book->permissions()->delete();
- $book->restricted = $bookshelf->restricted;
- $book->permissions()->createMany($shelfPermissions);
- $book->save();
- $this->permissionService->buildJointPermissionsForEntity($book);
- $updatedBookCount++;
- }
-
- return $updatedBookCount;
- }
-}
diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php
index 6b004984f..e49eeb1ef 100644
--- a/app/Entities/Repos/PageRepo.php
+++ b/app/Entities/Repos/PageRepo.php
@@ -3,278 +3,253 @@
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\TrashCan;
use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
-use Carbon\Carbon;
-use DOMDocument;
-use DOMElement;
-use DOMXPath;
-
-class PageRepo extends EntityRepo
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Collection;
+
+class PageRepo
{
+ protected $baseRepo;
+
/**
- * Get page by slug.
- * @param string $pageSlug
- * @param string $bookSlug
- * @return Page
- * @throws \BookStack\Exceptions\NotFoundException
+ * PageRepo constructor.
*/
- public function getPageBySlug(string $pageSlug, string $bookSlug)
+ public function __construct(BaseRepo $baseRepo)
{
- return $this->getBySlug('page', $pageSlug, $bookSlug);
+ $this->baseRepo = $baseRepo;
}
/**
- * Search through page revisions and retrieve the last page in the
- * current book that has a slug equal to the one given.
- * @param string $pageSlug
- * @param string $bookSlug
- * @return null|Page
+ * Get a page by ID.
+ * @throws NotFoundException
*/
- public function getPageByOldSlug(string $pageSlug, string $bookSlug)
+ public function getById(int $id): Page
{
- $revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
- ->whereHas('page', function ($query) {
- $this->permissionService->enforceEntityRestrictions('page', $query);
- })
- ->where('type', '=', 'version')
- ->where('book_slug', '=', $bookSlug)
- ->orderBy('created_at', 'desc')
- ->with('page')->first();
- return $revision !== null ? $revision->page : null;
+ $page = Page::visible()->with(['book'])->find($id);
+
+ if (!$page) {
+ throw new NotFoundException(trans('errors.page_not_found'));
+ }
+
+ return $page;
}
/**
- * Updates a page with any fillable data and saves it into the database.
- * @param Page $page
- * @param int $book_id
- * @param array $input
- * @return Page
- * @throws \Exception
+ * Get a page its book and own slug.
+ * @throws NotFoundException
*/
- public function updatePage(Page $page, int $book_id, array $input)
+ public function getBySlug(string $bookSlug, string $pageSlug): Page
{
- // Hold the old details to compare later
- $oldHtml = $page->html;
- $oldName = $page->name;
+ $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
- // Prevent slug being updated if no name change
- if ($page->name !== $input['name']) {
- $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+ if (!$page) {
+ throw new NotFoundException(trans('errors.page_not_found'));
}
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($page, $input['tags']);
- }
-
- // Update with new details
- $userId = user()->id;
- $page->fill($input);
- $page->html = $this->formatHtml($input['html']);
- $page->text = $this->pageToPlainText($page);
- if (setting('app-editor') !== 'markdown') {
- $page->markdown = '';
- }
- $page->updated_by = $userId;
- $page->revision_count++;
- $page->save();
+ return $page;
+ }
- // Remove all update drafts for this user & page.
- $this->userUpdatePageDraftsQuery($page, $userId)->delete();
+ /**
+ * Get a page by its old slug but checking the revisions table
+ * for the last revision that matched the given page and book slug.
+ */
+ public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
+ {
+ $revision = PageRevision::query()
+ ->whereHas('page', function (Builder $query) {
+ $query->visible();
+ })
+ ->where('slug', '=', $pageSlug)
+ ->where('type', '=', 'version')
+ ->where('book_slug', '=', $bookSlug)
+ ->orderBy('created_at', 'desc')
+ ->with('page')
+ ->first();
+ return $revision ? $revision->page : null;
+ }
- // Save a revision after updating
- if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
- $this->savePageRevision($page, $input['summary']);
+ /**
+ * Get pages that have been marked as a template.
+ */
+ public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
+ {
+ $query = Page::visible()
+ ->where('template', '=', true)
+ ->orderBy('name', 'asc')
+ ->skip(($page - 1) * $count)
+ ->take($count);
+
+ if ($search) {
+ $query->where('name', 'like', '%' . $search . '%');
}
- $this->searchService->indexEntity($page);
+ $paginator = $query->paginate($count, ['*'], 'page', $page);
+ $paginator->withPath('/templates');
- return $page;
+ return $paginator;
}
/**
- * Saves a page revision into the system.
- * @param Page $page
- * @param null|string $summary
- * @return PageRevision
- * @throws \Exception
+ * Get a parent item via slugs.
*/
- public function savePageRevision(Page $page, string $summary = null)
+ public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
{
- $revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
- if (setting('app-editor') !== 'markdown') {
- $revision->markdown = '';
+ if ($chapterSlug !== null) {
+ return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
}
- $revision->page_id = $page->id;
- $revision->slug = $page->slug;
- $revision->book_slug = $page->book->slug;
- $revision->created_by = user()->id;
- $revision->created_at = $page->updated_at;
- $revision->type = 'version';
- $revision->summary = $summary;
- $revision->revision_number = $page->revision_count;
- $revision->save();
- $revisionLimit = config('app.revision_limit');
- if ($revisionLimit !== false) {
- $revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
- ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
- if ($revisionsToDelete->count() > 0) {
- $this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
- }
- }
+ return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+ }
+ /**
+ * Get the draft copy of the given page for the current user.
+ */
+ public function getUserDraft(Page $page): ?PageRevision
+ {
+ $revision = $this->getUserDraftQuery($page)->first();
return $revision;
}
/**
- * Formats a page's html to be tagged correctly within the system.
- * @param string $htmlText
- * @return string
+ * Get a new draft page belonging to the given parent entity.
*/
- protected function formatHtml(string $htmlText)
+ public function getNewDraftPage(Entity $parent)
{
- if ($htmlText == '') {
- return $htmlText;
+ $page = (new Page())->forceFill([
+ 'name' => trans('entities.pages_initial_name'),
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ 'draft' => true,
+ ]);
+
+ if ($parent instanceof Chapter) {
+ $page->chapter_id = $parent->id;
+ $page->book_id = $parent->book_id;
+ } else {
+ $page->book_id = $parent->id;
}
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
-
- $container = $doc->documentElement;
- $body = $container->childNodes->item(0);
- $childNodes = $body->childNodes;
-
- // Set ids on top-level nodes
- $idMap = [];
- foreach ($childNodes as $index => $childNode) {
- $this->setUniqueId($childNode, $idMap);
- }
+ $page->save();
+ $page->refresh()->rebuildPermissions();
+ return $page;
+ }
- // Ensure no duplicate ids within child items
- $xPath = new DOMXPath($doc);
- $idElems = $xPath->query('//body//*//*[@id]');
- foreach ($idElems as $domElem) {
- $this->setUniqueId($domElem, $idMap);
+ /**
+ * Publish a draft page to make it a live, non-draft page.
+ */
+ public function publishDraft(Page $draft, array $input): Page
+ {
+ $this->baseRepo->update($draft, $input);
+ if (isset($input['template']) && userCan('templates-manage')) {
+ $draft->template = ($input['template'] === 'true');
}
- // Generate inner html as a string
- $html = '';
- foreach ($childNodes as $childNode) {
- $html .= $doc->saveHTML($childNode);
- }
+ $pageContent = new PageContent($draft);
+ $pageContent->setNewHTML($input['html']);
+ $draft->draft = false;
+ $draft->revision_count = 1;
+ $draft->priority = $this->getNewPriority($draft);
+ $draft->refreshSlug();
+ $draft->save();
- return $html;
+ $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
+ $draft->indexForSearch();
+ return $draft->refresh();
}
/**
- * Set a unique id on the given DOMElement.
- * A map for existing ID's should be passed in to check for current existence.
- * @param DOMElement $element
- * @param array $idMap
+ * Update a page in the system.
*/
- protected function setUniqueId($element, array &$idMap)
+ public function update(Page $page, array $input): Page
{
- if (get_class($element) !== 'DOMElement') {
- return;
- }
+ // Hold the old details to compare later
+ $oldHtml = $page->html;
+ $oldName = $page->name;
- // Overwrite id if not a BookStack custom id
- $existingId = $element->getAttribute('id');
- if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
- $idMap[$existingId] = true;
- return;
+ if (isset($input['template']) && userCan('templates-manage')) {
+ $page->template = ($input['template'] === 'true');
}
- // Create an unique id for the element
- // Uses the content as a basis to ensure output is the same every time
- // the same content is passed through.
- $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
- $newId = urlencode($contentId);
- $loopIndex = 0;
+ $this->baseRepo->update($page, $input);
- while (isset($idMap[$newId])) {
- $newId = urlencode($contentId . '-' . $loopIndex);
- $loopIndex++;
+ // Update with new details
+ $page->fill($input);
+ $pageContent = new PageContent($page);
+ $pageContent->setNewHTML($input['html']);
+ $page->revision_count++;
+
+ if (setting('app-editor') !== 'markdown') {
+ $page->markdown = '';
}
- $element->setAttribute('id', $newId);
- $idMap[$newId] = true;
- }
+ $page->save();
- /**
- * Get the plain text version of a page's content.
- * @param \BookStack\Entities\Page $page
- * @return string
- */
- protected function pageToPlainText(Page $page) : string
- {
- $html = $this->renderPage($page, true);
- return strip_tags($html);
+ // Remove all update drafts for this user & page.
+ $this->getUserDraftQuery($page)->delete();
+
+ // Save a revision after updating
+ $summary = $input['summary'] ?? null;
+ if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
+ $this->savePageRevision($page, $summary);
+ }
+
+ return $page;
}
/**
- * Get a new draft page instance.
- * @param Book $book
- * @param Chapter|null $chapter
- * @return \BookStack\Entities\Page
- * @throws \Throwable
+ * Saves a page revision into the system.
*/
- public function getDraftPage(Book $book, Chapter $chapter = null)
+ protected function savePageRevision(Page $page, string $summary = null)
{
- $page = $this->entityProvider->page->newInstance();
- $page->name = trans('entities.pages_initial_name');
- $page->created_by = user()->id;
- $page->updated_by = user()->id;
- $page->draft = true;
+ $revision = new PageRevision($page->toArray());
- if ($chapter) {
- $page->chapter_id = $chapter->id;
+ if (setting('app-editor') !== 'markdown') {
+ $revision->markdown = '';
}
- $book->pages()->save($page);
- $page = $this->entityProvider->page->find($page->id);
- $this->permissionService->buildJointPermissionsForEntity($page);
- return $page;
+ $revision->page_id = $page->id;
+ $revision->slug = $page->slug;
+ $revision->book_slug = $page->book->slug;
+ $revision->created_by = user()->id;
+ $revision->created_at = $page->updated_at;
+ $revision->type = 'version';
+ $revision->summary = $summary;
+ $revision->revision_number = $page->revision_count;
+ $revision->save();
+
+ $this->deleteOldRevisions($page);
+ return $revision;
}
/**
* Save a page update draft.
- * @param Page $page
- * @param array $data
- * @return PageRevision|Page
*/
- public function updatePageDraft(Page $page, array $data = [])
+ public function updatePageDraft(Page $page, array $input)
{
// If the page itself is a draft simply update that
if ($page->draft) {
- $page->fill($data);
- if (isset($data['html'])) {
- $page->text = $this->pageToPlainText($page);
+ $page->fill($input);
+ if (isset($input['html'])) {
+ $content = new PageContent($page);
+ $content->setNewHTML($input['html']);
}
$page->save();
return $page;
}
// Otherwise save the data to a revision
- $userId = user()->id;
- $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
-
- if ($drafts->count() > 0) {
- $draft = $drafts->first();
- } else {
- $draft = $this->entityProvider->pageRevision->newInstance();
- $draft->page_id = $page->id;
- $draft->slug = $page->slug;
- $draft->book_slug = $page->book->slug;
- $draft->created_by = $userId;
- $draft->type = 'update_draft';
- }
-
- $draft->fill($data);
+ $draft = $this->getPageRevisionToUpdate($page);
+ $draft->fill($input);
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
@@ -284,243 +259,203 @@ class PageRepo extends EntityRepo
}
/**
- * Publish a draft page to make it a normal page.
- * Sets the slug and updates the content.
- * @param Page $draftPage
- * @param array $input
- * @return Page
- * @throws \Exception
+ * Destroy a page from the system.
+ * @throws NotifyException
*/
- public function publishPageDraft(Page $draftPage, array $input)
+ public function destroy(Page $page)
{
- $draftPage->fill($input);
-
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
- }
-
- $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
- $draftPage->html = $this->formatHtml($input['html']);
- $draftPage->text = $this->pageToPlainText($draftPage);
- $draftPage->draft = false;
- $draftPage->revision_count = 1;
-
- $draftPage->save();
- $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
- $this->searchService->indexEntity($draftPage);
- return $draftPage;
+ $trashCan = new TrashCan();
+ $trashCan->destroyPage($page);
}
/**
- * The base query for getting user update drafts.
- * @param Page $page
- * @param $userId
- * @return mixed
+ * Restores a revision's content back into a page.
*/
- protected function userUpdatePageDraftsQuery(Page $page, int $userId)
+ public function restoreRevision(Page $page, int $revisionId): Page
{
- return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
- ->where('type', 'update_draft')
- ->where('page_id', '=', $page->id)
- ->orderBy('created_at', 'desc');
+ $page->revision_count++;
+ $this->savePageRevision($page);
+
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ $page->fill($revision->toArray());
+ $content = new PageContent($page);
+ $content->setNewHTML($page->html);
+ $page->updated_by = user()->id;
+ $page->refreshSlug();
+ $page->save();
+
+ $page->indexForSearch();
+ return $page;
}
/**
- * Get the latest updated draft revision for a particular page and user.
- * @param Page $page
- * @param $userId
- * @return PageRevision|null
+ * Move the given page into a new parent book or chapter.
+ * The $parentIdentifier must be a string of the following format:
+ * 'book:' (book:5)
+ * @throws MoveOperationException
+ * @throws PermissionsException
*/
- public function getUserPageDraft(Page $page, int $userId)
+ public function move(Page $page, string $parentIdentifier): Book
{
- return $this->userUpdatePageDraftsQuery($page, $userId)->first();
+ $parent = $this->findParentByIdentifier($parentIdentifier);
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
+ }
+
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
+ }
+
+ $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
+ $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
+ $page->rebuildPermissions();
+
+ return ($parent instanceof Book ? $parent : $parent->book);
}
/**
- * Get the notification message that informs the user that they are editing a draft page.
- * @param PageRevision $draft
- * @return string
+ * Copy an existing page in the system.
+ * Optionally providing a new parent via string identifier and a new name.
+ * @throws MoveOperationException
+ * @throws PermissionsException
*/
- public function getUserPageDraftMessage(PageRevision $draft)
+ public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
{
- $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
- if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
- return $message;
+ $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
}
- return $message . "\n" . trans('entities.pages_draft_edited_notification');
+
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
+ }
+
+ $copyPage = $this->getNewDraftPage($parent);
+ $pageData = $page->getAttributes();
+
+ // Update name
+ if (!empty($newName)) {
+ $pageData['name'] = $newName;
+ }
+
+ // Copy tags from previous page if set
+ if ($page->tags) {
+ $pageData['tags'] = [];
+ foreach ($page->tags as $tag) {
+ $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+ }
+ }
+
+ return $this->publishDraft($copyPage, $pageData);
}
/**
- * A query to check for active update drafts on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return mixed
+ * Find a page parent entity via a identifier string in the format:
+ * {type}:{id}
+ * Example: (book:5)
+ * @throws MoveOperationException
*/
- protected function activePageEditingQuery(Page $page, int $minRange = null)
+ protected function findParentByIdentifier(string $identifier): ?Entity
{
- $query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
- ->where('page_id', '=', $page->id)
- ->where('updated_at', '>', $page->updated_at)
- ->where('created_by', '!=', user()->id)
- ->with('createdBy');
+ $stringExploded = explode(':', $identifier);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
- if ($minRange !== null) {
- $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+ if ($entityType !== 'book' && $entityType !== 'chapter') {
+ throw new MoveOperationException('Pages can only be in books or chapters');
}
- return $query;
+ $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+ return $parentClass::visible()->where('id', '=', $entityId)->first();
}
/**
- * Check if a page is being actively editing.
- * Checks for edits since last page updated.
- * Passing in a minuted range will check for edits
- * within the last x minutes.
- * @param Page $page
- * @param int $minRange
- * @return bool
+ * Update the permissions of a page.
*/
- public function isPageEditingActive(Page $page, int $minRange = null)
+ public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
{
- $draftSearch = $this->activePageEditingQuery($page, $minRange);
- return $draftSearch->count() > 0;
+ $this->baseRepo->updatePermissions($page, $restricted, $permissions);
}
/**
- * Get a notification message concerning the editing activity on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return string
+ * Change the page's parent to the given entity.
*/
- public function getPageEditingActiveMessage(Page $page, int $minRange = null)
+ protected function changeParent(Page $page, Entity $parent)
{
- $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+ $book = ($parent instanceof Book) ? $parent : $parent->book;
+ $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
+ $page->save();
- $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
- $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
- return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+ if ($page->book->id !== $book->id) {
+ $page->changeBook($book->id);
+ }
+
+ $page->load('book');
+ $book->rebuildPermissions();
}
/**
- * Parse the headers on the page to get a navigation menu
- * @param string $pageContent
- * @return array
+ * Get a page revision to update for the given page.
+ * Checks for an existing revisions before providing a fresh one.
*/
- public function getPageNav(string $pageContent)
+ protected function getPageRevisionToUpdate(Page $page): PageRevision
{
- if ($pageContent == '') {
- return [];
- }
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
- $xPath = new DOMXPath($doc);
- $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
-
- if (is_null($headers)) {
- return [];
+ $drafts = $this->getUserDraftQuery($page)->get();
+ if ($drafts->count() > 0) {
+ return $drafts->first();
}
- $tree = collect($headers)->map(function($header) {
- $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
- $text = mb_substr($text, 0, 100);
-
- return [
- 'nodeName' => strtolower($header->nodeName),
- 'level' => intval(str_replace('h', '', $header->nodeName)),
- 'link' => '#' . $header->getAttribute('id'),
- 'text' => $text,
- ];
- })->filter(function($header) {
- return mb_strlen($header['text']) > 0;
- });
-
- // Shift headers if only smaller headers have been used
- $levelChange = ($tree->pluck('level')->min() - 1);
- $tree = $tree->map(function ($header) use ($levelChange) {
- $header['level'] -= ($levelChange);
- return $header;
- });
-
- return $tree->toArray();
+ $draft = new PageRevision();
+ $draft->page_id = $page->id;
+ $draft->slug = $page->slug;
+ $draft->book_slug = $page->book->slug;
+ $draft->created_by = user()->id;
+ $draft->type = 'update_draft';
+ return $draft;
}
/**
- * Restores a revision's content back into a page.
- * @param Page $page
- * @param Book $book
- * @param int $revisionId
- * @return Page
- * @throws \Exception
+ * Delete old revisions, for the given page, from the system.
*/
- public function restorePageRevision(Page $page, Book $book, int $revisionId)
+ protected function deleteOldRevisions(Page $page)
{
- $page->revision_count++;
- $this->savePageRevision($page);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- $page->fill($revision->toArray());
- $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
- $page->text = $this->pageToPlainText($page);
- $page->updated_by = user()->id;
- $page->save();
- $this->searchService->indexEntity($page);
- return $page;
+ $revisionLimit = config('app.revision_limit');
+ if ($revisionLimit === false) {
+ return;
+ }
+
+ $revisionsToDelete = PageRevision::query()
+ ->where('page_id', '=', $page->id)
+ ->orderBy('created_at', 'desc')
+ ->skip(intval($revisionLimit))
+ ->take(10)
+ ->get(['id']);
+ if ($revisionsToDelete->count() > 0) {
+ PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
+ }
}
/**
- * Change the page's parent to the given entity.
- * @param Page $page
- * @param Entity $parent
- * @throws \Throwable
+ * Get a new priority for a page
*/
- public function changePageParent(Page $page, Entity $parent)
+ protected function getNewPriority(Page $page): int
{
- $book = $parent->isA('book') ? $parent : $parent->book;
- $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
- $page->save();
- if ($page->book->id !== $book->id) {
- $page = $this->changeBook('page', $book->id, $page);
+ if ($page->parent() instanceof Chapter) {
+ $lastPage = $page->parent()->pages('desc')->first();
+ return $lastPage ? $lastPage->priority + 1 : 0;
}
- $page->load('book');
- $this->permissionService->buildJointPermissionsForEntity($book);
+
+ return (new BookContents($page->book))->getLastPriority() + 1;
}
/**
- * Create a copy of a page in a new location with a new name.
- * @param \BookStack\Entities\Page $page
- * @param \BookStack\Entities\Entity $newParent
- * @param string $newName
- * @return \BookStack\Entities\Page
- * @throws \Throwable
+ * Get the query to find the user's draft copies of the given page.
*/
- public function copyPage(Page $page, Entity $newParent, string $newName = '')
+ protected function getUserDraftQuery(Page $page)
{
- $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
- $newChapter = $newParent->isA('chapter') ? $newParent : null;
- $copyPage = $this->getDraftPage($newBook, $newChapter);
- $pageData = $page->getAttributes();
-
- // Update name
- if (!empty($newName)) {
- $pageData['name'] = $newName;
- }
-
- // Copy tags from previous page if set
- if ($page->tags) {
- $pageData['tags'] = [];
- foreach ($page->tags as $tag) {
- $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
- }
- }
-
- // Set priority
- if ($newParent->isA('chapter')) {
- $pageData['priority'] = $this->getNewChapterPriority($newParent);
- } else {
- $pageData['priority'] = $this->getNewBookPriority($newParent);
- }
-
- return $this->publishPageDraft($copyPage, $pageData);
+ return PageRevision::query()->where('created_by', '=', user()->id)
+ ->where('type', 'update_draft')
+ ->where('page_id', '=', $page->id)
+ ->orderBy('created_at', 'desc');
}
}
diff --git a/app/Entities/SearchService.php b/app/Entities/SearchService.php
index 9e7cfdd0c..ee9b87786 100644
--- a/app/Entities/SearchService.php
+++ b/app/Entities/SearchService.php
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
class SearchService
{
@@ -210,7 +211,7 @@ class SearchService
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
- $functionName = camel_case('filter_' . $filterTerm);
+ $functionName = Str::camel('filter_' . $filterTerm);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
}
@@ -514,7 +515,7 @@ class SearchService
protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
{
- $functionName = camel_case('sort_by_' . $input);
+ $functionName = Str::camel('sort_by_' . $input);
if (method_exists($this, $functionName)) {
$this->$functionName($query, $model);
}
diff --git a/app/Entities/SlugGenerator.php b/app/Entities/SlugGenerator.php
new file mode 100644
index 000000000..459a5264a
--- /dev/null
+++ b/app/Entities/SlugGenerator.php
@@ -0,0 +1,62 @@
+entity = $entity;
+ }
+
+ /**
+ * Generate a fresh slug for the given entity.
+ * The slug will generated so it does not conflict within the same parent item.
+ */
+ public function generate(): string
+ {
+ $slug = $this->formatNameAsSlug($this->entity->name);
+ while ($this->slugInUse($slug)) {
+ $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+ }
+ return $slug;
+ }
+
+ /**
+ * Format a name as a url slug.
+ */
+ protected function formatNameAsSlug(string $name): string
+ {
+ $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
+ $slug = preg_replace('/\s{2,}/', ' ', $slug);
+ $slug = str_replace(' ', '-', $slug);
+ if ($slug === "") {
+ $slug = substr(md5(rand(1, 500)), 0, 5);
+ }
+ return $slug;
+ }
+
+ /**
+ * Check if a slug is already in-use for this
+ * type of model within the same parent.
+ */
+ protected function slugInUse(string $slug): bool
+ {
+ $query = $this->entity->newQuery()->where('slug', '=', $slug);
+
+ if ($this->entity instanceof BookChild) {
+ $query->where('book_id', '=', $this->entity->book_id);
+ }
+
+ if ($this->entity->id) {
+ $query->where('id', '!=', $this->entity->id);
+ }
+
+ return $query->count() > 0;
+ }
+}
diff --git a/app/Exceptions/MoveOperationException.php b/app/Exceptions/MoveOperationException.php
new file mode 100644
index 000000000..c237dfad3
--- /dev/null
+++ b/app/Exceptions/MoveOperationException.php
@@ -0,0 +1,8 @@
+userId = $userId;
+ parent::__construct($message);
+ }
+}
diff --git a/app/Exceptions/UserTokenNotFoundException.php b/app/Exceptions/UserTokenNotFoundException.php
new file mode 100644
index 000000000..3ed53f72a
--- /dev/null
+++ b/app/Exceptions/UserTokenNotFoundException.php
@@ -0,0 +1,6 @@
+attachmentService = $attachmentService;
$this->attachment = $attachment;
- $this->entityRepo = $entityRepo;
+ $this->pageRepo = $pageRepo;
parent::__construct();
}
/**
* Endpoint at which attachments are uploaded to.
- * @param Request $request
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+ * @throws ValidationException
+ * @throws NotFoundException
*/
public function upload(Request $request)
{
@@ -41,7 +41,7 @@ class AttachmentController extends Controller
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
@@ -59,11 +59,10 @@ class AttachmentController extends Controller
/**
* Update an uploaded attachment.
- * @param int $attachmentId
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function uploadUpdate($attachmentId, Request $request)
+ public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
@@ -71,7 +70,7 @@ class AttachmentController extends Controller
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
@@ -94,11 +93,10 @@ class AttachmentController extends Controller
/**
* Update the details of an existing file.
- * @param $attachmentId
- * @param Request $request
- * @return Attachment|mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function update($attachmentId, Request $request)
+ public function update(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
@@ -107,7 +105,7 @@ class AttachmentController extends Controller
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
@@ -123,8 +121,8 @@ class AttachmentController extends Controller
/**
* Attach a link to a page.
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
public function attachLink(Request $request)
{
@@ -135,7 +133,7 @@ class AttachmentController extends Controller
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
@@ -149,29 +147,26 @@ class AttachmentController extends Controller
/**
* Get the attachments for a specific page.
- * @param $pageId
- * @return mixed
*/
- public function listForPage($pageId)
+ public function listForPage(int $pageId)
{
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
}
/**
* Update the attachment sorting.
- * @param $pageId
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function sortForPage($pageId, Request $request)
+ public function sortForPage(Request $request, int $pageId)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
]);
- $page = $this->entityRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
@@ -181,16 +176,15 @@ class AttachmentController extends Controller
/**
* Get an attachment from storage.
- * @param $attachmentId
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ * @throws FileNotFoundException
* @throws NotFoundException
*/
- public function get($attachmentId)
+ public function get(int $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
- $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
- if ($page === null) {
+ try {
+ $page = $this->pageRepo->getById($attachment->uploaded_to);
+ } catch (NotFoundException $exception) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
@@ -208,9 +202,9 @@ class AttachmentController extends Controller
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
- * @throws \Exception
+ * @throws Exception
*/
- public function delete($attachmentId)
+ public function delete(int $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
diff --git a/app/Http/Controllers/Auth/ConfirmEmailController.php b/app/Http/Controllers/Auth/ConfirmEmailController.php
new file mode 100644
index 000000000..099558eb7
--- /dev/null
+++ b/app/Http/Controllers/Auth/ConfirmEmailController.php
@@ -0,0 +1,116 @@
+emailConfirmationService = $emailConfirmationService;
+ $this->userRepo = $userRepo;
+ parent::__construct();
+ }
+
+
+ /**
+ * Show the page to tell the user to check their email
+ * and confirm their address.
+ */
+ public function show()
+ {
+ return view('auth.register-confirm');
+ }
+
+ /**
+ * Shows a notice that a user's email address has not been confirmed,
+ * Also has the option to re-send the confirmation email.
+ * @return View
+ */
+ public function showAwaiting()
+ {
+ return view('auth.user-unconfirmed');
+ }
+
+ /**
+ * Confirms an email via a token and logs the user into the system.
+ * @param $token
+ * @return RedirectResponse|Redirector
+ * @throws ConfirmationEmailException
+ * @throws Exception
+ */
+ public function confirm($token)
+ {
+ try {
+ $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ if ($exception instanceof UserTokenNotFoundException) {
+ $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
+ return redirect('/register');
+ }
+
+ if ($exception instanceof UserTokenExpiredException) {
+ $user = $this->userRepo->getById($exception->userId);
+ $this->emailConfirmationService->sendConfirmation($user);
+ $this->showErrorNotification(trans('errors.email_confirmation_expired'));
+ return redirect('/register/confirm');
+ }
+
+ throw $exception;
+ }
+
+ $user = $this->userRepo->getById($userId);
+ $user->email_confirmed = true;
+ $user->save();
+
+ auth()->login($user);
+ $this->showSuccessNotification(trans('auth.email_confirm_success'));
+ $this->emailConfirmationService->deleteByUser($user);
+
+ return redirect('/');
+ }
+
+
+ /**
+ * Resend the confirmation email
+ * @param Request $request
+ * @return View
+ */
+ public function resend(Request $request)
+ {
+ $this->validate($request, [
+ 'email' => 'required|email|exists:users,email'
+ ]);
+ $user = $this->userRepo->getByEmail($request->get('email'));
+
+ try {
+ $this->emailConfirmationService->sendConfirmation($user);
+ } catch (Exception $e) {
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
+ return redirect('/register/confirm');
+ }
+
+ $this->showSuccessNotification(trans('auth.email_confirm_resent'));
+ return redirect('/register/confirm');
+ }
+}
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
index a0cbae9c6..a3c0433a5 100644
--- a/app/Http/Controllers/Auth/ForgotPasswordController.php
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -53,7 +53,7 @@ class ForgotPasswordController extends Controller
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return back()->with('status', trans($response));
}
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 0a9537995..0cb050a89 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -98,6 +98,7 @@ class LoginController extends Controller
$user->save();
$this->userRepo->attachDefaultRole($user);
+ $this->userRepo->downloadAndAssignUserAvatar($user);
auth()->login($user);
}
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index a285899cc..304d3bed2 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -18,7 +18,8 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Redirector;
-use Illuminate\View\View;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialUser;
use Validator;
@@ -53,7 +54,7 @@ class RegisterController extends Controller
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
- * @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService
+ * @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
@@ -78,7 +79,7 @@ class RegisterController extends Controller
return Validator::make($data, [
'name' => 'required|min:2|max:255',
'email' => 'required|email|max:255|unique:users',
- 'password' => 'required|min:6',
+ 'password' => 'required|min:8',
]);
}
@@ -130,7 +131,7 @@ class RegisterController extends Controller
return User::create([
'name' => $data['name'],
'email' => $data['email'],
- 'password' => bcrypt($data['password']),
+ 'password' => Hash::make($data['password']),
]);
}
@@ -159,83 +160,23 @@ class RegisterController extends Controller
$newUser->socialAccounts()->save($socialAccount);
}
- if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
+ if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
$newUser->save();
try {
$this->emailConfirmationService->sendConfirmation($newUser);
} catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
}
return redirect('/register/confirm');
}
auth()->login($newUser);
- session()->flash('success', trans('auth.register_success'));
+ $this->showSuccessNotification(trans('auth.register_success'));
return redirect($this->redirectPath());
}
- /**
- * Show the page to tell the user to check their email
- * and confirm their address.
- */
- public function getRegisterConfirmation()
- {
- return view('auth.register-confirm');
- }
-
- /**
- * Confirms an email via a token and logs the user into the system.
- * @param $token
- * @return RedirectResponse|Redirector
- * @throws UserRegistrationException
- */
- public function confirmEmail($token)
- {
- $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
- $user = $confirmation->user;
- $user->email_confirmed = true;
- $user->save();
- auth()->login($user);
- session()->flash('success', trans('auth.email_confirm_success'));
- $this->emailConfirmationService->deleteConfirmationsByUser($user);
- return redirect($this->redirectPath);
- }
-
- /**
- * Shows a notice that a user's email address has not been confirmed,
- * Also has the option to re-send the confirmation email.
- * @return View
- */
- public function showAwaitingConfirmation()
- {
- return view('auth.user-unconfirmed');
- }
-
- /**
- * Resend the confirmation email
- * @param Request $request
- * @return View
- */
- public function resendConfirmation(Request $request)
- {
- $this->validate($request, [
- 'email' => 'required|email|exists:users,email'
- ]);
- $user = $this->userRepo->getByEmail($request->get('email'));
-
- try {
- $this->emailConfirmationService->sendConfirmation($user);
- } catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
- return redirect('/register/confirm');
- }
-
- session()->flash('success', trans('auth.email_confirm_resent'));
- return redirect('/register/confirm');
- }
-
/**
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
@@ -252,14 +193,14 @@ class RegisterController extends Controller
/**
* The callback for social login services.
- * @param $socialDriver
* @param Request $request
+ * @param string $socialDriver
* @return RedirectResponse|Redirector
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws SocialDriverNotConfigured
*/
- public function socialCallback($socialDriver, Request $request)
+ public function socialCallback(Request $request, string $socialDriver)
{
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
@@ -322,7 +263,7 @@ class RegisterController extends Controller
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
- 'password' => str_random(30)
+ 'password' => Str::random(30)
];
return $this->registerUser($userData, $socialAccount, $emailVerified);
}
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
index 56f1cf026..4d98eca59 100644
--- a/app/Http/Controllers/Auth/ResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -4,6 +4,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
@@ -36,13 +37,14 @@ class ResetPasswordController extends Controller
/**
* Get the response for a successful password reset.
*
- * @param string $response
+ * @param Request $request
+ * @param string $response
* @return \Illuminate\Http\Response
*/
- protected function sendResetResponse($response)
+ protected function sendResetResponse(Request $request, $response)
{
$message = trans('auth.reset_password_success');
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return redirect($this->redirectPath())
->with('status', trans($response));
}
diff --git a/app/Http/Controllers/Auth/UserInviteController.php b/app/Http/Controllers/Auth/UserInviteController.php
new file mode 100644
index 000000000..c361b0a9b
--- /dev/null
+++ b/app/Http/Controllers/Auth/UserInviteController.php
@@ -0,0 +1,105 @@
+inviteService = $inviteService;
+ $this->userRepo = $userRepo;
+ $this->middleware('guest');
+ parent::__construct();
+ }
+
+ /**
+ * Show the page for the user to set the password for their account.
+ * @param string $token
+ * @return Factory|View|RedirectResponse
+ * @throws Exception
+ */
+ public function showSetPassword(string $token)
+ {
+ try {
+ $this->inviteService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ return $this->handleTokenException($exception);
+ }
+
+ return view('auth.invite-set-password', [
+ 'token' => $token,
+ ]);
+ }
+
+ /**
+ * Sets the password for an invited user and then grants them access.
+ * @param Request $request
+ * @param string $token
+ * @return RedirectResponse|Redirector
+ * @throws Exception
+ */
+ public function setPassword(Request $request, string $token)
+ {
+ $this->validate($request, [
+ 'password' => 'required|min:8'
+ ]);
+
+ try {
+ $userId = $this->inviteService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ return $this->handleTokenException($exception);
+ }
+
+ $user = $this->userRepo->getById($userId);
+ $user->password = bcrypt($request->get('password'));
+ $user->email_confirmed = true;
+ $user->save();
+
+ auth()->login($user);
+ $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
+ $this->inviteService->deleteByUser($user);
+
+ return redirect('/');
+ }
+
+ /**
+ * Check and validate the exception thrown when checking an invite token.
+ * @param Exception $exception
+ * @return RedirectResponse|Redirector
+ * @throws Exception
+ */
+ protected function handleTokenException(Exception $exception)
+ {
+ if ($exception instanceof UserTokenNotFoundException) {
+ return redirect('/');
+ }
+
+ if ($exception instanceof UserTokenExpiredException) {
+ $this->showErrorNotification(trans('errors.invite_token_expired'));
+ return redirect('/password/email');
+ }
+
+ throw $exception;
+ }
+}
diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php
index a990eed9a..e7d788d91 100644
--- a/app/Http/Controllers/BookController.php
+++ b/app/Http/Controllers/BookController.php
@@ -1,67 +1,46 @@
entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
- $this->exportService = $exportService;
+ public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+ {
+ $this->bookRepo = $bookRepo;
$this->entityContextManager = $entityContextManager;
- $this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Display a listing of the book.
- * @return Response
*/
public function index()
{
- $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
- $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
- $sortOptions = [
- 'name' => trans('common.sort_name'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ];
-
- $books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
- $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
- $popular = $this->entityRepo->getPopular('book', 4, 0);
- $new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
+ $view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
+ $sort = setting()->getForCurrentUser('books_sort', 'name');
+ $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+
+ $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+ $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
+ $popular = $this->bookRepo->getPopular(4);
+ $new = $this->bookRepo->getRecentlyCreated(4);
$this->entityContextManager->clearShelfContext();
@@ -74,25 +53,22 @@ class BookController extends Controller
'view' => $view,
'sort' => $sort,
'order' => $order,
- 'sortOptions' => $sortOptions,
]);
}
/**
* Show the form for creating a new book.
- * @param string $shelfSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function create(string $shelfSlug = null)
{
+ $this->checkPermission('book-create-all');
+
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+ $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
- $this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books.create', [
'bookshelf' => $bookshelf
@@ -101,12 +77,8 @@ class BookController extends Controller
/**
* Store a newly created book in storage.
- *
- * @param Request $request
- * @param string $shelfSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ImageUploadException
+ * @throws ValidationException
*/
public function store(Request $request, string $shelfSlug = null)
{
@@ -114,21 +86,21 @@ class BookController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+ $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
- $book = $this->entityRepo->createFromInput('book', $request->all());
- $this->bookUpdateActions($book, $request);
+ $book = $this->bookRepo->create($request->all());
+ $this->bookRepo->updateCoverImage($book, $request->file('image', null));
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
- $this->entityRepo->appendBookToShelf($bookshelf, $book);
+ $bookshelf->appendBook($book);
Activity::add($bookshelf, 'bookshelf_update');
}
@@ -137,17 +109,11 @@ class BookController extends Controller
/**
* Display the specified book.
- * @param $slug
- * @param Request $request
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
- public function show($slug, Request $request)
+ public function show(Request $request, string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
- $this->checkOwnablePermission('book-view', $book);
-
- $bookChildren = $this->entityRepo->getBookChildren($book);
+ $book = $this->bookRepo->getBySlug($slug);
+ $bookChildren = (new BookContents($book))->getTree(true);
Views::add($book);
if ($request->has('shelf')) {
@@ -165,12 +131,10 @@ class BookController extends Controller
/**
* Show the form for editing the specified book.
- * @param $slug
- * @return Response
*/
- public function edit($slug)
+ public function edit(string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
+ $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books.edit', ['book' => $book, 'current' => $book]);
@@ -178,254 +142,83 @@ class BookController extends Controller
/**
* Update the specified book in storage.
- * @param Request $request
- * @param $slug
- * @return Response
- * @throws \BookStack\Exceptions\ImageUploadException
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws ImageUploadException
+ * @throws ValidationException
+ * @throws Throwable
*/
public function update(Request $request, string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
+ $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
- $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
- $this->bookUpdateActions($book, $request);
+ $book = $this->bookRepo->update($book, $request->all());
+ $resetCover = $request->has('image_reset');
+ $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
- Activity::add($book, 'book_update', $book->id);
+ Activity::add($book, 'book_update', $book->id);
- return redirect($book->getUrl());
+ return redirect($book->getUrl());
}
/**
- * Shows the page to confirm deletion
- * @param $bookSlug
- * @return \Illuminate\View\View
+ * Shows the page to confirm deletion.
*/
- public function showDelete($bookSlug)
+ public function showDelete(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
+ $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
- * Shows the view which allows pages to be re-ordered and sorted.
- * @param string $bookSlug
- * @return \Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
- */
- public function sort($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $this->checkOwnablePermission('book-update', $book);
-
- $bookChildren = $this->entityRepo->getBookChildren($book, true);
-
- $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
- return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
- }
-
- /**
- * Shows the sort box for a single book.
- * Used via AJAX when loading in extra books to a sort.
- * @param $bookSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Remove the specified book from the system.
+ * @throws Throwable
+ * @throws NotifyException
*/
- public function getSortItem($bookSlug)
+ public function destroy(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $bookChildren = $this->entityRepo->getBookChildren($book);
- return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
- }
-
- /**
- * Saves an array of sort mapping to pages and chapters.
- * @param string $bookSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function saveSort($bookSlug, Request $request)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $this->checkOwnablePermission('book-update', $book);
-
- // Return if no map sent
- if (!$request->filled('sort-tree')) {
- return redirect($book->getUrl());
- }
-
- // Sort pages and chapters
- $sortMap = collect(json_decode($request->get('sort-tree')));
- $bookIdsInvolved = collect([$book->id]);
-
- // Load models into map
- $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
- $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
- $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
- // Store source and target books
- $bookIdsInvolved->push(intval($mapItem->model->book_id));
- $bookIdsInvolved->push(intval($mapItem->book));
- });
-
- // Get the books involved in the sort
- $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
- $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
- // Throw permission error if invalid ids or inaccessible books given.
- if (count($bookIdsInvolved) !== count($booksInvolved)) {
- $this->showPermissionError();
- }
- // Check permissions of involved books
- $booksInvolved->each(function (Book $book) {
- $this->checkOwnablePermission('book-update', $book);
- });
-
- // Perform the sort
- $sortMap->each(function ($mapItem) {
- $model = $mapItem->model;
-
- $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
- $bookChanged = intval($model->book_id) !== intval($mapItem->book);
- $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
-
- if ($bookChanged) {
- $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
- }
- if ($chapterChanged) {
- $model->chapter_id = intval($mapItem->parentChapter);
- $model->save();
- }
- if ($priorityChanged) {
- $model->priority = intval($mapItem->sort);
- $model->save();
- }
- });
-
- // Rebuild permissions and add activity for involved books.
- $booksInvolved->each(function (Book $book) {
- $this->entityRepo->buildJointPermissionsForBook($book);
- Activity::add($book, 'book_sort', $book->id);
- });
-
- return redirect($book->getUrl());
- }
-
- /**
- * Remove the specified book from storage.
- * @param $bookSlug
- * @return Response
- */
- public function destroy($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- Activity::addMessage('book_delete', 0, $book->name);
- if ($book->cover) {
- $this->imageRepo->destroyImage($book->cover);
- }
- $this->entityRepo->destroyBook($book);
+ Activity::addMessage('book_delete', $book->name);
+ $this->bookRepo->destroy($book);
return redirect('/books');
}
/**
- * Show the Restrictions view.
- * @param $bookSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Show the permissions view.
*/
- public function showPermissions($bookSlug)
+ public function showPermissions(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $roles = $this->userRepo->getRestrictableRoles();
+
return view('books.permissions', [
'book' => $book,
- 'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
- * @param $bookSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws Throwable
*/
- public function permissions($bookSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
- session()->flash('success', trans('entities.books_permissions_updated'));
- return redirect($book->getUrl());
- }
-
- /**
- * Export a book as a PDF file.
- * @param string $bookSlug
- * @return mixed
- */
- public function exportPdf($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $pdfContent = $this->exportService->bookToPdf($book);
- return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
- }
-
- /**
- * Export a book as a contained HTML file.
- * @param string $bookSlug
- * @return mixed
- */
- public function exportHtml($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $htmlContent = $this->exportService->bookToContainedHtml($book);
- return $this->downloadResponse($htmlContent, $bookSlug . '.html');
- }
-
- /**
- * Export a book as a plain text file.
- * @param $bookSlug
- * @return mixed
- */
- public function exportPlainText($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $textContent = $this->exportService->bookToPlainText($book);
- return $this->downloadResponse($textContent, $bookSlug . '.txt');
- }
- /**
- * Common actions to run on book update.
- * Handles updating the cover image.
- * @param Book $book
- * @param Request $request
- * @throws \BookStack\Exceptions\ImageUploadException
- */
- protected function bookUpdateActions(Book $book, Request $request)
- {
- // Update the cover image if in request
- if ($request->has('image')) {
- $this->imageRepo->destroyImage($book->cover);
- $newImage = $request->file('image');
- $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
- $book->image_id = $image->id;
- $book->save();
- }
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->bookRepo->updatePermissions($book, $restricted, $permissions);
- if ($request->has('image_reset')) {
- $this->imageRepo->destroyImage($book->cover);
- $book->image_id = 0;
- $book->save();
- }
+ $this->showSuccessNotification(trans('entities.books_permissions_updated'));
+ return redirect($book->getUrl());
}
}
diff --git a/app/Http/Controllers/BookExportController.php b/app/Http/Controllers/BookExportController.php
new file mode 100644
index 000000000..cfa3d6a3a
--- /dev/null
+++ b/app/Http/Controllers/BookExportController.php
@@ -0,0 +1,56 @@
+bookRepo = $bookRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Export a book as a PDF file.
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $pdfContent = $this->exportService->bookToPdf($book);
+ return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
+ }
+
+ /**
+ * Export a book as a contained HTML file.
+ * @throws Throwable
+ */
+ public function html(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $htmlContent = $this->exportService->bookToContainedHtml($book);
+ return $this->downloadResponse($htmlContent, $bookSlug . '.html');
+ }
+
+ /**
+ * Export a book as a plain text file.
+ */
+ public function plainText(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $textContent = $this->exportService->bookToPlainText($book);
+ return $this->downloadResponse($textContent, $bookSlug . '.txt');
+ }
+}
diff --git a/app/Http/Controllers/BookSortController.php b/app/Http/Controllers/BookSortController.php
new file mode 100644
index 000000000..f5fb6f255
--- /dev/null
+++ b/app/Http/Controllers/BookSortController.php
@@ -0,0 +1,82 @@
+bookRepo = $bookRepo;
+ parent::__construct();
+ }
+
+ /**
+ * Shows the view which allows pages to be re-ordered and sorted.
+ */
+ public function show(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('book-update', $book);
+
+ $bookChildren = (new BookContents($book))->getTree(false);
+
+ $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
+ return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
+ }
+
+ /**
+ * Shows the sort box for a single book.
+ * Used via AJAX when loading in extra books to a sort.
+ */
+ public function showItem(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $bookChildren = (new BookContents($book))->getTree();
+ return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
+ }
+
+ /**
+ * Sorts a book using a given mapping array.
+ */
+ public function update(Request $request, string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('book-update', $book);
+
+ // Return if no map sent
+ if (!$request->filled('sort-tree')) {
+ return redirect($book->getUrl());
+ }
+
+ $sortMap = collect(json_decode($request->get('sort-tree')));
+ $bookContents = new BookContents($book);
+ $booksInvolved = collect();
+
+ try {
+ $booksInvolved = $bookContents->sortUsingMap($sortMap);
+ } catch (SortOperationException $exception) {
+ $this->showPermissionError();
+ }
+
+ // Rebuild permissions and add activity for involved books.
+ $booksInvolved->each(function (Book $book) {
+ Activity::add($book, 'book_sort', $book->id);
+ });
+
+ return redirect($book->getUrl());
+ }
+}
diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php
index bcf2e12df..57e67dc00 100644
--- a/app/Http/Controllers/BookshelfController.php
+++ b/app/Http/Controllers/BookshelfController.php
@@ -1,34 +1,30 @@
entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
+ $this->bookshelfRepo = $bookshelfRepo;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
@@ -36,27 +32,22 @@ class BookshelfController extends Controller
/**
* Display a listing of the book.
- * @return Response
*/
public function index()
{
- $view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
- $sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
+ $view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+ $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
+ $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
- $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
- foreach ($shelves as $shelf) {
- $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
- }
-
- $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
- $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
- $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
+ $shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
+ $recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
+ $popular = $this->bookshelfRepo->getPopular(4);
+ $new = $this->bookshelfRepo->getRecentlyCreated(4);
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
@@ -74,21 +65,19 @@ class BookshelfController extends Controller
/**
* Show the form for creating a new bookshelf.
- * @return Response
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
- $books = $this->entityRepo->getAll('book', false, 'update');
+ $books = Book::hasPermission('update')->get();
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
- * @param Request $request
- * @return Response
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ValidationException
+ * @throws ImageUploadException
*/
public function store(Request $request)
{
@@ -96,80 +85,63 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
- $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
- $this->shelfUpdateActions($shelf, $request);
+ $bookIds = explode(',', $request->get('books', ''));
+ $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
+ $this->bookshelfRepo->updateCoverImage($shelf);
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
-
/**
- * Display the specified bookshelf.
- * @param String $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
+ * Display the bookshelf of the given slug.
+ * @throws NotFoundException
*/
public function show(string $slug)
{
- /** @var Bookshelf $shelf */
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('book-view', $shelf);
- $books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$this->setPageTitle($shelf->getShortName());
-
return view('shelves.show', [
'shelf' => $shelf,
- 'books' => $books,
'activity' => Activity::entityActivity($shelf, 20, 1)
]);
}
/**
* Show the form for editing the specified bookshelf.
- * @param $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
- $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
- $shelfBookIds = $shelfBooks->pluck('id');
- $books = $this->entityRepo->getAll('book', false, 'update');
- $books = $books->filter(function ($book) use ($shelfBookIds) {
- return !$shelfBookIds->contains($book->id);
- });
+ $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
+ $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
- 'shelfBooks' => $shelfBooks,
]);
}
-
/**
* Update the specified bookshelf in storage.
- * @param Request $request
- * @param string $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ValidationException
+ * @throws ImageUploadException
+ * @throws NotFoundException
*/
public function update(Request $request, string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
@@ -177,24 +149,22 @@ class BookshelfController extends Controller
'image' => $this->imageRepo->getImageValidationRules(),
]);
- $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
- $this->shelfUpdateActions($shelf, $request);
- Activity::add($shelf, 'bookshelf_update');
+ $bookIds = explode(',', $request->get('books', ''));
+ $shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
+ $resetCover = $request->has('image_reset');
+ $this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
+ Activity::add($shelf, 'bookshelf_update');
- return redirect($shelf->getUrl());
+ return redirect($shelf->getUrl());
}
-
/**
* Shows the page to confirm deletion
- * @param $slug
- * @return \Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@@ -203,101 +173,58 @@ class BookshelfController extends Controller
/**
* Remove the specified bookshelf from storage.
- * @param string $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws Exception
*/
public function destroy(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
- Activity::addMessage('bookshelf_delete', 0, $shelf->name);
- if ($shelf->cover) {
- $this->imageRepo->destroyImage($shelf->cover);
- }
- $this->entityRepo->destroyBookshelf($shelf);
+ Activity::addMessage('bookshelf_delete', $shelf->name);
+ $this->bookshelfRepo->destroy($shelf);
return redirect('/shelves');
}
/**
* Show the permissions view.
- * @param string $slug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function showPermissions(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $roles = $this->userRepo->getRestrictableRoles();
return view('shelves.permissions', [
'shelf' => $shelf,
- 'roles' => $roles
]);
}
/**
* Set the permissions for this bookshelf.
- * @param string $slug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
*/
- public function permissions(string $slug, Request $request)
+ public function permissions(Request $request, string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
- session()->flash('success', trans('entities.shelves_permissions_updated'));
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+
+ $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
- * @param string $slug
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
- session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+ $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
+ $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($shelf->getUrl());
}
-
- /**
- * Common actions to run on bookshelf update.
- * @param Bookshelf $shelf
- * @param Request $request
- * @throws \BookStack\Exceptions\ImageUploadException
- */
- protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
- {
- // Update the books that the shelf references
- $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
-
- // Update the cover image if in request
- if ($request->has('image')) {
- $newImage = $request->file('image');
- $this->imageRepo->destroyImage($shelf->cover);
- $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
- $shelf->image_id = $image->id;
- $shelf->save();
- }
-
- if ($request->has('image_reset')) {
- $this->imageRepo->destroyImage($shelf->cover);
- $shelf->image_id = 0;
- $shelf->save();
- }
- }
}
diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php
index c19e45694..135597910 100644
--- a/app/Http/Controllers/ChapterController.php
+++ b/app/Http/Controllers/ChapterController.php
@@ -1,83 +1,74 @@
entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
- $this->exportService = $exportService;
+ $this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Show the form for creating a new chapter.
- * @param $bookSlug
- * @return Response
*/
- public function create($bookSlug)
+ public function create(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
+
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
* Store a newly created chapter in storage.
- * @param $bookSlug
- * @param Request $request
- * @return Response
+ * @throws ValidationException
*/
- public function store($bookSlug, Request $request)
+ public function store(Request $request, string $bookSlug)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
- $input = $request->all();
- $input['priority'] = $this->entityRepo->getNewBookPriority($book);
- $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
+ $chapter = $this->chapterRepo->create($request->all(), $book);
Activity::add($chapter, 'chapter_create', $book->id);
+
return redirect($chapter->getUrl());
}
/**
* Display the specified chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
*/
- public function show($bookSlug, $chapterSlug)
+ public function show(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
- $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
+
+ $sidebarTree = (new BookContents($chapter->book))->getTree();
+ $pages = $chapter->getVisiblePages();
Views::add($chapter);
+
$this->setPageTitle($chapter->getShortName());
- $pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
@@ -89,79 +80,71 @@ class ChapterController extends Controller
/**
* Show the form for editing the specified chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
*/
- public function edit($bookSlug, $chapterSlug)
+ public function edit(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
+
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Update the specified chapter in storage.
- * @param Request $request
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function update(Request $request, $bookSlug, $chapterSlug)
+ public function update(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
- $this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
+ $this->chapterRepo->update($chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
+
return redirect($chapter->getUrl());
}
/**
* Shows the page to confirm deletion of this chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return \Illuminate\View\View
+ * @throws NotFoundException
*/
- public function showDelete($bookSlug, $chapterSlug)
+ public function showDelete(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
+
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Remove the specified chapter from storage.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
+ * @throws NotFoundException
+ * @throws Throwable
*/
- public function destroy($bookSlug, $chapterSlug)
+ public function destroy(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
- Activity::addMessage('chapter_delete', $book->id, $chapter->name);
- $this->entityRepo->destroyChapter($chapter);
- return redirect($book->getUrl());
+
+ Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+ $this->chapterRepo->destroy($chapter);
+
+ return redirect($chapter->book->getUrl());
}
/**
* Show the page for moving a chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return mixed
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function showMove($bookSlug, $chapterSlug)
+ public function showMove(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
+
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book
@@ -170,15 +153,11 @@ class ChapterController extends Controller
/**
* Perform the move action for a chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @param Request $request
- * @return mixed
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function move($bookSlug, $chapterSlug, Request $request)
+ public function move(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
@@ -187,100 +166,47 @@ class ChapterController extends Controller
return redirect($chapter->getUrl());
}
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
- $parent = false;
-
- if ($entityType == 'book') {
- $parent = $this->entityRepo->getById('book', $entityId);
- }
-
- if ($parent === false || $parent === null) {
- session()->flash('error', trans('errors.selected_book_not_found'));
+ try {
+ $newBook = $this->chapterRepo->move($chapter, $entitySelection);
+ } catch (MoveOperationException $exception) {
+ $this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect()->back();
}
- $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
- Activity::add($chapter, 'chapter_move', $chapter->book->id);
- session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
+ Activity::add($chapter, 'chapter_move', $newBook->id);
+ $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
return redirect($chapter->getUrl());
}
/**
* Show the Restrictions view.
- * @param $bookSlug
- * @param $chapterSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function showPermissions($bookSlug, $chapterSlug)
+ public function showPermissions(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $roles = $this->userRepo->getRestrictableRoles();
+
return view('chapters.permissions', [
'chapter' => $chapter,
- 'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws NotFoundException
*/
- public function permissions($bookSlug, $chapterSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
- session()->flash('success', trans('entities.chapters_permissions_success'));
- return redirect($chapter->getUrl());
- }
- /**
- * Exports a chapter to pdf .
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPdf($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $pdfContent = $this->exportService->chapterToPdf($chapter);
- return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
- }
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
- /**
- * Export a chapter to a self-contained HTML file.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportHtml($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
- return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
- }
-
- /**
- * Export a chapter to a simple plaintext .txt file.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPlainText($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $chapterText = $this->exportService->chapterToPlainText($chapter);
- return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+ $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
+ return redirect($chapter->getUrl());
}
}
diff --git a/app/Http/Controllers/ChapterExportController.php b/app/Http/Controllers/ChapterExportController.php
new file mode 100644
index 000000000..0c86f8548
--- /dev/null
+++ b/app/Http/Controllers/ChapterExportController.php
@@ -0,0 +1,58 @@
+chapterRepo = $chapterRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Exports a chapter to pdf.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $pdfContent = $this->exportService->chapterToPdf($chapter);
+ return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
+ }
+
+ /**
+ * Export a chapter to a self-contained HTML file.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function html(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+ return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
+ }
+
+ /**
+ * Export a chapter to a simple plaintext .txt file.
+ * @throws NotFoundException
+ */
+ public function plainText(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapterText = $this->exportService->chapterToPlainText($chapter);
+ return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+ }
+}
diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php
index 860b50762..068358d72 100644
--- a/app/Http/Controllers/CommentController.php
+++ b/app/Http/Controllers/CommentController.php
@@ -2,44 +2,36 @@
use Activity;
use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
+use BookStack\Entities\Page;
use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
class CommentController extends Controller
{
- protected $entityRepo;
protected $commentRepo;
/**
* CommentController constructor.
- * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
- * @param \BookStack\Actions\CommentRepo $commentRepo
*/
- public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
+ public function __construct(CommentRepo $commentRepo)
{
- $this->entityRepo = $entityRepo;
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Save a new comment for a Page
- * @param Request $request
- * @param integer $pageId
- * @param null|integer $commentId
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+ * @throws ValidationException
*/
- public function savePageComment(Request $request, $pageId, $commentId = null)
+ public function savePageComment(Request $request, int $pageId, int $commentId = null)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
- try {
- $page = $this->entityRepo->getById('page', $pageId, true);
- } catch (ModelNotFoundException $e) {
+ $page = Page::visible()->find($pageId);
+ if ($page === null) {
return response('Not found', 404);
}
@@ -59,11 +51,9 @@ class CommentController extends Controller
/**
* Update an existing comment.
- * @param Request $request
- * @param integer $commentId
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws ValidationException
*/
- public function update(Request $request, $commentId)
+ public function update(Request $request, int $commentId)
{
$this->validate($request, [
'text' => 'required|string',
@@ -80,13 +70,12 @@ class CommentController extends Controller
/**
* Delete a comment from the system.
- * @param integer $id
- * @return \Illuminate\Http\JsonResponse
*/
- public function destroy($id)
+ public function destroy(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('comment-delete', $comment);
+
$this->commentRepo->delete($comment);
return response()->json(['message' => trans('entities.comment_deleted')]);
}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 5bc62c601..b9576f2fe 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -2,7 +2,6 @@
namespace BookStack\Http\Controllers;
-use BookStack\Auth\User;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
@@ -14,42 +13,27 @@ abstract class Controller extends BaseController
{
use DispatchesJobs, ValidatesRequests;
- /**
- * @var User static
- */
- protected $currentUser;
- /**
- * @var bool
- */
- protected $signedIn;
-
/**
* Controller constructor.
*/
public function __construct()
{
- $this->middleware(function ($request, $next) {
-
- // Get a user instance for the current user
- $user = user();
-
- // 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);
- });
+ /**
+ * Check if the current user is signed in.
+ */
+ protected function isSignedIn(): bool
+ {
+ return auth()->check();
}
/**
* Stops the application and shows a permission error if
* the application is in demo mode.
*/
- protected function preventAccessForDemoUsers()
+ protected function preventAccessInDemoMode()
{
if (config('app.env') === 'demo') {
$this->showPermissionError();
@@ -75,7 +59,7 @@ abstract class Controller extends BaseController
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
} else {
$response = redirect('/');
- session()->flash('error', trans('errors.permission'));
+ $this->showErrorNotification(trans('errors.permission'));
}
throw new HttpResponseException($response);
@@ -133,7 +117,7 @@ abstract class Controller extends BaseController
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function () use ($userId) {
- return $userId === $this->currentUser->id;
+ return $userId === user()->id;
});
}
@@ -145,7 +129,7 @@ abstract class Controller extends BaseController
*/
protected function jsonError($messageText = "", $statusCode = 500)
{
- return response()->json(['message' => $messageText], $statusCode);
+ return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
@@ -178,4 +162,39 @@ abstract class Controller extends BaseController
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
+
+ /**
+ * Show a positive, successful notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showSuccessNotification(string $message)
+ {
+ session()->flash('success', $message);
+ }
+
+ /**
+ * Show a warning notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showWarningNotification(string $message)
+ {
+ session()->flash('warning', $message);
+ }
+
+ /**
+ * Show an error notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showErrorNotification(string $message)
+ {
+ session()->flash('error', $message);
+ }
+
+ /**
+ * Get the validation rules for image files.
+ */
+ protected function getImageValidationRules(): string
+ {
+ return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
+ }
}
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index d2c75f956..260952fd1 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -1,23 +1,16 @@
entityRepo = $entityRepo;
- parent::__construct();
- }
/**
* Display the homepage.
@@ -26,10 +19,20 @@ class HomeController extends Controller
public function index()
{
$activity = Activity::latest(10);
- $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
+ $draftPages = [];
+
+ if ($this->isSignedIn()) {
+ $draftPages = Page::visible()->where('draft', '=', true)
+ ->where('created_by', '=', user()->id)
+ ->orderBy('updated_at', 'desc')->take(6)->get();
+ }
+
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
- $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
- $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
+ $recents = $this->isSignedIn() ?
+ Views::getUserRecentlyViewed(12*$recentFactor, 0)
+ : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+ $recentlyUpdatedPages = Page::visible()->where('draft', false)
+ ->orderBy('updated_at', 'desc')->take(12)->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$homepageOption = setting('app-homepage-type', 'default');
@@ -47,9 +50,9 @@ class HomeController extends Controller
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
- $view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
- $sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
- $order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
+ $view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
+ $sort = setting()->getForCurrentUser($key . '_sort', 'name');
+ $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
@@ -66,16 +69,18 @@ class HomeController extends Controller
}
if ($homepageOption === 'bookshelves') {
- $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
+ $shelfRepo = app(BookshelfRepo::class);
+ $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
foreach ($shelves as $shelf) {
- $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
+ $shelf->books = $shelf->visibleBooks;
}
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
- $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
+ $bookRepo = app(BookRepo::class);
+ $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
return view('common.home-book', $data);
}
@@ -83,8 +88,9 @@ class HomeController extends Controller
if ($homepageOption === 'page') {
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
- $customHomepage = $this->entityRepo->getById('page', $id, false, true);
- $this->entityRepo->renderPage($customHomepage, true);
+ $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+ $pageContent = new PageContent($customHomepage);
+ $customHomepage->html = $pageContent->render(true);
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php
index 024003f87..9c67704dd 100644
--- a/app/Http/Controllers/Images/ImageController.php
+++ b/app/Http/Controllers/Images/ImageController.php
@@ -1,6 +1,6 @@
validate($request, [
'name' => 'required|min:2|string'
@@ -69,16 +69,21 @@ class ImageController extends Controller
/**
* Show the usage of an image on pages.
- * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
- * @param $id
- * @return \Illuminate\Http\JsonResponse
*/
- public function usage(EntityRepo $entityRepo, $id)
+ public function usage(int $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
- $pageSearch = $entityRepo->searchForImage($image->url);
- return response()->json($pageSearch);
+
+ $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
+ foreach ($pages as $page) {
+ $page->url = $page->getUrl();
+ $page->html = '';
+ $page->text = '';
+ }
+ $result = count($pages) > 0 ? $pages : false;
+
+ return response()->json($result);
}
/**
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index 89fb83fd9..630f888ed 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -1,61 +1,46 @@
pageRepo = $pageRepo;
- $this->exportService = $exportService;
- $this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new page.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return Response
- * @internal param bool $pageSlug
- * @throws NotFoundException
+ * @throws Throwable
*/
- public function create($bookSlug, $chapterSlug = null)
+ public function create(string $bookSlug, string $chapterSlug = null)
{
- if ($chapterSlug !== null) {
- $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
- } else {
- $chapter = null;
- $book = $this->pageRepo->getBySlug('book', $bookSlug);
- }
-
- $parent = $chapter ? $chapter : $book;
+ $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
- if ($this->signedIn) {
- $draft = $this->pageRepo->getDraftPage($book, $chapter);
+ if ($this->isSignedIn()) {
+ $draft = $this->pageRepo->getNewDraftPage($parent);
return redirect($draft->getUrl());
}
@@ -66,117 +51,94 @@ class PageController extends Controller
/**
* Create a new page as a guest user.
- * @param Request $request
- * @param string $bookSlug
- * @param string|null $chapterSlug
- * @return mixed
- * @throws NotFoundException
+ * @throws ValidationException
*/
- public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
+ public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- if ($chapterSlug !== null) {
- $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
- } else {
- $chapter = null;
- $book = $this->pageRepo->getBySlug('book', $bookSlug);
- }
-
- $parent = $chapter ? $chapter : $book;
+ $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
- $page = $this->pageRepo->getDraftPage($book, $chapter);
- $this->pageRepo->publishPageDraft($page, [
+ $page = $this->pageRepo->getNewDraftPage($parent);
+ $this->pageRepo->publishDraft($page, [
'name' => $request->get('name'),
'html' => ''
]);
+
return redirect($page->getUrl('/edit'));
}
/**
* Show form to continue editing a draft page.
- * @param string $bookSlug
- * @param int $pageId
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws NotFoundException
*/
- public function editDraft($bookSlug, $pageId)
+ public function editDraft(string $bookSlug, int $pageId)
{
- $draft = $this->pageRepo->getById('page', $pageId, true);
- $this->checkOwnablePermission('page-create', $draft->parent);
+ $draft = $this->pageRepo->getById($pageId);
+ $this->checkOwnablePermission('page-create', $draft->parent());
$this->setPageTitle(trans('entities.pages_edit_draft'));
- $draftsEnabled = $this->signedIn;
+ $draftsEnabled = $this->isSignedIn();
+ $templates = $this->pageRepo->getTemplates(10);
+
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
- 'draftsEnabled' => $draftsEnabled
+ 'draftsEnabled' => $draftsEnabled,
+ 'templates' => $templates,
]);
}
/**
* Store a new page by changing a draft into a page.
- * @param Request $request
- * @param string $bookSlug
- * @param int $pageId
- * @return Response
+ * @throws NotFoundException
+ * @throws ValidationException
*/
- public function store(Request $request, $bookSlug, $pageId)
+ public function store(Request $request, string $bookSlug, int $pageId)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
+ $draftPage = $this->pageRepo->getById($pageId);
+ $this->checkOwnablePermission('page-create', $draftPage->parent());
- $input = $request->all();
- $draftPage = $this->pageRepo->getById('page', $pageId, true);
- $book = $draftPage->book;
-
- $parent = $draftPage->parent;
- $this->checkOwnablePermission('page-create', $parent);
-
- if ($parent->isA('chapter')) {
- $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
- } else {
- $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
- }
-
- $page = $this->pageRepo->publishPageDraft($draftPage, $input);
+ $page = $this->pageRepo->publishDraft($draftPage, $request->all());
+ Activity::add($page, 'page_create', $draftPage->book->id);
- Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
}
/**
* Display the specified page.
* If the page is not found via the slug the revisions are searched for a match.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
* @throws NotFoundException
*/
- public function show($bookSlug, $pageSlug)
+ public function show(string $bookSlug, string $pageSlug)
{
try {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
} catch (NotFoundException $e) {
- $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+
if ($page === null) {
throw $e;
}
+
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
- $page->html = $this->pageRepo->renderPage($page);
- $sidebarTree = $this->pageRepo->getBookChildren($page->book);
- $pageNav = $this->pageRepo->getPageNav($page->html);
+ $pageContent = (new PageContent($page));
+ $page->html = $pageContent->render();
+ $sidebarTree = (new BookContents($page->book))->getTree();
+ $pageNav = $pageContent->getNavigation($page->html);
- // check if the comment's are enabled
+ // Check if page comments are enabled
$commentsEnabled = !setting('app-disable-comments');
if ($commentsEnabled) {
$page->load(['comments.createdBy']);
@@ -185,7 +147,8 @@ class PageController extends Controller
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
- 'page' => $page,'book' => $page->book,
+ 'page' => $page,
+ 'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
@@ -195,93 +158,86 @@ class PageController extends Controller
/**
* Get page from an ajax request.
- * @param int $pageId
- * @return \Illuminate\Http\JsonResponse
+ * @throws NotFoundException
*/
- public function getPageAjax($pageId)
+ public function getPageAjax(int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
* @throws NotFoundException
*/
- public function edit($bookSlug, $pageSlug)
+ public function edit(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
+
$page->isDraft = false;
+ $editActivity = new PageEditActivity($page);
// Check for active editing
$warnings = [];
- if ($this->pageRepo->isPageEditingActive($page, 60)) {
- $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
+ if ($editActivity->hasActiveEditing()) {
+ $warnings[] = $editActivity->activeEditingMessage();
}
// Check for a current draft version for this user
- $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
- if ($userPageDraft !== null) {
- $page->name = $userPageDraft->name;
- $page->html = $userPageDraft->html;
- $page->markdown = $userPageDraft->markdown;
+ $userDraft = $this->pageRepo->getUserDraft($page);
+ if ($userDraft !== null) {
+ $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$page->isDraft = true;
- $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
+ $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
if (count($warnings) > 0) {
- session()->flash('warning', implode("\n", $warnings));
+ $this->showWarningNotification(implode("\n", $warnings));
}
- $draftsEnabled = $this->signedIn;
+ $templates = $this->pageRepo->getTemplates(10);
+ $draftsEnabled = $this->isSignedIn();
+ $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
- 'draftsEnabled' => $draftsEnabled
+ 'draftsEnabled' => $draftsEnabled,
+ 'templates' => $templates,
]);
}
/**
* Update the specified page in storage.
- * @param Request $request
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function update(Request $request, $bookSlug, $pageSlug)
+ public function update(Request $request, string $bookSlug, string $pageSlug)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $this->pageRepo->updatePage($page, $page->book->id, $request->all());
+
+ $this->pageRepo->update($page, $request->all());
Activity::add($page, 'page_update', $page->book->id);
+
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
- * @param Request $request
- * @param int $pageId
- * @return \Illuminate\Http\JsonResponse
+ * @throws NotFoundException
*/
- public function saveDraft(Request $request, $pageId)
+ public function saveDraft(Request $request, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
- if (!$this->signedIn) {
- return response()->json([
- 'status' => 'error',
- 'message' => trans('errors.guests_cannot_save_drafts'),
- ], 500);
+ if (!$this->isSignedIn()) {
+ return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
@@ -295,253 +251,98 @@ class PageController extends Controller
}
/**
- * Redirect from a special link url which
- * uses the page id rather than the name.
- * @param int $pageId
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * Redirect from a special link url which uses the page id rather than the name.
+ * @throws NotFoundException
*/
- public function redirectFromLink($pageId)
+ public function redirectFromLink(int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\View\View
+ * @throws NotFoundException
*/
- public function showDelete($bookSlug, $pageSlug)
+ public function showDelete(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
- return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+ return view('pages.delete', [
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page
+ ]);
}
-
/**
* Show the deletion page for the specified page.
- * @param string $bookSlug
- * @param int $pageId
- * @return \Illuminate\View\View
* @throws NotFoundException
*/
- public function showDeleteDraft($bookSlug, $pageId)
+ public function showDeleteDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
- return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+ return view('pages.delete', [
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page
+ ]);
}
/**
* Remove the specified page from storage.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
- * @internal param int $id
+ * @throws NotFoundException
+ * @throws Throwable
+ * @throws NotifyException
*/
- public function destroy($bookSlug, $pageSlug)
+ public function destroy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $book = $page->book;
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
- $this->pageRepo->destroyPage($page);
- Activity::addMessage('page_delete', $book->id, $page->name);
- session()->flash('success', trans('entities.pages_delete_success'));
+ $book = $page->book;
+ $this->pageRepo->destroy($page);
+ Activity::addMessage('page_delete', $page->name, $book->id);
+
+ $this->showSuccessNotification(trans('entities.pages_delete_success'));
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
- * @param string $bookSlug
- * @param int $pageId
- * @return Response
* @throws NotFoundException
+ * @throws NotifyException
+ * @throws Throwable
*/
- public function destroyDraft($bookSlug, $pageId)
+ public function destroyDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$book = $page->book;
+ $chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page);
- session()->flash('success', trans('entities.pages_delete_draft_success'));
- $this->pageRepo->destroyPage($page);
- return redirect($book->getUrl());
- }
-
- /**
- * Shows the last revisions for this page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\View\View
- * @throws NotFoundException
- */
- public function showRevisions($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
- return view('pages.revisions', ['page' => $page, 'current' => $page]);
- }
- /**
- * Shows a preview of a single revision
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revisionId
- * @return \Illuminate\View\View
- */
- public function showRevision($bookSlug, $pageSlug, $revisionId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- if ($revision === null) {
- abort(404);
- }
+ $this->pageRepo->destroy($page);
- $page->fill($revision->toArray());
- $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+ $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
- return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => null,
- 'revision' => $revision
- ]);
- }
-
- /**
- * 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)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- if ($revision === null) {
- abort(404);
+ if ($chapter && userCan('view', $chapter)) {
+ return redirect($chapter->getUrl());
}
-
- $prev = $revision->getPrevious();
- $prevContent = ($prev === null) ? '' : $prev->html;
- $diff = (new Htmldiff)->diff($prevContent, $revision->html);
-
- $page->fill($revision->toArray());
- $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
-
- return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => $diff,
- 'revision' => $revision
- ]);
- }
-
- /**
- * Restores a page using the content of the specified revision.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revisionId
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function restoreRevision($bookSlug, $pageSlug, $revisionId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->checkOwnablePermission('page-update', $page);
- $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
- Activity::add($page, 'page_restore', $page->book->id);
- return redirect($page->getUrl());
- }
-
-
- /**
- * Deletes a revision using the id of the specified revision.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revId
- * @throws NotFoundException
- * @throws BadRequestException
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function destroyRevision($bookSlug, $pageSlug, $revId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->checkOwnablePermission('page-delete', $page);
-
- $revision = $page->revisions()->where('id', '=', $revId)->first();
- if ($revision === null) {
- throw new NotFoundException("Revision #{$revId} not found");
- }
-
- // Get the current revision for the page
- $currentRevision = $page->getCurrentRevision();
-
- // Check if its the latest revision, cannot delete latest revision.
- if (intval($currentRevision->id) === intval($revId)) {
- session()->flash('error', trans('entities.revision_cannot_delete_latest'));
- return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
- }
-
- $revision->delete();
- session()->flash('success', trans('entities.revision_delete_success'));
- return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
- }
-
- /**
- * Exports a page to a PDF.
- * https://github.com/barryvdh/laravel-dompdf
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPdf($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $page->html = $this->pageRepo->renderPage($page);
- $pdfContent = $this->exportService->pageToPdf($page);
- return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
- }
-
- /**
- * Export a page to a self-contained HTML file.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportHtml($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $page->html = $this->pageRepo->renderPage($page);
- $containedHtml = $this->exportService->pageToContainedHtml($page);
- return $this->downloadResponse($containedHtml, $pageSlug . '.html');
- }
-
- /**
- * Export a page to a simple plaintext .txt file.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPlainText($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $pageText = $this->exportService->pageToPlainText($page);
- return $this->downloadResponse($pageText, $pageSlug . '.txt');
+ return redirect($book->getUrl());
}
/**
- * Show a listing of recently created pages
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Show a listing of recently created pages.
*/
public function showRecentlyUpdated()
{
- // TODO - Still exist?
- $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
+ $pages = Page::visible()->orderBy('updated_at', 'desc')
+ ->paginate(20)
+ ->setPath(url('/pages/recently-updated'));
+
return view('pages.detailed-listing', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
@@ -550,14 +351,11 @@ class PageController extends Controller
/**
* Show the view to choose a new parent to move a page into.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return mixed
* @throws NotFoundException
*/
- public function showMove($bookSlug, $pageSlug)
+ public function showMove(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
@@ -567,16 +365,13 @@ class PageController extends Controller
}
/**
- * Does the action of moving the location of a page
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return mixed
+ * Does the action of moving the location of a page.
* @throws NotFoundException
+ * @throws Throwable
*/
- public function move($bookSlug, $pageSlug, Request $request)
+ public function move(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
@@ -585,37 +380,29 @@ class PageController extends Controller
return redirect($page->getUrl());
}
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
-
try {
- $parent = $this->pageRepo->getById($entityType, $entityId);
- } catch (\Exception $e) {
- session()->flash(trans('entities.selected_book_chapter_not_found'));
+ $parent = $this->pageRepo->move($page, $entitySelection);
+ } catch (Exception $exception) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
+
+ $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect()->back();
}
- $this->checkOwnablePermission('page-create', $parent);
-
- $this->pageRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
- session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
+ $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return mixed
* @throws NotFoundException
*/
- public function showCopy($bookSlug, $pageSlug)
+ public function showCopy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
@@ -624,78 +411,65 @@ class PageController extends Controller
]);
}
+
/**
* Create a copy of a page within the requested target destination.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return mixed
* @throws NotFoundException
+ * @throws Throwable
*/
- public function copy($bookSlug, $pageSlug, Request $request)
+ public function copy(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
- $entitySelection = $request->get('entity_selection', null);
- if ($entitySelection === null || $entitySelection === '') {
- $parent = $page->chapter ? $page->chapter : $page->book;
- } else {
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
- try {
- $parent = $this->pageRepo->getById($entityType, $entityId);
- } catch (\Exception $e) {
- session()->flash(trans('entities.selected_book_chapter_not_found'));
- return redirect()->back();
- }
- }
+ $entitySelection = $request->get('entity_selection', null) ?? null;
+ $newName = $request->get('name', null);
- $this->checkOwnablePermission('page-create', $parent);
+ try {
+ $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
+ } catch (Exception $exception) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
- $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
+ $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+ return redirect()->back();
+ }
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
- session()->flash('success', trans('entities.pages_copy_success'));
+ $this->showSuccessNotification(trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws NotFoundException
*/
- public function showPermissions($bookSlug, $pageSlug)
+ public function showPermissions(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $roles = $this->userRepo->getRestrictableRoles();
return view('pages.permissions', [
'page' => $page,
- 'roles' => $roles
]);
}
/**
* Set the permissions for this page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
- * @throws \Throwable
+ * @throws Throwable
*/
- public function permissions($bookSlug, $pageSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
- session()->flash('success', trans('entities.pages_permissions_success'));
+
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+
+ $this->showSuccessNotification(trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}
diff --git a/app/Http/Controllers/PageExportController.php b/app/Http/Controllers/PageExportController.php
new file mode 100644
index 000000000..3b02ea224
--- /dev/null
+++ b/app/Http/Controllers/PageExportController.php
@@ -0,0 +1,66 @@
+pageRepo = $pageRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Exports a page to a PDF.
+ * https://github.com/barryvdh/laravel-dompdf
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page->html = (new PageContent($page))->render();
+ $pdfContent = $this->exportService->pageToPdf($page);
+ return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
+ }
+
+ /**
+ * Export a page to a self-contained HTML file.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function html(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page->html = (new PageContent($page))->render();
+ $containedHtml = $this->exportService->pageToContainedHtml($page);
+ return $this->downloadResponse($containedHtml, $pageSlug . '.html');
+ }
+
+ /**
+ * Export a page to a simple plaintext .txt file.
+ * @throws NotFoundException
+ */
+ public function plainText(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $pageText = $this->exportService->pageToPlainText($page);
+ return $this->downloadResponse($pageText, $pageSlug . '.txt');
+ }
+}
diff --git a/app/Http/Controllers/PageRevisionController.php b/app/Http/Controllers/PageRevisionController.php
new file mode 100644
index 000000000..3c65b50ac
--- /dev/null
+++ b/app/Http/Controllers/PageRevisionController.php
@@ -0,0 +1,128 @@
+pageRepo = $pageRepo;
+ parent::__construct();
+ }
+
+ /**
+ * Shows the last revisions for this page.
+ * @throws NotFoundException
+ */
+ public function index(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+ return view('pages.revisions', [
+ 'page' => $page,
+ 'current' => $page
+ ]);
+ }
+
+ /**
+ * Shows a preview of a single revision.
+ * @throws NotFoundException
+ */
+ public function show(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ if ($revision === null) {
+ throw new NotFoundException();
+ }
+
+ $page->fill($revision->toArray());
+
+ $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+ return view('pages.revision', [
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => null,
+ 'revision' => $revision
+ ]);
+ }
+
+ /**
+ * Shows the changes of a single revision.
+ * @throws NotFoundException
+ */
+ public function changes(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ if ($revision === null) {
+ throw new NotFoundException();
+ }
+
+ $prev = $revision->getPrevious();
+ $prevContent = $prev->html ?? '';
+ $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+
+ $page->fill($revision->toArray());
+ $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
+
+ return view('pages.revision', [
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => $diff,
+ 'revision' => $revision
+ ]);
+ }
+
+ /**
+ * Restores a page using the content of the specified revision.
+ * @throws NotFoundException
+ */
+ public function restore(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->checkOwnablePermission('page-update', $page);
+
+ $page = $this->pageRepo->restoreRevision($page, $revisionId);
+
+ Activity::add($page, 'page_restore', $page->book->id);
+ return redirect($page->getUrl());
+ }
+
+ /**
+ * Deletes a revision using the id of the specified revision.
+ * @throws NotFoundException
+ */
+ public function destroy(string $bookSlug, string $pageSlug, int $revId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->checkOwnablePermission('page-delete', $page);
+
+ $revision = $page->revisions()->where('id', '=', $revId)->first();
+ if ($revision === null) {
+ throw new NotFoundException("Revision #{$revId} not found");
+ }
+
+ // Get the current revision for the page
+ $currentRevision = $page->getCurrentRevision();
+
+ // Check if its the latest revision, cannot delete latest revision.
+ if (intval($currentRevision->id) === intval($revId)) {
+ $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
+ return redirect($page->getUrl('/revisions'));
+ }
+
+ $revision->delete();
+ $this->showSuccessNotification(trans('entities.revision_delete_success'));
+ return redirect($page->getUrl('/revisions'));
+ }
+}
diff --git a/app/Http/Controllers/PageTemplateController.php b/app/Http/Controllers/PageTemplateController.php
new file mode 100644
index 000000000..eaa1a8ae2
--- /dev/null
+++ b/app/Http/Controllers/PageTemplateController.php
@@ -0,0 +1,57 @@
+pageRepo = $pageRepo;
+ parent::__construct();
+ }
+
+ /**
+ * Fetch a list of templates from the system.
+ */
+ public function list(Request $request)
+ {
+ $page = $request->get('page', 1);
+ $search = $request->get('search', '');
+ $templates = $this->pageRepo->getTemplates(10, $page, $search);
+
+ if ($search) {
+ $templates->appends(['search' => $search]);
+ }
+
+ return view('pages.template-manager-list', [
+ 'templates' => $templates
+ ]);
+ }
+
+ /**
+ * Get the content of a template.
+ * @throws NotFoundException
+ */
+ public function get(int $templateId)
+ {
+ $page = $this->pageRepo->getById($templateId);
+
+ if (!$page->template) {
+ throw new NotFoundException();
+ }
+
+ return response()->json([
+ 'html' => $page->html,
+ 'markdown' => $page->markdown,
+ ]);
+ }
+}
diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php
index 9893d5993..148ae5cd6 100644
--- a/app/Http/Controllers/PermissionController.php
+++ b/app/Http/Controllers/PermissionController.php
@@ -53,7 +53,7 @@ class PermissionController extends Controller
]);
$this->permissionsRepo->saveNewRole($request->all());
- session()->flash('success', trans('settings.role_create_success'));
+ $this->showSuccessNotification(trans('settings.role_create_success'));
return redirect('/settings/roles');
}
@@ -75,12 +75,13 @@ class PermissionController extends Controller
/**
* Updates a user role.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
+ * @throws \Illuminate\Validation\ValidationException
*/
- public function updateRole($id, Request $request)
+ public function updateRole(Request $request, $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
@@ -89,7 +90,7 @@ class PermissionController extends Controller
]);
$this->permissionsRepo->updateRole($id, $request->all());
- session()->flash('success', trans('settings.role_update_success'));
+ $this->showSuccessNotification(trans('settings.role_update_success'));
return redirect('/settings/roles');
}
@@ -112,22 +113,22 @@ class PermissionController extends Controller
/**
* Delete a role from the system,
* Migrate from a previous role if set.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
- public function deleteRole($id, Request $request)
+ public function deleteRole(Request $request, $id)
{
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
- session()->flash('error', $e->getMessage());
+ $this->showErrorNotification($e->getMessage());
return redirect()->back();
}
- session()->flash('success', trans('settings.role_delete_success'));
+ $this->showSuccessNotification(trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}
diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php
index 1691ee9b0..a5cd7ad6b 100644
--- a/app/Http/Controllers/SearchController.php
+++ b/app/Http/Controllers/SearchController.php
@@ -1,35 +1,27 @@
entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
@@ -38,9 +30,6 @@ class SearchController extends Controller
/**
* Searches all entities.
- * @param Request $request
- * @return View
- * @internal param string $searchTerm
*/
public function search(Request $request)
{
@@ -64,12 +53,8 @@ class SearchController extends Controller
/**
* Searches all entities within a book.
- * @param Request $request
- * @param integer $bookId
- * @return View
- * @internal param string $searchTerm
*/
- public function searchBook(Request $request, $bookId)
+ public function searchBook(Request $request, int $bookId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
@@ -78,12 +63,8 @@ class SearchController extends Controller
/**
* Searches all entities within a chapter.
- * @param Request $request
- * @param integer $chapterId
- * @return View
- * @internal param string $searchTerm
*/
- public function searchChapter(Request $request, $chapterId)
+ public function searchChapter(Request $request, int $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
@@ -93,8 +74,6 @@ class SearchController extends Controller
/**
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
- * @param Request $request
- * @return mixed
*/
public function searchEntitiesAjax(Request $request)
{
@@ -115,15 +94,13 @@ class SearchController extends Controller
/**
* Search siblings items in the system.
- * @param Request $request
- * @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
- $entity = $this->entityRepo->getById($type, $id);
+ $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
@@ -132,12 +109,12 @@ class SearchController extends Controller
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
- $entities = $this->entityRepo->getChapterChildren($entity->chapter);
+ $entities = $entity->chapter->visiblePages();
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
- $entities = $this->entityRepo->getBookDirectChildren($entity->book);
+ $entities = $entity->book->getDirectChildren();
}
// Book
@@ -145,15 +122,15 @@ class SearchController extends Controller
if ($entity->isA('book')) {
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
- $entities = $this->entityRepo->getBookshelfChildren($contextShelf);
+ $entities = $contextShelf->visibleBooks()->get();
} else {
- $entities = $this->entityRepo->getAll('book');
+ $entities = Book::visible()->get();
}
}
// Shelve
if ($entity->isA('bookshelf')) {
- $entities = $this->entityRepo->getAll('bookshelf');
+ $entities = Bookshelf::visible()->get();
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 650833c7f..f0a078300 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -1,6 +1,7 @@
preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
@@ -76,7 +77,7 @@ class SettingController extends Controller
setting()->remove('app-logo');
}
- session()->flash('success', trans('settings.settings_save_success'));
+ $this->showSuccessNotification(trans('settings.settings_save_success'));
return redirect('/settings');
}
@@ -111,16 +112,32 @@ class SettingController extends Controller
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
- session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+ $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
- session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+ $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
+
+ /**
+ * Action to send a test e-mail to the current user.
+ * @param Request $request
+ * @param User $user
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ */
+ public function sendTestEmail(Request $request)
+ {
+ $this->checkPermission('settings-manage');
+
+ user()->notify(new TestEmail());
+ $this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
+
+ return redirect('/settings/maintenance#image-cleanup')->withInput();
+ }
}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 8191fbfe2..b55398d2f 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -1,30 +1,35 @@
user = $user;
$this->userRepo = $userRepo;
+ $this->inviteService = $inviteService;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -75,8 +80,10 @@ class UserController extends Controller
];
$authMethod = config('auth.method');
- if ($authMethod === 'standard') {
- $validationRules['password'] = 'required|min:5';
+ $sendInvite = ($request->get('send_invite', 'false') === 'true');
+
+ if ($authMethod === 'standard' && !$sendInvite) {
+ $validationRules['password'] = 'required|min:6';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
$validationRules['external_auth_id'] = 'required';
@@ -86,13 +93,17 @@ class UserController extends Controller
$user = $this->user->fill($request->all());
if ($authMethod === 'standard') {
- $user->password = bcrypt($request->get('password'));
+ $user->password = bcrypt($request->get('password', Str::random(32)));
} elseif ($authMethod === 'ldap') {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
+ if ($sendInvite) {
+ $this->inviteService->sendInvitation($user);
+ }
+
if ($request->filled('roles')) {
$roles = $request->get('roles');
$this->userRepo->setUserRoles($user, $roles);
@@ -133,20 +144,25 @@ class UserController extends Controller
*/
public function update(Request $request, $id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->validate($request, [
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
- 'password' => 'min:5|required_with:password_confirm',
+ 'password' => 'min:6|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
]);
$user = $this->userRepo->getById($id);
- $user->fill($request->all());
+ $user->fill($request->except(['email']));
+
+ // Email updates
+ if (userCan('users-manage') && $request->filled('email')) {
+ $user->email = $request->get('email');
+ }
// Role updates
if (userCan('users-manage') && $request->filled('roles')) {
@@ -161,7 +177,7 @@ class UserController extends Controller
}
// External auth id updates
- if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
+ if (user()->can('users-manage') && $request->filled('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
@@ -186,7 +202,7 @@ class UserController extends Controller
}
$user->save();
- session()->flash('success', trans('settings.users_edit_success'));
+ $this->showSuccessNotification(trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
@@ -214,23 +230,23 @@ class UserController extends Controller
*/
public function destroy($id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) {
- session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
+ $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
if ($user->system_name === 'public') {
- session()->flash('error', trans('errors.users_cannot_delete_guest'));
+ $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
- session()->flash('success', trans('settings.users_delete_success'));
+ $this->showSuccessNotification(trans('settings.users_delete_success'));
return redirect('/settings/users');
}
@@ -245,7 +261,7 @@ class UserController extends Controller
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
- $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
+ $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users.profile', [
@@ -258,22 +274,22 @@ class UserController extends Controller
/**
* Update the user's preferred book-list display setting.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse
*/
- public function switchBookView($id, Request $request)
+ public function switchBookView(Request $request, $id)
{
return $this->switchViewType($id, $request, 'books');
}
/**
* Update the user's preferred shelf-list display setting.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse
*/
- public function switchShelfView($id, Request $request)
+ public function switchShelfView(Request $request, $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
}
@@ -303,12 +319,12 @@ class UserController extends Controller
/**
* Change the stored sort type for a particular view.
+ * @param Request $request
* @param string $id
* @param string $type
- * @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
- public function changeSort(string $id, string $type, Request $request)
+ public function changeSort(Request $request, string $id, string $type)
{
$validSortTypes = ['books', 'bookshelves'];
if (!in_array($type, $validSortTypes)) {
@@ -319,12 +335,12 @@ class UserController extends Controller
/**
* Update the stored section expansion preference for the given user.
+ * @param Request $request
* @param string $id
* @param string $key
- * @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
- public function updateExpansionPreference(string $id, string $key, Request $request)
+ public function updateExpansionPreference(Request $request, string $id, string $key)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index 7794f3401..027e469c8 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -12,7 +12,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
- \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
+ \BookStack\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\BookStack\Http\Middleware\TrimStrings::class,
\BookStack\Http\Middleware\TrustProxies::class,
@@ -29,9 +29,11 @@ class Kernel extends HttpKernel
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \Illuminate\Routing\Middleware\ThrottleRequests::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
- \BookStack\Http\Middleware\Localization::class
+ \BookStack\Http\Middleware\Localization::class,
+ \BookStack\Http\Middleware\GlobalViewData::class,
],
'api' => [
'throttle:60,1',
diff --git a/app/Http/Middleware/CheckForMaintenanceMode.php b/app/Http/Middleware/CheckForMaintenanceMode.php
new file mode 100644
index 000000000..0c7683836
--- /dev/null
+++ b/app/Http/Middleware/CheckForMaintenanceMode.php
@@ -0,0 +1,17 @@
+share('signedIn', auth()->check());
+ view()->share('currentUser', user());
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php
index 07852bb00..de48cb196 100644
--- a/app/Http/Middleware/Localization.php
+++ b/app/Http/Middleware/Localization.php
@@ -27,7 +27,7 @@ class Localization
'fr' => 'fr_FR',
'it' => 'it_IT',
'ja' => 'ja',
- 'kr' => 'ko_KR',
+ 'ko' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt_BR' => 'pt_BR',
@@ -37,6 +37,7 @@ class Localization
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
+ 'tr' => 'tr_TR',
];
/**
@@ -57,6 +58,8 @@ class Localization
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
+ config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
+
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
@@ -86,6 +89,16 @@ class Localization
return $default;
}
+ /**
+ * Get the ISO version of a BookStack language name
+ * @param string $locale
+ * @return string
+ */
+ public function getLocaleIso(string $locale)
+ {
+ return $this->localeMap[$locale] ?? $locale;
+ }
+
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
@@ -93,7 +106,7 @@ class Localization
*/
protected function setSystemDateLocale(string $locale)
{
- $systemLocale = $this->localeMap[$locale] ?? $locale;
+ $systemLocale = $this->getLocaleIso($locale);
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php
index 73c11a827..878c2f164 100644
--- a/app/Http/Middleware/TrustProxies.php
+++ b/app/Http/Middleware/TrustProxies.php
@@ -16,17 +16,11 @@ class TrustProxies extends Middleware
protected $proxies;
/**
- * The current proxy header mappings.
+ * The headers that should be used to detect proxies.
*
- * @var array
+ * @var int
*/
- protected $headers = [
- Request::HEADER_FORWARDED => 'FORWARDED',
- Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
- Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
- Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
- Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
- ];
+ protected $headers = Request::HEADER_X_FORWARDED_ALL;
/**
* Handle the request, Set the correct user-configured proxy information.
diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php
index 291b8326f..1a29a2b1d 100644
--- a/app/Http/Middleware/VerifyCsrfToken.php
+++ b/app/Http/Middleware/VerifyCsrfToken.php
@@ -6,6 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
+ /**
+ * Indicates whether the XSRF-TOKEN cookie should be set on the response.
+ *
+ * @var bool
+ */
+ protected $addHttpCookie = true;
+
/**
* The URIs that should be excluded from CSRF verification.
*
diff --git a/app/Http/Request.php b/app/Http/Request.php
index bd2761a0b..183686f67 100644
--- a/app/Http/Request.php
+++ b/app/Http/Request.php
@@ -22,5 +22,4 @@ class Request extends LaravelRequest
return $base;
}
-
-}
\ No newline at end of file
+}
diff --git a/app/Notifications/TestEmail.php b/app/Notifications/TestEmail.php
new file mode 100644
index 000000000..7fce1c19c
--- /dev/null
+++ b/app/Notifications/TestEmail.php
@@ -0,0 +1,18 @@
+newMailMessage()
+ ->subject(trans('settings.maint_send_test_email_mail_subject'))
+ ->greeting(trans('settings.maint_send_test_email_mail_greeting'))
+ ->line(trans('settings.maint_send_test_email_mail_text'));
+ }
+}
diff --git a/app/Notifications/UserInvite.php b/app/Notifications/UserInvite.php
new file mode 100644
index 000000000..b01911bcd
--- /dev/null
+++ b/app/Notifications/UserInvite.php
@@ -0,0 +1,31 @@
+token = $token;
+ }
+
+ /**
+ * 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 $this->newMailMessage()
+ ->subject(trans('auth.user_invite_email_subject', $appName))
+ ->greeting(trans('auth.user_invite_email_greeting', $appName))
+ ->line(trans('auth.user_invite_email_text'))
+ ->action(trans('auth.user_invite_email_action'), url('/https/source.bookstackapp.com/register/invite/' . $this->token));
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index a2fc673f4..3a1b4f42e 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -25,7 +25,12 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Set root URL
- URL::forceRootUrl(config('app.url'));
+ $appUrl = config('app.url');
+ if ($appUrl) {
+ $isHttps = (strpos($appUrl, 'https://') === 0);
+ URL::forceRootUrl($appUrl);
+ URL::forceScheme($isHttps ? 'https' : 'http');
+ }
// Custom validation methods
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
@@ -43,7 +48,7 @@ class AppServiceProvider extends ServiceProvider
return "";
});
- Blade::directive('exposeTranslations', function($expression) {
+ Blade::directive('exposeTranslations', function ($expression) {
return "startPush('translations'); ?>" .
"" .
'' . "\n" .
diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php
index e7bde5290..b4158187c 100644
--- a/app/Providers/CustomFacadeProvider.php
+++ b/app/Providers/CustomFacadeProvider.php
@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use BookStack\Actions\ActivityService;
use BookStack\Actions\ViewService;
+use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\SettingService;
use BookStack\Uploads\ImageService;
use Illuminate\Support\ServiceProvider;
@@ -27,20 +28,24 @@ class CustomFacadeProvider extends ServiceProvider
*/
public function register()
{
- $this->app->bind('activity', function () {
+ $this->app->singleton('activity', function () {
return $this->app->make(ActivityService::class);
});
- $this->app->bind('views', function () {
+ $this->app->singleton('views', function () {
return $this->app->make(ViewService::class);
});
- $this->app->bind('setting', function () {
+ $this->app->singleton('setting', function () {
return $this->app->make(SettingService::class);
});
- $this->app->bind('images', function () {
+ $this->app->singleton('images', function () {
return $this->app->make(ImageService::class);
});
+
+ $this->app->singleton('permissions', function () {
+ return $this->app->make(PermissionService::class);
+ });
}
}
diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php
index 7d51cc73d..9ff607afe 100644
--- a/app/Providers/TranslationServiceProvider.php
+++ b/app/Providers/TranslationServiceProvider.php
@@ -1,31 +1,21 @@
registerLoader();
-
- $this->app->singleton('translator', function ($app) {
- $loader = $app['translation.loader'];
-
- // When registering the translator component, we'll need to set the default
- // locale as well as the fallback locale. So, we'll grab the application
- // configuration so we can easily get both of these values from there.
- $locale = $app['config']['app.locale'];
-
- $trans = new Translator($loader, $locale);
-
- $trans->setFallback($app['config']['app.fallback_locale']);
-
- return $trans;
+ $this->app->singleton('translation.loader', function ($app) {
+ return new FileLoader($app['files'], $app['path.lang']);
});
}
-}
+
+}
\ No newline at end of file
diff --git a/app/Translation/FileLoader.php b/app/Translation/FileLoader.php
new file mode 100644
index 000000000..f0f895da5
--- /dev/null
+++ b/app/Translation/FileLoader.php
@@ -0,0 +1,30 @@
+loadJsonPaths($locale);
+ }
+
+ if (is_null($namespace) || $namespace === '*') {
+ $themeTranslations = $this->loadPath(theme_path('lang'), $locale, $group);
+ $originalTranslations = $this->loadPath($this->path, $locale, $group);
+ return array_merge($originalTranslations, $themeTranslations);
+ }
+
+ return $this->loadNamespaced($locale, $group, $namespace);
+ }
+}
\ No newline at end of file
diff --git a/app/Translation/Translator.php b/app/Translation/Translator.php
deleted file mode 100644
index 032f43ff7..000000000
--- a/app/Translation/Translator.php
+++ /dev/null
@@ -1,72 +0,0 @@
- 'de',
- ];
-
- /**
- * Get the translation for a given key.
- *
- * @param string $key
- * @param array $replace
- * @param string $locale
- * @return string|array|null
- */
- public function trans($key, array $replace = [], $locale = null)
- {
- $translation = $this->get($key, $replace, $locale);
-
- if (is_array($translation)) {
- $translation = $this->mergeBackupTranslations($translation, $key, $locale);
- }
-
- return $translation;
- }
-
- /**
- * Merge the fallback translations, and base translations if existing,
- * into the provided core key => value array of translations content.
- * @param array $translationArray
- * @param string $key
- * @param null $locale
- * @return array
- */
- protected function mergeBackupTranslations(array $translationArray, string $key, $locale = null)
- {
- $fallback = $this->get($key, [], $this->fallback);
- $baseLocale = $this->getBaseLocale($locale ?? $this->locale);
- $baseTranslations = $baseLocale ? $this->get($key, [], $baseLocale) : [];
-
- return array_replace_recursive($fallback, $baseTranslations, $translationArray);
- }
-
- /**
- * Get the array of locales to be checked.
- *
- * @param string|null $locale
- * @return array
- */
- protected function localeArray($locale)
- {
- $primaryLocale = $locale ?: $this->locale;
- return array_filter([$primaryLocale, $this->getBaseLocale($primaryLocale), $this->fallback]);
- }
-
- /**
- * Get the locale to extend for the given locale.
- *
- * @param string $locale
- * @return string|null
- */
- protected function getBaseLocale($locale)
- {
- return $this->baseLocaleMap[$locale] ?? null;
- }
-}
diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php
index 8720d3c09..3f0b447df 100644
--- a/app/Uploads/Attachment.php
+++ b/app/Uploads/Attachment.php
@@ -13,7 +13,7 @@ class Attachment extends Ownable
*/
public function getFileName()
{
- if (str_contains($this->name, '.')) {
+ if (strpos($this->name, '.') !== false) {
return $this->name;
}
return $this->name . '.' . $this->extension;
diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php
index 6e875a1e7..ae4fb6e96 100644
--- a/app/Uploads/AttachmentService.php
+++ b/app/Uploads/AttachmentService.php
@@ -2,6 +2,7 @@
use BookStack\Exceptions\FileUploadException;
use Exception;
+use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
@@ -185,9 +186,9 @@ class AttachmentService extends UploadService
$storage = $this->getStorage();
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
- $uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
+ $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
- $uploadFileName = str_random(3) . $uploadFileName;
+ $uploadFileName = Str::random(3) . $uploadFileName;
}
$attachmentPath = $basePath . $uploadFileName;
diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php
index 860230d00..e7668471b 100644
--- a/app/Uploads/ImageService.php
+++ b/app/Uploads/ImageService.php
@@ -7,6 +7,7 @@ use DB;
use Exception;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
@@ -140,12 +141,12 @@ class ImageService extends UploadService
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) {
- $imageName = str_random(3) . $imageName;
+ $imageName = Str::random(3) . $imageName;
}
$fullPath = $imagePath . $imageName;
if ($secureUploads) {
- $fullPath = $imagePath . str_random(16) . '-' . $imageName;
+ $fullPath = $imagePath . Str::random(16) . '-' . $imageName;
}
try {
@@ -220,7 +221,7 @@ class ImageService extends UploadService
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
- $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
+ $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
diff --git a/app/helpers.php b/app/helpers.php
index 9bbfcfbf0..6211f41be 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -12,7 +12,7 @@ use BookStack\Settings\SettingService;
* @return string
* @throws Exception
*/
-function versioned_asset($file = '') : string
+function versioned_asset(string $file = ''): string
{
static $version = null;
@@ -35,7 +35,7 @@ function versioned_asset($file = '') : string
* Defaults to public 'Guest' user if not logged in.
* @return User
*/
-function user() : User
+function user(): User
{
return auth()->user() ?: User::getDefault();
}
@@ -44,7 +44,7 @@ function user() : User
* Check if current user is a signed in user.
* @return bool
*/
-function signedInUser() : bool
+function signedInUser(): bool
{
return auth()->user() && !auth()->user()->isDefault();
}
@@ -53,7 +53,7 @@ function signedInUser() : bool
* Check if the current user has general access.
* @return bool
*/
-function hasAppAccess() : bool
+function hasAppAccess(): bool
{
return !auth()->guest() || setting('app-public');
}
@@ -66,7 +66,7 @@ function hasAppAccess() : bool
* @param Ownable $ownable
* @return bool
*/
-function userCan(string $permission, Ownable $ownable = null) : bool
+function userCan(string $permission, Ownable $ownable = null): bool
{
if ($ownable === null) {
return user() && user()->can($permission);
@@ -84,7 +84,7 @@ function userCan(string $permission, Ownable $ownable = null) : bool
* @param string|null $entityClass
* @return bool
*/
-function userCanOnAny(string $permission, string $entityClass = null) : bool
+function userCanOnAny(string $permission, string $entityClass = null): bool
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
@@ -92,11 +92,11 @@ function userCanOnAny(string $permission, string $entityClass = null) : bool
/**
* Helper to access system settings.
- * @param $key
- * @param bool $default
+ * @param string $key
+ * @param $default
* @return bool|string|SettingService
*/
-function setting($key = null, $default = false)
+function setting(string $key = null, $default = false)
{
$settingService = resolve(SettingService::class);
if (is_null($key)) {
@@ -110,7 +110,7 @@ function setting($key = null, $default = false)
* @param string $path
* @return string
*/
-function theme_path($path = '') : string
+function theme_path(string $path = ''): string
{
$theme = config('view.theme');
if (!$theme) {
@@ -130,18 +130,19 @@ function theme_path($path = '') : string
* @param array $attrs
* @return mixed
*/
-function icon($name, $attrs = [])
+function icon(string $name, array $attrs = []): string
{
$attrs = array_merge([
- 'class' => 'svg-icon',
- 'data-icon' => $name
+ 'class' => 'svg-icon',
+ 'data-icon' => $name,
+ 'role' => 'presentation',
], $attrs);
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
- $iconPath = resource_path('assets/icons/' . $name . '.svg');
+ $iconPath = resource_path('icons/' . $name . '.svg');
$themeIconPath = theme_path('icons/' . $name . '.svg');
if ($themeIconPath && file_exists($themeIconPath)) {
$iconPath = $themeIconPath;
@@ -157,12 +158,12 @@ function icon($name, $attrs = [])
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
- * @param $path
+ * @param string $path
* @param array $data
* @param array $overrideData
* @return string
*/
-function sortUrl($path, $data, $overrideData = [])
+function sortUrl(string $path, array $data, array $overrideData = []): string
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 516980cc1..6538aa81c 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -11,8 +11,8 @@
|
*/
-$app = new \BookStack\Application(
- realpath(__DIR__.'/../')
+$app = new BookStack\Application(
+ dirname(__DIR__)
);
/*
diff --git a/composer.json b/composer.json
index 457ce5093..1d952a0c5 100644
--- a/composer.json
+++ b/composer.json
@@ -5,47 +5,48 @@
"license": "MIT",
"type": "project",
"require": {
- "php": ">=7.0.5",
+ "php": "^7.2",
+ "ext-curl": "*",
+ "ext-dom": "*",
+ "ext-gd": "*",
"ext-json": "*",
+ "ext-mbstring": "*",
"ext-tidy": "*",
- "ext-dom": "*",
"ext-xml": "*",
- "ext-mbstring": "*",
- "ext-gd": "*",
- "ext-curl": "*",
- "laravel/framework": "~5.5.44",
- "fideloper/proxy": "~3.3",
- "intervention/image": "^2.4",
- "laravel/socialite": "3.0.x-dev",
+ "barryvdh/laravel-dompdf": "^0.8.5",
+ "barryvdh/laravel-snappy": "^0.4.5",
+ "doctrine/dbal": "^2.9",
+ "fideloper/proxy": "^4.0",
+ "gathercontent/htmldiff": "^0.2.1",
+ "intervention/image": "^2.5",
+ "laravel/framework": "^6.0",
+ "laravel/socialite": "^4.2",
"league/flysystem-aws-s3-v3": "^1.0",
- "barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",
- "gathercontent/htmldiff": "^0.2.1",
- "barryvdh/laravel-snappy": "^0.4.0",
- "socialiteproviders/slack": "^3.0",
+ "socialiteproviders/discord": "^2.0",
+ "socialiteproviders/gitlab": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
- "socialiteproviders/gitlab": "^3.0",
- "socialiteproviders/twitch": "^3.0",
- "socialiteproviders/discord": "^2.0",
- "doctrine/dbal": "^2.5",
+ "socialiteproviders/slack": "^3.0",
+ "socialiteproviders/twitch": "^5.0",
"aacotroneo/laravel-saml2": "^1.0"
},
"require-dev": {
- "filp/whoops": "~2.0",
- "fzaninotto/faker": "~1.4",
- "mockery/mockery": "~1.0",
- "phpunit/phpunit": "~6.0",
- "symfony/css-selector": "3.1.*",
- "symfony/dom-crawler": "3.1.*",
- "laravel/browser-kit-testing": "^2.0",
- "barryvdh/laravel-ide-helper": "^2.4.1",
- "barryvdh/laravel-debugbar": "^3.1.0",
- "squizlabs/php_codesniffer": "^3.2"
+ "barryvdh/laravel-debugbar": "^3.2.8",
+ "barryvdh/laravel-ide-helper": "^2.6.4",
+ "facade/ignition": "^1.4",
+ "fzaninotto/faker": "^1.4",
+ "laravel/browser-kit-testing": "^5.1",
+ "mockery/mockery": "^1.0",
+ "nunomaduro/collision": "^3.0",
+ "phpunit/phpunit": "^8.0",
+ "squizlabs/php_codesniffer": "^3.4",
+ "wnx/laravel-stats": "^2.0"
},
"autoload": {
"classmap": [
- "database"
+ "database/seeds",
+ "database/factories"
],
"psr-4": {
"BookStack\\": "app/"
@@ -58,37 +59,45 @@
},
"scripts": {
"post-root-package-install": [
- "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
- "php artisan key:generate"
+ "@php artisan key:generate --ansi"
],
"pre-update-cmd": [
- "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
- "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+ "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+ "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"pre-install-cmd": [
- "php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
- "php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+ "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
+ "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"post-install-cmd": [
- "php artisan cache:clear",
- "php artisan view:clear"
+ "@php artisan cache:clear",
+ "@php artisan view:clear"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover"
+ "@php artisan package:discover --ansi"
],
"refresh-test-database": [
- "php artisan migrate:refresh --database=mysql_testing",
- "php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
+ "@php artisan migrate:refresh --database=mysql_testing",
+ "@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
]
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
+ "sort-packages": true,
"platform": {
- "php": "7.0.5"
+ "php": "7.2.0"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "dont-discover": []
}
- }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
diff --git a/composer.lock b/composer.lock
index d35594642..3ec106ded 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1,89 +1,29 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "26a2c3ad0409c970f4f0c9b6dad49322",
+ "content-hash": "c156e1738dbab2a57f9a926d9a9a5a6a",
"packages": [
- {
- "name": "aacotroneo/laravel-saml2",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/aacotroneo/laravel-saml2.git",
- "reference": "5045701a07bcd7600a17c92971368669870f546a"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/aacotroneo/laravel-saml2/zipball/5045701a07bcd7600a17c92971368669870f546a",
- "reference": "5045701a07bcd7600a17c92971368669870f546a",
- "shasum": ""
- },
- "require": {
- "ext-openssl": "*",
- "illuminate/support": ">=5.0.0",
- "onelogin/php-saml": "^3.0.0",
- "php": ">=5.4.0"
- },
- "require-dev": {
- "mockery/mockery": "0.9.*"
- },
- "type": "library",
- "extra": {
- "laravel": {
- "providers": [
- "Aacotroneo\\Saml2\\Saml2ServiceProvider"
- ],
- "aliases": {
- "Saml2": "Aacotroneo\\Saml2\\Facades\\Saml2Auth"
- }
- }
- },
- "autoload": {
- "psr-0": {
- "Aacotroneo\\Saml2\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "aacotroneo",
- "email": "aacotroneo@gmail.com"
- }
- ],
- "description": "A Laravel package for Saml2 integration as a SP (service provider) based on OneLogin toolkit, which is much lightweight than simplesamlphp",
- "homepage": "https://github.com/aacotroneo/laravel-saml2",
- "keywords": [
- "SAML2",
- "laravel",
- "onelogin",
- "saml"
- ],
- "time": "2018-11-08T14:03:58+00:00"
- },
{
"name": "aws/aws-sdk-php",
- "version": "3.86.2",
+ "version": "3.112.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
+ "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
- "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
+ "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
- "ext-spl": "*",
"guzzlehttp/guzzle": "^5.3.3|^6.2.1",
"guzzlehttp/promises": "~1.0",
"guzzlehttp/psr7": "^1.4.1",
@@ -101,7 +41,8 @@
"ext-sockets": "*",
"nette/neon": "^2.3",
"phpunit/phpunit": "^4.8.35|^5.4.3",
- "psr/cache": "^1.0"
+ "psr/cache": "^1.0",
+ "psr/simple-cache": "^1.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
@@ -146,25 +87,25 @@
"s3",
"sdk"
],
- "time": "2019-01-18T21:10:44+00:00"
+ "time": "2019-09-12T18:09:53+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
- "version": "v0.8.3",
+ "version": "v0.8.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
- "reference": "46781d0304277845a19c09c169bc595fd182cce4"
+ "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/46781d0304277845a19c09c169bc595fd182cce4",
- "reference": "46781d0304277845a19c09c169bc595fd182cce4",
+ "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
+ "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^0.8",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/support": "^5.5|^6",
"php": ">=7"
},
"type": "library",
@@ -202,25 +143,25 @@
"laravel",
"pdf"
],
- "time": "2018-08-31T13:25:44+00:00"
+ "time": "2019-08-23T14:30:33+00:00"
},
{
"name": "barryvdh/laravel-snappy",
- "version": "v0.4.3",
+ "version": "v0.4.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-snappy.git",
- "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478"
+ "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
- "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
+ "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/9be767fc7a082665a84945f36c70b0cbead91ce9",
+ "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9",
"shasum": ""
},
"require": {
- "illuminate/filesystem": "5.5.x|5.6.x|5.7.x",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/filesystem": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
+ "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
"knplabs/knp-snappy": "^1",
"php": ">=7"
},
@@ -254,7 +195,7 @@
"email": "barryvdh@gmail.com"
}
],
- "description": "Snappy PDF/Image for Laravel 4",
+ "description": "Snappy PDF/Image for Laravel",
"keywords": [
"image",
"laravel",
@@ -263,7 +204,7 @@
"wkhtmltoimage",
"wkhtmltopdf"
],
- "time": "2018-09-06T10:14:15+00:00"
+ "time": "2019-08-30T16:12:23+00:00"
},
{
"name": "cogpowered/finediff",
@@ -316,103 +257,40 @@
],
"time": "2014-05-19T10:25:02+00:00"
},
- {
- "name": "doctrine/annotations",
- "version": "v1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/annotations.git",
- "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
- "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
- "shasum": ""
- },
- "require": {
- "doctrine/lexer": "1.*",
- "php": "^5.6 || ^7.0"
- },
- "require-dev": {
- "doctrine/cache": "1.*",
- "phpunit/phpunit": "^5.7"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Benjamin Eberlei",
- "email": "kontakt@beberlei.de"
- },
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
- },
- {
- "name": "Jonathan Wage",
- "email": "jonwage@gmail.com"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
- }
- ],
- "description": "Docblock Annotations Parser",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "annotations",
- "docblock",
- "parser"
- ],
- "time": "2017-02-24T16:22:25+00:00"
- },
{
"name": "doctrine/cache",
- "version": "v1.6.2",
+ "version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57",
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57",
"shasum": ""
},
"require": {
- "php": "~5.5|~7.0"
+ "php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
- "phpunit/phpunit": "~4.8|~5.0",
- "predis/predis": "~1.0",
- "satooshi/php-coveralls": "~0.6"
+ "alcaeus/mongo-php-adapter": "^1.1",
+ "doctrine/coding-standard": "^4.0",
+ "mongodb/mongodb": "^1.1",
+ "phpunit/phpunit": "^7.0",
+ "predis/predis": "~1.0"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6.x-dev"
+ "dev-master": "1.8.x-dev"
}
},
"autoload": {
@@ -447,43 +325,57 @@
}
],
"description": "Caching library offering an object-oriented API for many cache backends",
- "homepage": "http://www.doctrine-project.org",
+ "homepage": "https://www.doctrine-project.org",
"keywords": [
"cache",
"caching"
],
- "time": "2017-07-22T12:49:21+00:00"
+ "time": "2018-08-21T18:01:43+00:00"
},
{
- "name": "doctrine/collections",
- "version": "v1.4.0",
+ "name": "doctrine/dbal",
+ "version": "v2.9.2",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/collections.git",
- "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
+ "url": "https://github.com/doctrine/dbal.git",
+ "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
- "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
+ "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "doctrine/cache": "^1.0",
+ "doctrine/event-manager": "^1.0",
+ "ext-pdo": "*",
+ "php": "^7.1"
},
"require-dev": {
- "doctrine/coding-standard": "~0.1@dev",
- "phpunit/phpunit": "^5.7"
+ "doctrine/coding-standard": "^5.0",
+ "jetbrains/phpstorm-stubs": "^2018.1.2",
+ "phpstan/phpstan": "^0.10.1",
+ "phpunit/phpunit": "^7.4",
+ "symfony/console": "^2.0.5|^3.0|^4.0",
+ "symfony/phpunit-bridge": "^3.4.5|^4.0.5"
+ },
+ "suggest": {
+ "symfony/console": "For helpful console commands such as SQL execution and import of files."
},
+ "bin": [
+ "bin/doctrine-dbal"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.3.x-dev"
+ "dev-master": "2.9.x-dev",
+ "dev-develop": "3.0.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Collections\\": "lib/"
+ "psr-4": {
+ "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -506,50 +398,50 @@
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
- },
- {
- "name": "Johannes Schmitt",
- "email": "schmittjoh@gmail.com"
}
],
- "description": "Collections Abstraction library",
- "homepage": "http://www.doctrine-project.org",
+ "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+ "homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
- "array",
- "collections",
- "iterator"
+ "abstraction",
+ "database",
+ "dbal",
+ "mysql",
+ "persistence",
+ "pgsql",
+ "php",
+ "queryobject"
],
- "time": "2017-01-03T10:49:41+00:00"
+ "time": "2018-12-31T03:27:51+00:00"
},
{
- "name": "doctrine/common",
- "version": "v2.7.3",
+ "name": "doctrine/event-manager",
+ "version": "v1.0.0",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/common.git",
- "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
- "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3",
"shasum": ""
},
"require": {
- "doctrine/annotations": "1.*",
- "doctrine/cache": "1.*",
- "doctrine/collections": "1.*",
- "doctrine/inflector": "1.*",
- "doctrine/lexer": "1.*",
- "php": "~5.6|~7.0"
+ "php": "^7.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9@dev"
},
"require-dev": {
- "phpunit/phpunit": "^5.4.6"
+ "doctrine/coding-standard": "^4.0",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7.x-dev"
+ "dev-master": "1.0.x-dev"
}
},
"autoload": {
@@ -581,106 +473,37 @@
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
- }
- ],
- "description": "Common Library for Doctrine projects",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "annotations",
- "collections",
- "eventmanager",
- "persistence",
- "spl"
- ],
- "time": "2017-07-22T08:35:12+00:00"
- },
- {
- "name": "doctrine/dbal",
- "version": "v2.5.13",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/dbal.git",
- "reference": "729340d8d1eec8f01bff708e12e449a3415af873"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
- "reference": "729340d8d1eec8f01bff708e12e449a3415af873",
- "shasum": ""
- },
- "require": {
- "doctrine/common": ">=2.4,<2.8-dev",
- "php": ">=5.3.2"
- },
- "require-dev": {
- "phpunit/phpunit": "4.*",
- "symfony/console": "2.*||^3.0"
- },
- "suggest": {
- "symfony/console": "For helpful console commands such as SQL execution and import of files."
- },
- "bin": [
- "bin/doctrine-dbal"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.5.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Doctrine\\DBAL\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
- {
- "name": "Benjamin Eberlei",
- "email": "kontakt@beberlei.de"
- },
- {
- "name": "Guilherme Blanco",
- "email": "guilhermeblanco@gmail.com"
},
{
- "name": "Jonathan Wage",
- "email": "jonwage@gmail.com"
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
}
],
- "description": "Database Abstraction Layer",
- "homepage": "http://www.doctrine-project.org",
+ "description": "Doctrine Event Manager component",
+ "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
- "database",
- "dbal",
- "persistence",
- "queryobject"
+ "event",
+ "eventdispatcher",
+ "eventmanager"
],
- "time": "2017-07-22T20:44:48+00:00"
+ "time": "2018-06-11T11:59:03+00:00"
},
{
"name": "doctrine/inflector",
- "version": "v1.2.0",
+ "version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
- "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a",
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^6.2"
@@ -688,7 +511,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "1.3.x-dev"
}
},
"autoload": {
@@ -730,34 +553,39 @@
"singularize",
"string"
],
- "time": "2017-07-22T12:18:28+00:00"
+ "time": "2018-01-09T20:05:19+00:00"
},
{
"name": "doctrine/lexer",
- "version": "v1.0.1",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": "^7.2"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "phpstan/phpstan": "^0.11.8",
+ "phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.1.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Lexer\\": "lib/"
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -765,26 +593,29 @@
"MIT"
],
"authors": [
- {
- "name": "Roman Borschel",
- "email": "roman@code-factory.org"
- },
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
- "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
- "homepage": "http://www.doctrine-project.org",
+ "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
+ "annotations",
+ "docblock",
"lexer",
- "parser"
+ "parser",
+ "php"
],
- "time": "2014-09-09T13:34:57+00:00"
+ "time": "2019-07-30T19:33:28+00:00"
},
{
"name": "dompdf/dompdf",
@@ -853,40 +684,34 @@
"time": "2018-12-14T02:40:31+00:00"
},
{
- "name": "egulias/email-validator",
- "version": "2.1.7",
+ "name": "dragonmantank/cron-expression",
+ "version": "v2.3.0",
"source": {
"type": "git",
- "url": "https://github.com/egulias/EmailValidator.git",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+ "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
"shasum": ""
},
"require": {
- "doctrine/lexer": "^1.0.1",
- "php": ">= 5.5"
+ "php": "^7.0"
},
"require-dev": {
- "dominicsayers/isemail": "dev-master",
- "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
- "satooshi/php-coveralls": "^1.0.1"
- },
- "suggest": {
- "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+ "phpunit/phpunit": "^6.4|^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.3-dev"
}
},
"autoload": {
"psr-4": {
- "Egulias\\EmailValidator\\": "EmailValidator"
+ "Cron\\": "src/Cron/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -895,41 +720,102 @@
],
"authors": [
{
- "name": "Eduardo Gulias Davis"
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Chris Tankersley",
+ "email": "chris@ctankersley.com",
+ "homepage": "https://github.com/dragonmantank"
}
],
- "description": "A library for validating emails against several RFCs",
- "homepage": "https://github.com/egulias/EmailValidator",
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": [
- "email",
- "emailvalidation",
- "emailvalidator",
- "validation",
- "validator"
+ "cron",
+ "schedule"
],
- "time": "2018-12-04T22:38:24+00:00"
+ "time": "2019-03-31T00:38:28+00:00"
},
{
- "name": "erusev/parsedown",
- "version": "1.7.1",
+ "name": "egulias/email-validator",
+ "version": "2.1.11",
"source": {
"type": "git",
- "url": "https://github.com/erusev/parsedown.git",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
+ "url": "https://github.com/egulias/EmailValidator.git",
+ "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
+ "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
"shasum": ""
},
"require": {
- "ext-mbstring": "*",
- "php": ">=5.3.0"
+ "doctrine/lexer": "^1.0.1",
+ "php": ">= 5.5"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35"
- },
+ "dominicsayers/isemail": "dev-master",
+ "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
+ "satooshi/php-coveralls": "^1.0.1",
+ "symfony/phpunit-bridge": "^4.4@dev"
+ },
+ "suggest": {
+ "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Egulias\\EmailValidator\\": "EmailValidator"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eduardo Gulias Davis"
+ }
+ ],
+ "description": "A library for validating emails against several RFCs",
+ "homepage": "https://github.com/egulias/EmailValidator",
+ "keywords": [
+ "email",
+ "emailvalidation",
+ "emailvalidator",
+ "validation",
+ "validator"
+ ],
+ "time": "2019-08-13T17:33:27+00:00"
+ },
+ {
+ "name": "erusev/parsedown",
+ "version": "1.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/erusev/parsedown.git",
+ "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
+ "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
"type": "library",
"autoload": {
"psr-0": {
@@ -953,36 +839,33 @@
"markdown",
"parser"
],
- "time": "2018-03-08T01:11:30+00:00"
+ "time": "2019-03-17T18:48:37+00:00"
},
{
"name": "fideloper/proxy",
- "version": "3.3.4",
+ "version": "4.2.1",
"source": {
"type": "git",
"url": "https://github.com/fideloper/TrustedProxy.git",
- "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f"
+ "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9cdf6f118af58d89764249bbcc7bb260c132924f",
- "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f",
+ "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/03085e58ec7bee24773fa5a8850751a6e61a7e8a",
+ "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a",
"shasum": ""
},
"require": {
- "illuminate/contracts": "~5.0",
+ "illuminate/contracts": "^5.0|^6.0|^7.0",
"php": ">=5.4.0"
},
"require-dev": {
- "illuminate/http": "~5.0",
- "mockery/mockery": "~0.9.3",
- "phpunit/phpunit": "^5.7"
+ "illuminate/http": "^5.0|^6.0|^7.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^6.0"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.3-dev"
- },
"laravel": {
"providers": [
"Fideloper\\Proxy\\TrustedProxyServiceProvider"
@@ -1010,7 +893,7 @@
"proxy",
"trusted proxy"
],
- "time": "2017-06-15T17:19:42+00:00"
+ "time": "2019-09-03T16:45:42+00:00"
},
{
"name": "gathercontent/htmldiff",
@@ -1179,33 +1062,37 @@
},
{
"name": "guzzlehttp/psr7",
- "version": "1.5.2",
+ "version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "9f83dded91781a01c63574e387eaa769be769115"
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
- "reference": "9f83dded91781a01c63574e387eaa769be769115",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
- "ralouphie/getallheaders": "^2.0.5"
+ "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
+ "ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
+ "suggest": {
+ "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.5-dev"
+ "dev-master": "1.6-dev"
}
},
"autoload": {
@@ -1242,20 +1129,20 @@
"uri",
"url"
],
- "time": "2018-12-04T20:46:45+00:00"
+ "time": "2019-07-01T23:21:34+00:00"
},
{
"name": "intervention/image",
- "version": "2.4.2",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
- "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb"
+ "reference": "39eaef720d082ecc54c64bf54541c55f10db546d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb",
- "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb",
+ "url": "https://api.github.com/repos/Intervention/image/zipball/39eaef720d082ecc54c64bf54541c55f10db546d",
+ "reference": "39eaef720d082ecc54c64bf54541c55f10db546d",
"shasum": ""
},
"require": {
@@ -1312,29 +1199,29 @@
"thumbnail",
"watermark"
],
- "time": "2018-05-29T14:19:03+00:00"
+ "time": "2019-06-24T14:06:31+00:00"
},
{
"name": "knplabs/knp-snappy",
- "version": "v1.0.4",
+ "version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/snappy.git",
- "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850"
+ "reference": "ea037298d3c613454da77ecb9588cf0397d695e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850",
- "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850",
+ "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/ea037298d3c613454da77ecb9588cf0397d695e1",
+ "reference": "ea037298d3c613454da77ecb9588cf0397d695e1",
"shasum": ""
},
"require": {
- "php": ">=5.6",
+ "php": ">=7.1",
"psr/log": "^1.0",
- "symfony/process": "~2.3 || ~3.0 || ~4.0"
+ "symfony/process": "~3.4||~4.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.8.36"
+ "phpunit/phpunit": "~7.4"
},
"suggest": {
"h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
@@ -1346,7 +1233,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
@@ -1378,46 +1265,52 @@
"thumbnail",
"wkhtmltopdf"
],
- "time": "2018-01-22T19:40:51+00:00"
+ "time": "2018-12-14T14:59:37+00:00"
},
{
"name": "laravel/framework",
- "version": "v5.5.44",
+ "version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b"
+ "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
- "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
+ "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
"shasum": ""
},
"require": {
- "doctrine/inflector": "~1.1",
- "erusev/parsedown": "~1.7",
+ "doctrine/inflector": "^1.1",
+ "dragonmantank/cron-expression": "^2.0",
+ "egulias/email-validator": "^2.1.10",
+ "erusev/parsedown": "^1.7",
+ "ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"league/flysystem": "^1.0.8",
- "monolog/monolog": "~1.12",
- "mtdowling/cron-expression": "~1.0",
- "nesbot/carbon": "^1.24.1",
- "php": ">=7.0",
- "psr/container": "~1.0",
+ "monolog/monolog": "^1.12|^2.0",
+ "nesbot/carbon": "^2.0",
+ "opis/closure": "^3.1",
+ "php": "^7.2",
+ "psr/container": "^1.0",
"psr/simple-cache": "^1.0",
- "ramsey/uuid": "~3.0",
- "swiftmailer/swiftmailer": "~6.0",
- "symfony/console": "~3.3",
- "symfony/debug": "~3.3",
- "symfony/finder": "~3.3",
- "symfony/http-foundation": "~3.3",
- "symfony/http-kernel": "~3.3",
- "symfony/process": "~3.3",
- "symfony/routing": "~3.3",
- "symfony/var-dumper": "~3.3",
- "tijsverkoyen/css-to-inline-styles": "~2.2",
- "vlucas/phpdotenv": "~2.2"
+ "ramsey/uuid": "^3.7",
+ "swiftmailer/swiftmailer": "^6.0",
+ "symfony/console": "^4.3.4",
+ "symfony/debug": "^4.3.4",
+ "symfony/finder": "^4.3.4",
+ "symfony/http-foundation": "^4.3.4",
+ "symfony/http-kernel": "^4.3.4",
+ "symfony/process": "^4.3.4",
+ "symfony/routing": "^4.3.4",
+ "symfony/var-dumper": "^4.3.4",
+ "tijsverkoyen/css-to-inline-styles": "^2.2.1",
+ "vlucas/phpdotenv": "^3.3"
+ },
+ "conflict": {
+ "tightenco/collect": "<5.5.33"
},
"replace": {
"illuminate/auth": "self.version",
@@ -1447,44 +1340,49 @@
"illuminate/support": "self.version",
"illuminate/translation": "self.version",
"illuminate/validation": "self.version",
- "illuminate/view": "self.version",
- "tightenco/collect": "<5.5.33"
+ "illuminate/view": "self.version"
},
"require-dev": {
- "aws/aws-sdk-php": "~3.0",
- "doctrine/dbal": "~2.5",
- "filp/whoops": "^2.1.4",
- "mockery/mockery": "~1.0",
- "orchestra/testbench-core": "3.5.*",
- "pda/pheanstalk": "~3.0",
- "phpunit/phpunit": "~6.0",
+ "aws/aws-sdk-php": "^3.0",
+ "doctrine/dbal": "^2.6",
+ "filp/whoops": "^2.4",
+ "guzzlehttp/guzzle": "^6.3",
+ "league/flysystem-cached-adapter": "^1.0",
+ "mockery/mockery": "^1.2.3",
+ "moontoast/math": "^1.1",
+ "orchestra/testbench-core": "^4.0",
+ "pda/pheanstalk": "^4.0",
+ "phpunit/phpunit": "^8.3",
"predis/predis": "^1.1.1",
- "symfony/css-selector": "~3.3",
- "symfony/dom-crawler": "~3.3"
+ "symfony/cache": "^4.3",
+ "true/punycode": "^2.1"
},
"suggest": {
- "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).",
- "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).",
+ "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+ "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
+ "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
+ "ext-memcached": "Required to use the memcache cache driver.",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-posix": "Required to use all features of the queue worker.",
- "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).",
- "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
- "laravel/tinker": "Required to use the tinker console command (~1.0).",
- "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
- "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).",
- "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
- "nexmo/client": "Required to use the Nexmo transport (~1.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 (~3.0).",
- "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).",
- "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).",
- "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)."
+ "ext-redis": "Required to use the Redis cache and queue drivers.",
+ "filp/whoops": "Required for friendly error pages in development (^2.4).",
+ "fzaninotto/faker": "Required to use the eloquent factory builder (^1.4).",
+ "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0).",
+ "laravel/tinker": "Required to use the tinker console command (^1.0).",
+ "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
+ "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
+ "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
+ "moontoast/math": "Required to use ordered UUIDs (^1.1).",
+ "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
+ "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0).",
+ "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
+ "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
+ "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.5-dev"
+ "dev-master": "6.x-dev"
}
},
"autoload": {
@@ -1512,38 +1410,39 @@
"framework",
"laravel"
],
- "time": "2018-10-04T14:51:24+00:00"
+ "time": "2019-09-10T18:46:24+00:00"
},
{
"name": "laravel/socialite",
- "version": "3.0.x-dev",
+ "version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
+ "reference": "f509d06e1e7323997b804c5152874f8aad4508e9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
- "reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/f509d06e1e7323997b804c5152874f8aad4508e9",
+ "reference": "f509d06e1e7323997b804c5152874f8aad4508e9",
"shasum": ""
},
"require": {
+ "ext-json": "*",
"guzzlehttp/guzzle": "~6.0",
- "illuminate/contracts": "~5.4",
- "illuminate/http": "~5.4",
- "illuminate/support": "~5.4",
+ "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
"league/oauth1-client": "~1.0",
- "php": ">=5.6.4"
+ "php": "^7.1.3"
},
"require-dev": {
- "mockery/mockery": "~0.9",
- "phpunit/phpunit": "~4.0|~5.0"
+ "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^7.0|^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
},
"laravel": {
"providers": [
@@ -1575,20 +1474,20 @@
"laravel",
"oauth"
],
- "time": "2018-12-21T14:06:32+00:00"
+ "time": "2019-09-03T15:27:17+00:00"
},
{
"name": "league/flysystem",
- "version": "1.0.49",
+ "version": "1.0.55",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd"
+ "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd",
- "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6",
+ "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6",
"shasum": ""
},
"require": {
@@ -1659,20 +1558,20 @@
"sftp",
"storage"
],
- "time": "2018-11-23T23:41:29+00:00"
+ "time": "2019-08-24T11:17:19+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "1.0.21",
+ "version": "1.0.23",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49"
+ "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/43523fec10a831ea48bedb3277e3f3fa218f4e49",
- "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
+ "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
"shasum": ""
},
"require": {
@@ -1706,7 +1605,7 @@
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
- "time": "2018-10-08T07:53:55+00:00"
+ "time": "2019-06-05T17:18:29+00:00"
},
{
"name": "league/oauth1-client",
@@ -1773,21 +1672,21 @@
},
{
"name": "monolog/monolog",
- "version": "1.24.0",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
- "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/68545165e19249013afd1d6f7485aecff07a2d22",
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22",
"shasum": ""
},
"require": {
- "php": ">=5.3.0",
- "psr/log": "~1.0"
+ "php": "^7.2",
+ "psr/log": "^1.0.1"
},
"provide": {
"psr/log-implementation": "1.0.0"
@@ -1795,33 +1694,36 @@
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
- "graylog2/gelf-php": "~1.0",
- "jakub-onderka/php-parallel-lint": "0.9",
+ "elasticsearch/elasticsearch": "^6.0",
+ "graylog2/gelf-php": "^1.4.2",
+ "jakub-onderka/php-parallel-lint": "^0.9",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
- "phpunit/phpunit": "~4.5",
- "phpunit/phpunit-mock-objects": "2.3.0",
+ "phpspec/prophecy": "^1.6.1",
+ "phpunit/phpunit": "^8.3",
+ "predis/predis": "^1.1",
+ "rollbar/rollbar": "^1.3",
"ruflin/elastica": ">=0.90 <3.0",
- "sentry/sentry": "^0.13",
"swiftmailer/swiftmailer": "^5.3|^6.0"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
- "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
- "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"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",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
- "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
- "sentry/sentry": "Allow sending log messages to a Sentry server"
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.x-dev"
}
},
"autoload": {
@@ -1847,51 +1749,7 @@
"logging",
"psr-3"
],
- "time": "2018-11-05T09:00:11+00:00"
- },
- {
- "name": "mtdowling/cron-expression",
- "version": "v1.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/mtdowling/cron-expression.git",
- "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
- "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.2"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.0|~5.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Cron\\": "src/Cron/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- }
- ],
- "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
- "keywords": [
- "cron",
- "schedule"
- ],
- "time": "2017-01-23T04:29:33+00:00"
+ "time": "2019-08-30T09:56:44+00:00"
},
{
"name": "mtdowling/jmespath.php",
@@ -1950,29 +1808,34 @@
},
{
"name": "nesbot/carbon",
- "version": "1.36.2",
+ "version": "2.24.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
+ "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
- "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/934459c5ac0658bc765ad1e53512c7c77adcac29",
+ "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29",
"shasum": ""
},
"require": {
- "php": ">=5.3.9",
- "symfony/translation": "~2.6 || ~3.0 || ~4.0"
+ "ext-json": "*",
+ "php": "^7.1.8 || ^8.0",
+ "symfony/translation": "^3.4 || ^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7"
- },
- "suggest": {
- "friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
- "phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
+ "friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
+ "kylekatarnls/multi-tester": "^1.1",
+ "phpmd/phpmd": "dev-php-7.1-compatibility",
+ "phpstan/phpstan": "^0.11",
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "squizlabs/php_codesniffer": "^3.4"
},
+ "bin": [
+ "bin/carbon"
+ ],
"type": "library",
"extra": {
"laravel": {
@@ -1983,7 +1846,7 @@
},
"autoload": {
"psr-4": {
- "": "src/"
+ "Carbon\\": "src/Carbon/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1995,66 +1858,81 @@
"name": "Brian Nesbitt",
"email": "brian@nesbot.com",
"homepage": "http://nesbot.com"
+ },
+ {
+ "name": "kylekatarnls",
+ "homepage": "http://github.com/kylekatarnls"
}
],
- "description": "A simple API extension for DateTime.",
+ "description": "A API extension for DateTime that supports 281 different languages.",
"homepage": "http://carbon.nesbot.com",
"keywords": [
"date",
"datetime",
"time"
],
- "time": "2018-12-28T10:07:33+00:00"
+ "time": "2019-08-31T16:37:55+00:00"
},
{
- "name": "onelogin/php-saml",
- "version": "3.2.1",
+ "name": "opis/closure",
+ "version": "3.4.0",
"source": {
"type": "git",
- "url": "https://github.com/onelogin/php-saml.git",
- "reference": "845a6ce39e839ed9e687f80bffb02ffde16a70d0"
+ "url": "https://github.com/opis/closure.git",
+ "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/onelogin/php-saml/zipball/845a6ce39e839ed9e687f80bffb02ffde16a70d0",
- "reference": "845a6ce39e839ed9e687f80bffb02ffde16a70d0",
+ "url": "https://api.github.com/repos/opis/closure/zipball/60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+ "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7",
"shasum": ""
},
"require": {
- "php": ">=5.4",
- "robrichards/xmlseclibs": ">=3.0.3"
+ "php": "^5.4 || ^7.0"
},
"require-dev": {
- "pdepend/pdepend": "^2.5.0",
- "php-coveralls/php-coveralls": "^1.0.2 || ^2.0",
- "phploc/phploc": "^2.1 || ^3.0 || ^4.0",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1",
- "sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0",
- "squizlabs/php_codesniffer": "^3.1.1"
- },
- "suggest": {
- "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs",
- "ext-gettext": "Install gettext and php5-gettext libs to handle translations",
- "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)"
+ "jeremeamia/superclosure": "^2.0",
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
- "OneLogin\\": "src/"
- }
+ "Opis\\Closure\\": "src/"
+ },
+ "files": [
+ "functions.php"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "description": "OneLogin PHP SAML Toolkit",
- "homepage": "https://developers.onelogin.com/saml/php",
- "keywords": [
- "SAML2",
- "onelogin",
- "saml"
+ "authors": [
+ {
+ "name": "Marius Sarca",
+ "email": "marius.sarca@gmail.com"
+ },
+ {
+ "name": "Sorin Sarca",
+ "email": "sarca_sorin@hotmail.com"
+ }
],
- "time": "2019-06-25T10:28:20+00:00"
+ "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.",
+ "homepage": "https://opis.io/closure",
+ "keywords": [
+ "anonymous functions",
+ "closure",
+ "function",
+ "serializable",
+ "serialization",
+ "serialize"
+ ],
+ "time": "2019-09-02T21:07:33+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2140,28 +2018,28 @@
},
{
"name": "phenx/php-svg-lib",
- "version": "v0.3.2",
+ "version": "v0.3.3",
"source": {
"type": "git",
"url": "https://github.com/PhenX/php-svg-lib.git",
- "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c"
+ "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/ccc46ef6340d4b8a4a68047e68d8501ea961442c",
- "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c",
+ "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
+ "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
"shasum": ""
},
"require": {
- "sabberworm/php-css-parser": "8.1.*"
+ "sabberworm/php-css-parser": "^8.3"
},
"require-dev": {
- "phpunit/phpunit": "~5.0"
+ "phpunit/phpunit": "^5.5|^6.5"
},
"type": "library",
"autoload": {
- "psr-0": {
- "Svg\\": "src/"
+ "psr-4": {
+ "Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2176,7 +2054,57 @@
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
- "time": "2018-06-03T10:10:03+00:00"
+ "time": "2019-09-11T20:02:13+00:00"
+ },
+ {
+ "name": "phpoption/phpoption",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/schmittjoh/php-option.git",
+ "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+ "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.7.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "PhpOption\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache2"
+ ],
+ "authors": [
+ {
+ "name": "Johannes M. Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Option Type for PHP",
+ "keywords": [
+ "language",
+ "option",
+ "php",
+ "type"
+ ],
+ "time": "2015-07-25T16:39:46+00:00"
},
{
"name": "predis/predis",
@@ -2424,24 +2352,24 @@
},
{
"name": "ralouphie/getallheaders",
- "version": "2.0.5",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
- "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
- "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
- "php": ">=5.3"
+ "php": ">=5.6"
},
"require-dev": {
- "phpunit/phpunit": "~3.7.0",
- "satooshi/php-coveralls": ">=1.0"
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
@@ -2460,7 +2388,7 @@
}
],
"description": "A polyfill for getallheaders.",
- "time": "2016-02-11T07:05:27+00:00"
+ "time": "2019-03-08T08:55:37+00:00"
},
{
"name": "ramsey/uuid",
@@ -2544,63 +2472,26 @@
],
"time": "2018-07-19T23:38:55+00:00"
},
- {
- "name": "robrichards/xmlseclibs",
- "version": "3.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/robrichards/xmlseclibs.git",
- "reference": "406c68ac9124db033d079284b719958b829cb830"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/406c68ac9124db033d079284b719958b829cb830",
- "reference": "406c68ac9124db033d079284b719958b829cb830",
- "shasum": ""
- },
- "require": {
- "ext-openssl": "*",
- "php": ">= 5.4"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "RobRichards\\XMLSecLibs\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "description": "A PHP library for XML Security",
- "homepage": "https://github.com/robrichards/xmlseclibs",
- "keywords": [
- "security",
- "signature",
- "xml",
- "xmldsig"
- ],
- "time": "2018-11-15T11:59:02+00:00"
- },
{
"name": "sabberworm/php-css-parser",
- "version": "8.1.0",
+ "version": "8.3.0",
"source": {
"type": "git",
"url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
- "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef"
+ "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/850cbbcbe7fbb155387a151ea562897a67e242ef",
- "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef",
+ "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
+ "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
- "phpunit/phpunit": "*"
+ "codacy/coverage": "^1.4",
+ "phpunit/phpunit": "~4.8"
},
"type": "library",
"autoload": {
@@ -2624,7 +2515,7 @@
"parser",
"stylesheet"
],
- "time": "2016-07-19T19:14:21+00:00"
+ "time": "2019-02-22T07:42:52+00:00"
},
{
"name": "socialiteproviders/discord",
@@ -2702,19 +2593,20 @@
},
{
"name": "socialiteproviders/manager",
- "version": "v3.3.4",
+ "version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
- "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
+ "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
- "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
+ "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e3e8e78b9a3060801cd008941a0894a0a0c479e1",
+ "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1",
"shasum": ""
},
"require": {
+ "illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0",
"laravel/socialite": "~3.0|~4.0",
"php": "^5.6 || ^7.0"
},
@@ -2747,10 +2639,14 @@
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
+ },
+ {
+ "name": "Miguel Piedrafita",
+ "email": "soy@miguelpiedrafita.com"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
- "time": "2019-01-16T07:58:54+00:00"
+ "time": "2019-09-09T03:07:52+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@@ -2865,21 +2761,21 @@
},
{
"name": "socialiteproviders/twitch",
- "version": "v3.0.0",
+ "version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Twitch.git",
- "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93"
+ "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
- "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
+ "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/8c19b26ff24c40cc019413042a5492c5ed21a658",
+ "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~3.0"
+ "socialiteproviders/manager": "~2.0 || ~3.0"
},
"type": "library",
"autoload": {
@@ -2898,29 +2794,32 @@
}
],
"description": "Twitch OAuth2 Provider for Laravel Socialite",
- "time": "2017-01-25T09:48:29+00:00"
+ "time": "2018-06-20T10:59:51+00:00"
},
{
"name": "swiftmailer/swiftmailer",
- "version": "v6.1.3",
+ "version": "v6.2.1",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
- "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4"
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8ddcb66ac10c392d3beb54829eef8ac1438595f4",
- "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4",
+ "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
"shasum": ""
},
"require": {
"egulias/email-validator": "~2.0",
- "php": ">=7.0.0"
+ "php": ">=7.0.0",
+ "symfony/polyfill-iconv": "^1.0",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"mockery/mockery": "~0.9.1",
- "symfony/phpunit-bridge": "~3.3@dev"
+ "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses",
@@ -2929,7 +2828,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.1-dev"
+ "dev-master": "6.2-dev"
}
},
"autoload": {
@@ -2957,49 +2856,55 @@
"mail",
"mailer"
],
- "time": "2018-09-11T07:12:52+00:00"
+ "time": "2019-04-21T09:21:45+00:00"
},
{
"name": "symfony/console",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "b0878233cb5c4391347e5495089c7af11b8e6201"
+ "reference": "de63799239b3881b8a08f8481b22348f77ed7b36"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201",
- "reference": "b0878233cb5c4391347e5495089c7af11b8e6201",
+ "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36",
+ "reference": "de63799239b3881b8a08f8481b22348f77ed7b36",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/debug": "~2.8|~3.0",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/service-contracts": "^1.1"
},
"conflict": {
- "symfony/dependency-injection": "<3.3"
+ "symfony/dependency-injection": "<3.4",
+ "symfony/event-dispatcher": "<4.3",
+ "symfony/process": "<3.3"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~3.3",
- "symfony/dependency-injection": "~3.3",
- "symfony/event-dispatcher": "~2.8|~3.0",
- "symfony/filesystem": "~2.8|~3.0",
- "symfony/http-kernel": "~2.8|~3.0",
- "symfony/process": "~2.8|~3.0"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/lock": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
+ "symfony/var-dumper": "^4.3"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
- "symfony/filesystem": "",
+ "symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3026,29 +2931,29 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2017-07-29T21:27:59+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v3.1.10",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d"
+ "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
- "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
+ "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3064,14 +2969,14 @@
"MIT"
],
"authors": [
- {
- "name": "Jean-François Simon",
- "email": "jeanfrancois.simon@sensiolabs.com"
- },
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
+ {
+ "name": "Jean-François Simon",
+ "email": "jeanfrancois.simon@sensiolabs.com"
+ },
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
@@ -3079,36 +2984,36 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
- "time": "2017-01-02T20:31:54+00:00"
+ "time": "2019-08-20T14:07:54+00:00"
},
{
"name": "symfony/debug",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
- "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13"
+ "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
- "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/afcdea44a2e399c1e4b52246ec8d54c715393ced",
+ "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
"psr/log": "~1.0"
},
"conflict": {
- "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+ "symfony/http-kernel": "<3.4"
},
"require-dev": {
- "symfony/http-kernel": "~2.8|~3.0"
+ "symfony/http-kernel": "~3.4|~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3135,34 +3040,41 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
- "time": "2017-07-28T15:27:31+00:00"
+ "time": "2019-08-20T14:27:59+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e"
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e",
- "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3",
+ "symfony/event-dispatcher-contracts": "^1.1"
},
"conflict": {
- "symfony/dependency-injection": "<3.3"
+ "symfony/dependency-injection": "<3.4"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "1.1"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/stopwatch": "~2.8|~3.0"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/http-foundation": "^3.4|^4.0",
+ "symfony/service-contracts": "^1.1",
+ "symfony/stopwatch": "~3.4|~4.0"
},
"suggest": {
"symfony/dependency-injection": "",
@@ -3171,7 +3083,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3198,29 +3110,87 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2017-06-09T14:53:08+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "psr/event-dispatcher": "",
+ "symfony/event-dispatcher-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-06-20T06:46:26+00:00"
},
{
"name": "symfony/finder",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+ "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
+ "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3247,33 +3217,35 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
- "time": "2017-06-01T21:01:25+00:00"
+ "time": "2019-08-14T12:26:46+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031"
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031",
- "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
+ "symfony/mime": "^4.3",
"symfony/polyfill-mbstring": "~1.1"
},
"require-dev": {
- "symfony/expression-language": "~2.8|~3.0"
+ "predis/predis": "~1.0",
+ "symfony/expression-language": "~3.4|~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3300,66 +3272,72 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
- "time": "2017-07-21T11:04:46+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5"
+ "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db10d05f1d95e4168e638db7a81c79616f568ea5",
- "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/5e0fc71be03d52cd00c423061cfd300bd6f92a52",
+ "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
"psr/log": "~1.0",
- "symfony/debug": "~2.8|~3.0",
- "symfony/event-dispatcher": "~2.8|~3.0",
- "symfony/http-foundation": "~3.3"
+ "symfony/debug": "~3.4|~4.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/http-foundation": "^4.1.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-php73": "^1.9"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/dependency-injection": "<3.3",
- "symfony/var-dumper": "<3.3",
+ "symfony/browser-kit": "<4.3",
+ "symfony/config": "<3.4",
+ "symfony/dependency-injection": "<4.3",
+ "symfony/translation": "<4.2",
+ "symfony/var-dumper": "<4.1.1",
"twig/twig": "<1.34|<2.4,>=2"
},
+ "provide": {
+ "psr/log-implementation": "1.0"
+ },
"require-dev": {
"psr/cache": "~1.0",
- "symfony/browser-kit": "~2.8|~3.0",
- "symfony/class-loader": "~2.8|~3.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/console": "~2.8|~3.0",
- "symfony/css-selector": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/dom-crawler": "~2.8|~3.0",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/finder": "~2.8|~3.0",
- "symfony/process": "~2.8|~3.0",
- "symfony/routing": "~2.8|~3.0",
- "symfony/stopwatch": "~2.8|~3.0",
- "symfony/templating": "~2.8|~3.0",
- "symfony/translation": "~2.8|~3.0",
- "symfony/var-dumper": "~3.3"
+ "symfony/browser-kit": "^4.3",
+ "symfony/config": "~3.4|~4.0",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/css-selector": "~3.4|~4.0",
+ "symfony/dependency-injection": "^4.3",
+ "symfony/dom-crawler": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/finder": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
+ "symfony/routing": "~3.4|~4.0",
+ "symfony/stopwatch": "~3.4|~4.0",
+ "symfony/templating": "~3.4|~4.0",
+ "symfony/translation": "~4.2",
+ "symfony/translation-contracts": "^1.1",
+ "symfony/var-dumper": "^4.1.1",
+ "twig/twig": "^1.34|^2.4"
},
"suggest": {
"symfony/browser-kit": "",
- "symfony/class-loader": "",
"symfony/config": "",
"symfony/console": "",
"symfony/dependency-injection": "",
- "symfony/finder": "",
"symfony/var-dumper": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3386,20 +3364,79 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
- "time": "2017-08-01T10:25:59+00:00"
+ "time": "2019-08-26T16:47:42+00:00"
+ },
+ {
+ "name": "symfony/mime",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mime.git",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/987a05df1c6ac259b34008b932551353f4f408df",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10",
+ "symfony/dependency-injection": "~3.4|^4.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mime\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A library to manipulate MIME messages",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mime",
+ "mime-type"
+ ],
+ "time": "2019-08-22T08:16:11+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.10.0",
+ "version": "v1.12.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
"shasum": ""
},
"require": {
@@ -3411,7 +3448,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.9-dev"
+ "dev-master": "1.12-dev"
}
},
"autoload": {
@@ -3427,57 +3464,292 @@
"MIT"
],
"authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-iconv",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-iconv.git",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-iconv": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Iconv\\": ""
},
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
{
- "name": "Gert de Pagter",
- "email": "BackEndTea@gmail.com"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Iconv extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "iconv",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php72": "^1.9"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php72",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php72\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill for ctype functions",
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
- "ctype",
"polyfill",
- "portable"
+ "portable",
+ "shim"
],
- "time": "2018-08-06T14:22:27+00:00"
+ "time": "2019-08-06T08:03:45+00:00"
},
{
- "name": "symfony/polyfill-mbstring",
- "version": "v1.10.0",
+ "name": "symfony/polyfill-php73",
+ "version": "v1.12.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188",
+ "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
- "suggest": {
- "ext-mbstring": "For best performance"
- },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.9-dev"
+ "dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
+ "Symfony\\Polyfill\\Php73\\": ""
},
"files": [
"bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -3494,38 +3766,37 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill for the Mbstring extension",
+ "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
- "mbstring",
"polyfill",
"portable",
"shim"
],
- "time": "2018-09-21T13:07:52+00:00"
+ "time": "2019-08-06T08:03:45+00:00"
},
{
"name": "symfony/process",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a"
+ "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a",
- "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a",
+ "url": "https://api.github.com/repos/symfony/process/zipball/e89969c00d762349f078db1128506f7f3dcc0d4a",
+ "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3552,44 +3823,42 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
- "time": "2017-07-13T13:05:09+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/routing",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26"
+ "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
- "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
+ "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/dependency-injection": "<3.3",
- "symfony/yaml": "<3.3"
+ "symfony/config": "<4.2",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/yaml": "<3.4"
},
"require-dev": {
- "doctrine/annotations": "~1.0",
- "doctrine/common": "~2.2",
+ "doctrine/annotations": "~1.2",
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/http-foundation": "~2.8|~3.0",
- "symfony/yaml": "~3.3"
+ "symfony/config": "~4.2",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/http-foundation": "~3.4|~4.0",
+ "symfony/yaml": "~3.4|~4.0"
},
"suggest": {
"doctrine/annotations": "For using the annotation loader",
"symfony/config": "For using the all-in-one router or any loader",
- "symfony/dependency-injection": "For loading routes from a service",
"symfony/expression-language": "For using expression matching",
"symfony/http-foundation": "For using a Symfony Request object",
"symfony/yaml": "For using the YAML loader"
@@ -3597,7 +3866,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3630,45 +3899,114 @@
"uri",
"url"
],
- "time": "2017-07-21T17:43:13+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+ "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "psr/container": "^1.0"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-08-20T14:44:19+00:00"
},
{
"name": "symfony/translation",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3"
+ "reference": "28498169dd334095fa981827992f3a24d50fed0f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
- "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f",
+ "reference": "28498169dd334095fa981827992f3a24d50fed0f",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^1.1.6"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/yaml": "<3.3"
+ "symfony/config": "<3.4",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/yaml": "<3.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/intl": "^2.8.18|^3.2.5",
- "symfony/yaml": "~3.3"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/finder": "~2.8|~3.0|~4.0",
+ "symfony/http-kernel": "~3.4|~4.0",
+ "symfony/intl": "~3.4|~4.0",
+ "symfony/service-contracts": "^1.1.2",
+ "symfony/var-dumper": "~3.4|~4.0",
+ "symfony/yaml": "~3.4|~4.0"
},
"suggest": {
- "psr/log": "To use logging capability in translator",
+ "psr/log-implementation": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3695,41 +4033,106 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
- "time": "2017-06-24T16:45:30+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+ "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "symfony/translation-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-08-02T12:15:04+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "b2623bccb969ad595c2090f9be498b74670d0663"
+ "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663",
- "reference": "b2623bccb969ad595c2090f9be498b74670d0663",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/641043e0f3e615990a0f29479f9c117e8a6698c6",
+ "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php72": "~1.5"
},
"conflict": {
- "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+ "symfony/console": "<3.4"
},
"require-dev": {
"ext-iconv": "*",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
"twig/twig": "~1.34|~2.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
- "ext-symfony_debug": ""
+ "ext-intl": "To show region name in time zone dump",
+ "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -3763,7 +4166,7 @@
"debug",
"dump"
],
- "time": "2017-07-28T06:06:09+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -3814,28 +4217,30 @@
},
{
"name": "vlucas/phpdotenv",
- "version": "v2.5.2",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
+ "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
- "reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+ "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
"shasum": ""
},
"require": {
- "php": ">=5.3.9"
+ "php": "^5.4 || ^7.0",
+ "phpoption/phpoption": "^1.5",
+ "symfony/polyfill-ctype": "^1.9"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.0"
+ "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev"
+ "dev-master": "3.6-dev"
}
},
"autoload": {
@@ -3848,10 +4253,15 @@
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "graham@alt-three.com",
+ "homepage": "https://gjcampbell.co.uk/"
+ },
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
- "homepage": "http://www.vancelucas.com"
+ "homepage": "https://vancelucas.com/"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
@@ -3860,28 +4270,28 @@
"env",
"environment"
],
- "time": "2018-10-30T17:29:25+00:00"
+ "time": "2019-09-10T21:37:39+00:00"
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
- "version": "v3.2.1",
+ "version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
- "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c"
+ "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c",
- "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/18208d64897ab732f6c04a19b319fe8f1d57a9c0",
+ "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0",
"shasum": ""
},
"require": {
- "illuminate/routing": "5.5.x|5.6.x|5.7.x",
- "illuminate/session": "5.5.x|5.6.x|5.7.x",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/routing": "^5.5|^6",
+ "illuminate/session": "^5.5|^6",
+ "illuminate/support": "^5.5|^6",
"maximebf/debugbar": "~1.15.0",
"php": ">=7.0",
"symfony/debug": "^3|^4",
@@ -3930,46 +4340,43 @@
"profiler",
"webprofiler"
],
- "time": "2018-11-09T08:37:55+00:00"
+ "time": "2019-08-29T07:01:03+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
- "version": "v2.5.3",
+ "version": "v2.6.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
- "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2"
+ "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2",
- "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2",
+ "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
+ "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.0.6",
"composer/composer": "^1.6",
- "illuminate/console": "^5.5,<5.8",
- "illuminate/filesystem": "^5.5,<5.8",
- "illuminate/support": "^5.5,<5.8",
+ "doctrine/dbal": "~2.3",
+ "illuminate/console": "^5.5|^6",
+ "illuminate/filesystem": "^5.5|^6",
+ "illuminate/support": "^5.5|^6",
"php": ">=7"
},
"require-dev": {
- "doctrine/dbal": "~2.3",
- "illuminate/config": "^5.1,<5.8",
- "illuminate/view": "^5.1,<5.8",
+ "illuminate/config": "^5.5|^6",
+ "illuminate/view": "^5.5|^6",
"phpro/grumphp": "^0.14",
"phpunit/phpunit": "4.*",
"scrutinizer/ocular": "~1.1",
"squizlabs/php_codesniffer": "^3"
},
- "suggest": {
- "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
- },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev"
+ "dev-master": "2.6-dev"
},
"laravel": {
"providers": [
@@ -4004,7 +4411,7 @@
"phpstorm",
"sublime"
],
- "time": "2018-12-19T12:12:05+00:00"
+ "time": "2019-09-08T09:56:38+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@@ -4057,25 +4464,25 @@
},
{
"name": "composer/ca-bundle",
- "version": "1.1.3",
+ "version": "1.2.4",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660"
+ "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660",
- "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
+ "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
- "php": "^5.3.2 || ^7.0"
+ "php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
"psr/log": "^1.0",
"symfony/process": "^2.5 || ^3.0 || ^4.0"
},
@@ -4109,20 +4516,20 @@
"ssl",
"tls"
],
- "time": "2018-10-18T06:09:13+00:00"
+ "time": "2019-08-30T08:44:50+00:00"
},
{
"name": "composer/composer",
- "version": "1.8.0",
+ "version": "1.9.0",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "d8aef3af866b28786ce9b8647e52c42496436669"
+ "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669",
- "reference": "d8aef3af866b28786ce9b8647e52c42496436669",
+ "url": "https://api.github.com/repos/composer/composer/zipball/314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
+ "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
"shasum": ""
},
"require": {
@@ -4158,7 +4565,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.8-dev"
+ "dev-master": "1.9-dev"
}
},
"autoload": {
@@ -4182,27 +4589,27 @@
"homepage": "http://seld.be"
}
],
- "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
"homepage": "https://getcomposer.org/",
"keywords": [
"autoload",
"dependency",
"package"
],
- "time": "2018-12-03T09:31:16+00:00"
+ "time": "2019-08-02T18:55:33+00:00"
},
{
"name": "composer/semver",
- "version": "1.4.2",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+ "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
- "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+ "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
+ "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
"shasum": ""
},
"require": {
@@ -4244,153 +4651,318 @@
"homepage": "http://robbast.nl"
}
],
- "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "time": "2019-03-19T17:25:45+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7ac1e6aec371357df067f8a688c3d6974df68fa5",
+ "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Spdx\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "SPDX licenses list and validation library.",
+ "keywords": [
+ "license",
+ "spdx",
+ "validator"
+ ],
+ "time": "2019-07-29T10:31:59+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f",
+ "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "time": "2019-05-27T17:52:04+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
- "semantic",
- "semver",
- "validation",
- "versioning"
+ "constructor",
+ "instantiate"
],
- "time": "2016-08-30T16:08:34+00:00"
+ "time": "2019-03-17T17:37:11+00:00"
},
{
- "name": "composer/spdx-licenses",
- "version": "1.5.0",
+ "name": "facade/flare-client-php",
+ "version": "1.0.4",
"source": {
"type": "git",
- "url": "https://github.com/composer/spdx-licenses.git",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2"
+ "url": "https://github.com/facade/flare-client-php.git",
+ "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
+ "url": "https://api.github.com/repos/facade/flare-client-php/zipball/7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
+ "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0"
+ "facade/ignition-contracts": "~1.0",
+ "illuminate/pipeline": "~5.5|~5.6|~5.7|~5.8|^6.0",
+ "php": "^7.1",
+ "symfony/http-foundation": "~3.3|~4.1",
+ "symfony/var-dumper": "^3.4|^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
- "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ "larapack/dd": "^1.1",
+ "phpunit/phpunit": "^7.0",
+ "spatie/phpunit-snapshot-assertions": "^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
- "Composer\\Spdx\\": "src"
- }
+ "Facade\\FlareClient\\": "src"
+ },
+ "files": [
+ "src/helpers.php"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "Nils Adermann",
- "email": "naderman@naderman.de",
- "homepage": "http://www.naderman.de"
- },
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano@seld.be",
- "homepage": "http://seld.be"
- },
- {
- "name": "Rob Bast",
- "email": "rob.bast@gmail.com",
- "homepage": "http://robbast.nl"
- }
- ],
- "description": "SPDX licenses list and validation library.",
+ "description": "Send PHP errors to Flare",
+ "homepage": "https://github.com/facade/flare-client-php",
"keywords": [
- "license",
- "spdx",
- "validator"
+ "exception",
+ "facade",
+ "flare",
+ "reporting"
],
- "time": "2018-11-01T09:45:54+00:00"
+ "time": "2019-09-11T14:19:56+00:00"
},
{
- "name": "composer/xdebug-handler",
- "version": "1.3.1",
+ "name": "facade/ignition",
+ "version": "1.6.5",
"source": {
"type": "git",
- "url": "https://github.com/composer/xdebug-handler.git",
- "reference": "dc523135366eb68f22268d069ea7749486458562"
+ "url": "https://github.com/facade/ignition.git",
+ "reference": "97244f6d511332f3574acab8242c09ddcfda892b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
- "reference": "dc523135366eb68f22268d069ea7749486458562",
+ "url": "https://api.github.com/repos/facade/ignition/zipball/97244f6d511332f3574acab8242c09ddcfda892b",
+ "reference": "97244f6d511332f3574acab8242c09ddcfda892b",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0",
- "psr/log": "^1.0"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "facade/flare-client-php": "^1.0.4",
+ "facade/ignition-contracts": "^1.0",
+ "filp/whoops": "^2.4",
+ "illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0",
+ "monolog/monolog": "^1.12 || ^2.0",
+ "php": "^7.1",
+ "scrivo/highlight.php": "^9.15",
+ "symfony/console": "^3.4 || ^4.0",
+ "symfony/var-dumper": "^3.4 || ^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+ "friendsofphp/php-cs-fixer": "^2.14",
+ "mockery/mockery": "^1.2",
+ "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
+ },
+ "suggest": {
+ "laravel/telescope": "^2.0"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Facade\\Ignition\\IgnitionServiceProvider"
+ ],
+ "aliases": {
+ "Flare": "Facade\\Ignition\\Facades\\Flare"
+ }
+ }
+ },
"autoload": {
"psr-4": {
- "Composer\\XdebugHandler\\": "src"
+ "Facade\\Ignition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "John Stevenson",
- "email": "john-stevenson@blueyonder.co.uk"
- }
- ],
- "description": "Restarts a process without xdebug.",
+ "description": "A beautiful error page for Laravel applications.",
+ "homepage": "https://github.com/facade/ignition",
"keywords": [
- "Xdebug",
- "performance"
+ "error",
+ "flare",
+ "laravel",
+ "page"
],
- "time": "2018-11-29T10:59:02+00:00"
+ "time": "2019-09-13T13:38:04+00:00"
},
{
- "name": "doctrine/instantiator",
- "version": "1.0.5",
+ "name": "facade/ignition-contracts",
+ "version": "1.0.0",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+ "url": "https://github.com/facade/ignition-contracts.git",
+ "reference": "f445db0fb86f48e205787b2592840dd9c80ded28"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/f445db0fb86f48e205787b2592840dd9c80ded28",
+ "reference": "f445db0fb86f48e205787b2592840dd9c80ded28",
"shasum": ""
},
"require": {
- "php": ">=5.3,<8.0-DEV"
- },
- "require-dev": {
- "athletic/athletic": "~0.1.8",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpunit/phpunit": "~4.0",
- "squizlabs/php_codesniffer": "~2.0"
+ "php": "^7.1"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
"autoload": {
"psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ "Facade\\IgnitionContracts\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -4399,31 +4971,33 @@
],
"authors": [
{
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com",
- "homepage": "http://ocramius.github.com/"
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://flareapp.io",
+ "role": "Developer"
}
],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://github.com/doctrine/instantiator",
+ "description": "Solution contracts for Ignition",
+ "homepage": "https://github.com/facade/ignition-contracts",
"keywords": [
- "constructor",
- "instantiate"
+ "contracts",
+ "flare",
+ "ignition"
],
- "time": "2015-06-14T21:17:01+00:00"
+ "time": "2019-08-30T14:06:08+00:00"
},
{
"name": "filp/whoops",
- "version": "2.3.1",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7"
+ "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
- "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96",
+ "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96",
"shasum": ""
},
"require": {
@@ -4457,8 +5031,8 @@
"authors": [
{
"name": "Filipe Dobreira",
- "homepage": "https://github.com/filp",
- "role": "Developer"
+ "role": "Developer",
+ "homepage": "https://github.com/filp"
}
],
"description": "php error handling for cool kids",
@@ -4471,7 +5045,7 @@
"throwable",
"whoops"
],
- "time": "2018-10-23T09:00:00+00:00"
+ "time": "2019-08-07T09:00:00+00:00"
},
{
"name": "fzaninotto/faker",
@@ -4571,6 +5145,94 @@
],
"time": "2016-01-20T08:20:44+00:00"
},
+ {
+ "name": "jakub-onderka/php-console-color",
+ "version": "v0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Color.git",
+ "reference": "d5deaecff52a0d61ccb613bb3804088da0307191"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191",
+ "reference": "d5deaecff52a0d61ccb613bb3804088da0307191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "1.0",
+ "jakub-onderka/php-parallel-lint": "1.0",
+ "jakub-onderka/php-var-dump-check": "0.*",
+ "phpunit/phpunit": "~4.3",
+ "squizlabs/php_codesniffer": "1.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "JakubOnderka\\PhpConsoleColor\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "jakub.onderka@gmail.com"
+ }
+ ],
+ "time": "2018-09-29T17:23:10+00:00"
+ },
+ {
+ "name": "jakub-onderka/php-console-highlighter",
+ "version": "v0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git",
+ "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547",
+ "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "jakub-onderka/php-console-color": "~0.2",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "~1.0",
+ "jakub-onderka/php-parallel-lint": "~1.0",
+ "jakub-onderka/php-var-dump-check": "~0.1",
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "~1.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "JakubOnderka\\PhpConsoleHighlighter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "acci@acci.cz",
+ "homepage": "http://www.acci.cz/"
+ }
+ ],
+ "description": "Highlight PHP code in terminal",
+ "time": "2018-09-29T18:48:56+00:00"
+ },
{
"name": "justinrainbow/json-schema",
"version": "5.2.8",
@@ -4639,28 +5301,41 @@
},
{
"name": "laravel/browser-kit-testing",
- "version": "v2.0.1",
+ "version": "v5.1.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/browser-kit-testing.git",
- "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9"
+ "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
- "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
+ "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
+ "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "phpunit/phpunit": "~6.0",
- "symfony/css-selector": "~3.1",
- "symfony/dom-crawler": "~3.1"
+ "ext-dom": "*",
+ "ext-json": "*",
+ "illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
+ "illuminate/database": "~5.7.0|~5.8.0|^6.0",
+ "illuminate/http": "~5.7.0|~5.8.0|^6.0",
+ "illuminate/support": "~5.7.0|~5.8.0|^6.0",
+ "mockery/mockery": "^1.0",
+ "php": ">=7.1.3",
+ "phpunit/phpunit": "^7.0|^8.0",
+ "symfony/console": "^4.2",
+ "symfony/css-selector": "^4.2",
+ "symfony/dom-crawler": "^4.2",
+ "symfony/http-foundation": "^4.2",
+ "symfony/http-kernel": "^4.2"
+ },
+ "require-dev": {
+ "laravel/framework": "~5.7.0|~5.8.0|^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -4678,12 +5353,12 @@
"email": "taylor@laravel.com"
}
],
- "description": "Provides backwards compatibility for BrowserKit testing in Laravel 5.4.",
+ "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.",
"keywords": [
"laravel",
"testing"
],
- "time": "2017-06-21T11:44:53+00:00"
+ "time": "2019-07-30T14:57:44+00:00"
},
{
"name": "maximebf/debugbar",
@@ -4748,16 +5423,16 @@
},
{
"name": "mockery/mockery",
- "version": "1.2.0",
+ "version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
- "reference": "100633629bf76d57430b86b7098cd6beb996a35a"
+ "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a",
- "reference": "100633629bf76d57430b86b7098cd6beb996a35a",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/4eff936d83eb809bde2c57a3cea0ee9643769031",
+ "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031",
"shasum": ""
},
"require": {
@@ -4766,7 +5441,7 @@
"php": ">=5.6.0"
},
"require-dev": {
- "phpunit/phpunit": "~5.7.10|~6.5|~7.0"
+ "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0"
},
"type": "library",
"extra": {
@@ -4809,29 +5484,32 @@
"test double",
"testing"
],
- "time": "2018-10-02T21:52:37+00:00"
+ "time": "2019-08-07T15:01:07+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.7.0",
+ "version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.1"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
- "phpunit/phpunit": "^4.1"
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"autoload": {
@@ -4854,26 +5532,90 @@
"object",
"object graph"
],
- "time": "2017-10-19T19:58:43+00:00"
+ "time": "2019-08-09T12:45:53+00:00"
+ },
+ {
+ "name": "nunomaduro/collision",
+ "version": "v3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nunomaduro/collision.git",
+ "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+ "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+ "shasum": ""
+ },
+ "require": {
+ "filp/whoops": "^2.1.4",
+ "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
+ "php": "^7.1",
+ "symfony/console": "~2.8|~3.3|~4.0"
+ },
+ "require-dev": {
+ "laravel/framework": "5.8.*",
+ "nunomaduro/larastan": "^0.3.0",
+ "phpstan/phpstan": "^0.11",
+ "phpunit/phpunit": "~8.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "NunoMaduro\\Collision\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Cli error handling for console/command-line PHP applications.",
+ "keywords": [
+ "artisan",
+ "cli",
+ "command-line",
+ "console",
+ "error",
+ "handling",
+ "laravel",
+ "laravel-zero",
+ "php",
+ "symfony"
+ ],
+ "time": "2019-03-07T21:35:13+00:00"
},
{
"name": "phar-io/manifest",
- "version": "1.0.1",
+ "version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
- "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
- "phar-io/version": "^1.0.1",
+ "phar-io/version": "^2.0",
"php": "^5.6 || ^7.0"
},
"type": "library",
@@ -4894,35 +5636,35 @@
"authors": [
{
"name": "Arne Blankerts",
- "email": "arne@blankerts.de",
- "role": "Developer"
+ "role": "Developer",
+ "email": "arne@blankerts.de"
},
{
"name": "Sebastian Heuer",
- "email": "sebastian@phpeople.de",
- "role": "Developer"
+ "role": "Developer",
+ "email": "sebastian@phpeople.de"
},
{
"name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "Developer"
+ "role": "Developer",
+ "email": "sebastian@phpunit.de"
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
- "time": "2017-03-05T18:14:27+00:00"
+ "time": "2018-07-08T19:23:20+00:00"
},
{
"name": "phar-io/version",
- "version": "1.0.1",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
- "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
- "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"shasum": ""
},
"require": {
@@ -4956,39 +5698,37 @@
}
],
"description": "Library for handling version information and constraints",
- "time": "2017-03-05T17:38:23+00:00"
+ "time": "2018-07-08T19:19:57+00:00"
},
{
"name": "phpdocumentor/reflection-common",
- "version": "1.0.1",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
"shasum": ""
},
"require": {
- "php": ">=5.5"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.6"
+ "phpunit/phpunit": "~6"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src"
- ]
+ "phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -5010,30 +5750,30 @@
"reflection",
"static analysis"
],
- "time": "2017-09-11T18:02:19+00:00"
+ "time": "2018-08-07T13:53:10+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "4.3.0",
+ "version": "4.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
- "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
"shasum": ""
},
"require": {
"php": "^7.0",
- "phpdocumentor/reflection-common": "^1.0.0",
- "phpdocumentor/type-resolver": "^0.4.0",
+ "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+ "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
- "doctrine/instantiator": "~1.0.5",
+ "doctrine/instantiator": "^1.0.5",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^6.4"
},
@@ -5061,67 +5801,116 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2017-11-30T07:14:17+00:00"
+ "time": "2019-09-12T14:27:41+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "0.4.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
- "phpdocumentor/reflection-common": "^1.0"
+ "php": "^7.1",
+ "phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
- "mockery/mockery": "^0.9.4",
- "phpunit/phpunit": "^5.2||^4.8.24"
+ "ext-tokenizer": "^7.1",
+ "mockery/mockery": "~1",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src/"
- ]
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "time": "2019-08-22T18:11:29+00:00"
+ },
+ {
+ "name": "phploc/phploc",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phploc.git",
+ "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+ "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2",
+ "sebastian/finder-facade": "^1.1",
+ "sebastian/version": "^2.0",
+ "symfony/console": "^4.0"
+ },
+ "bin": [
+ "phploc"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
}
},
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "Mike van Riel",
- "email": "me@mikevanriel.com"
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
}
],
- "time": "2017-07-14T14:27:02+00:00"
+ "description": "A tool for quickly measuring the size of a PHP project.",
+ "homepage": "https://github.com/sebastianbergmann/phploc",
+ "time": "2019-03-16T10:41:19+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "1.8.0",
+ "version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
"shasum": ""
},
"require": {
@@ -5142,8 +5931,8 @@
}
},
"autoload": {
- "psr-0": {
- "Prophecy\\": "src/"
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -5171,44 +5960,44 @@
"spy",
"stub"
],
- "time": "2018-08-05T17:53:17+00:00"
+ "time": "2019-06-13T12:50:23+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "5.3.2",
+ "version": "7.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
+ "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
- "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
+ "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
- "php": "^7.0",
- "phpunit/php-file-iterator": "^1.4.2",
+ "php": "^7.2",
+ "phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
- "phpunit/php-token-stream": "^2.0.1",
+ "phpunit/php-token-stream": "^3.1.0",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
- "sebastian/environment": "^3.0",
+ "sebastian/environment": "^4.2.2",
"sebastian/version": "^2.0.1",
- "theseer/tokenizer": "^1.1"
+ "theseer/tokenizer": "^1.1.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^8.2.2"
},
"suggest": {
- "ext-xdebug": "^2.5.5"
+ "ext-xdebug": "^2.7.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.3.x-dev"
+ "dev-master": "7.0-dev"
}
},
"autoload": {
@@ -5234,29 +6023,32 @@
"testing",
"xunit"
],
- "time": "2018-04-06T15:36:58+00:00"
+ "time": "2019-07-25T05:31:54+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "1.4.5",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+ "reference": "050bedf145a257b1ff02746c31894800e5122946"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+ "reference": "050bedf145a257b1ff02746c31894800e5122946",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
@@ -5271,7 +6063,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
@@ -5281,7 +6073,7 @@
"filesystem",
"iterator"
],
- "time": "2017-11-27T13:52:08+00:00"
+ "time": "2018-09-13T20:33:42+00:00"
},
{
"name": "phpunit/php-text-template",
@@ -5326,28 +6118,28 @@
},
{
"name": "phpunit/php-timer",
- "version": "1.0.9",
+ "version": "2.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "2.1-dev"
}
},
"autoload": {
@@ -5362,7 +6154,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
@@ -5371,33 +6163,33 @@
"keywords": [
"timer"
],
- "time": "2017-02-26T11:10:40+00:00"
+ "time": "2019-06-07T04:22:29+00:00"
},
{
"name": "phpunit/php-token-stream",
- "version": "2.0.2",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "791198a2c6254db10131eecfe8c06670700904db"
+ "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
- "reference": "791198a2c6254db10131eecfe8c06670700904db",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
+ "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.2.4"
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.1-dev"
}
},
"autoload": {
@@ -5420,57 +6212,56 @@
"keywords": [
"tokenizer"
],
- "time": "2017-11-27T05:48:46+00:00"
+ "time": "2019-07-25T05:29:42+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "6.5.13",
+ "version": "8.3.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
+ "reference": "302faed7059fde575cf3403a78c730c5e3a62750"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
- "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/302faed7059fde575cf3403a78c730c5e3a62750",
+ "reference": "302faed7059fde575cf3403a78c730c5e3a62750",
"shasum": ""
},
"require": {
+ "doctrine/instantiator": "^1.2.0",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
- "myclabs/deep-copy": "^1.6.1",
- "phar-io/manifest": "^1.0.1",
- "phar-io/version": "^1.0",
- "php": "^7.0",
- "phpspec/prophecy": "^1.7",
- "phpunit/php-code-coverage": "^5.3",
- "phpunit/php-file-iterator": "^1.4.3",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.9.1",
+ "phar-io/manifest": "^1.0.3",
+ "phar-io/version": "^2.0.1",
+ "php": "^7.2",
+ "phpspec/prophecy": "^1.8.1",
+ "phpunit/php-code-coverage": "^7.0.7",
+ "phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
- "phpunit/php-timer": "^1.0.9",
- "phpunit/phpunit-mock-objects": "^5.0.9",
- "sebastian/comparator": "^2.1",
- "sebastian/diff": "^2.0",
- "sebastian/environment": "^3.1",
- "sebastian/exporter": "^3.1",
- "sebastian/global-state": "^2.0",
+ "phpunit/php-timer": "^2.1.2",
+ "sebastian/comparator": "^3.0.2",
+ "sebastian/diff": "^3.0.2",
+ "sebastian/environment": "^4.2.2",
+ "sebastian/exporter": "^3.1.1",
+ "sebastian/global-state": "^3.0.0",
"sebastian/object-enumerator": "^3.0.3",
- "sebastian/resource-operations": "^1.0",
+ "sebastian/resource-operations": "^2.0.1",
+ "sebastian/type": "^1.1.3",
"sebastian/version": "^2.0.1"
},
- "conflict": {
- "phpdocumentor/reflection-docblock": "3.0.2",
- "phpunit/dbunit": "<3.0"
- },
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
+ "ext-soap": "*",
"ext-xdebug": "*",
- "phpunit/php-invoker": "^1.1"
+ "phpunit/php-invoker": "^2.0.0"
},
"bin": [
"phpunit"
@@ -5478,7 +6269,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.5.x-dev"
+ "dev-master": "8.3-dev"
}
},
"autoload": {
@@ -5504,46 +6295,42 @@
"testing",
"xunit"
],
- "time": "2018-09-08T15:10:43+00:00"
+ "time": "2019-09-14T09:12:03+00:00"
},
{
- "name": "phpunit/phpunit-mock-objects",
- "version": "5.0.10",
+ "name": "scrivo/highlight.php",
+ "version": "v9.15.10.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
+ "url": "https://github.com/scrivo/highlight.php.git",
+ "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
- "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
+ "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/9ad3adb4456dc91196327498dbbce6aa1ba1239e",
+ "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.0.5",
- "php": "^7.0",
- "phpunit/php-text-template": "^1.2.1",
- "sebastian/exporter": "^3.1"
- },
- "conflict": {
- "phpunit/phpunit": "<6.0"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.4"
},
"require-dev": {
- "phpunit/phpunit": "^6.5.11"
+ "phpunit/phpunit": "^4.8|^5.7",
+ "symfony/finder": "^2.8"
},
"suggest": {
- "ext-soap": "*"
+ "ext-dom": "Needed to make use of the features in the utilities namespace"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.0.x-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-0": {
+ "Highlight\\": "",
+ "HighlightUtilities\\": ""
+ },
+ "files": [
+ "HighlightUtilities/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
@@ -5552,19 +6339,30 @@
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Geert Bergman",
+ "role": "Project Author",
+ "homepage": "http://www.scrivo.org/"
+ },
+ {
+ "name": "Vladimir Jimenez",
+ "role": "Contributor",
+ "homepage": "https://allejo.io"
+ },
+ {
+ "name": "Martin Folkers",
+ "role": "Contributor",
+ "homepage": "https://twobrain.io"
}
],
- "description": "Mock Object library for PHPUnit",
- "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+ "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js",
"keywords": [
- "mock",
- "xunit"
+ "code",
+ "highlight",
+ "highlight.js",
+ "highlight.php",
+ "syntax"
],
- "abandoned": true,
- "time": "2018-08-09T05:50:03+00:00"
+ "time": "2019-08-27T04:27:48+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@@ -5613,30 +6411,30 @@
},
{
"name": "sebastian/comparator",
- "version": "2.1.3",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
- "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"shasum": ""
},
"require": {
- "php": "^7.0",
- "sebastian/diff": "^2.0 || ^3.0",
+ "php": "^7.1",
+ "sebastian/diff": "^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.4"
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -5673,32 +6471,33 @@
"compare",
"equality"
],
- "time": "2018-02-01T13:46:46+00:00"
+ "time": "2018-07-12T15:12:46+00:00"
},
{
"name": "sebastian/diff",
- "version": "2.0.1",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
- "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.2"
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -5723,34 +6522,40 @@
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
- "diff"
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
],
- "time": "2017-08-03T08:09:46+00:00"
+ "time": "2019-02-04T06:01:07+00:00"
},
{
"name": "sebastian/environment",
- "version": "3.1.0",
+ "version": "4.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
- "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.1"
+ "phpunit/phpunit": "^7.5"
+ },
+ "suggest": {
+ "ext-posix": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1.x-dev"
+ "dev-master": "4.2-dev"
}
},
"autoload": {
@@ -5775,20 +6580,20 @@
"environment",
"hhvm"
],
- "time": "2017-07-01T08:51:00+00:00"
+ "time": "2019-05-05T09:05:15+00:00"
},
{
"name": "sebastian/exporter",
- "version": "3.1.0",
+ "version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
- "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
"shasum": ""
},
"require": {
@@ -5815,6 +6620,10 @@
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@@ -5823,17 +6632,13 @@
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
@@ -5842,27 +6647,69 @@
"export",
"exporter"
],
- "time": "2017-04-03T13:19:02+00:00"
+ "time": "2019-09-14T09:02:43+00:00"
+ },
+ {
+ "name": "sebastian/finder-facade",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/finder-facade.git",
+ "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+ "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+ "shasum": ""
+ },
+ "require": {
+ "symfony/finder": "~2.3|~3.0|~4.0",
+ "theseer/fdomdocument": "~1.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
+ "homepage": "https://github.com/sebastianbergmann/finder-facade",
+ "time": "2017-11-18T17:31:49+00:00"
},
{
"name": "sebastian/global-state",
- "version": "2.0.0",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+ "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+ "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.2",
+ "sebastian/object-reflector": "^1.1.1",
+ "sebastian/recursion-context": "^3.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "ext-dom": "*",
+ "phpunit/phpunit": "^8.0"
},
"suggest": {
"ext-uopz": "*"
@@ -5870,7 +6717,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -5893,7 +6740,7 @@
"keywords": [
"global state"
],
- "time": "2017-04-27T15:39:26+00:00"
+ "time": "2019-02-01T05:30:01+00:00"
},
{
"name": "sebastian/object-enumerator",
@@ -6042,25 +6889,25 @@
},
{
"name": "sebastian/resource-operations",
- "version": "1.0.0",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"shasum": ""
},
"require": {
- "php": ">=5.6.0"
+ "php": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -6080,7 +6927,53 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2015-07-28T20:34:47+00:00"
+ "time": "2018-10-04T04:07:39+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
+ "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "time": "2019-07-02T08:10:15+00:00"
},
{
"name": "sebastian/version",
@@ -6220,16 +7113,16 @@
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.4.0",
+ "version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "379deb987e26c7cd103a7b387aea178baec96e48"
+ "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
- "reference": "379deb987e26c7cd103a7b387aea178baec96e48",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
+ "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
"shasum": ""
},
"require": {
@@ -6262,33 +7155,38 @@
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "http://www.squizlabs.com/php-codesniffer",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
- "time": "2018-12-19T23:57:18+00:00"
+ "time": "2019-04-10T23:49:02+00:00"
},
{
"name": "symfony/dom-crawler",
- "version": "v3.1.10",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "7eede2a901a19928494194f7d1815a77b9a473a0"
+ "reference": "cc686552948d627528c0e2e759186dff67c2610e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0",
- "reference": "7eede2a901a19928494194f7d1815a77b9a473a0",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cc686552948d627528c0e2e759186dff67c2610e",
+ "reference": "cc686552948d627528c0e2e759186dff67c2610e",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
+ "conflict": {
+ "masterminds/html5": "<2.6"
+ },
"require-dev": {
- "symfony/css-selector": "~2.8|~3.0"
+ "masterminds/html5": "^2.6",
+ "symfony/css-selector": "~3.4|~4.0"
},
"suggest": {
"symfony/css-selector": ""
@@ -6296,7 +7194,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -6323,29 +7221,30 @@
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
- "time": "2017-01-21T17:13:55+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
@@ -6372,20 +7271,60 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
- "time": "2017-07-11T07:17:58+00:00"
+ "time": "2019-08-20T14:07:54+00:00"
+ },
+ {
+ "name": "theseer/fdomdocument",
+ "version": "1.6.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/fDOMDocument.git",
+ "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+ "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "lib-libxml": "*",
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "role": "lead",
+ "email": "arne@blankerts.de"
+ }
+ ],
+ "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
+ "homepage": "https://github.com/theseer/fDOMDocument",
+ "time": "2017-06-30T11:53:12+00:00"
},
{
"name": "theseer/tokenizer",
- "version": "1.1.0",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b",
- "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"shasum": ""
},
"require": {
@@ -6407,25 +7346,25 @@
"authors": [
{
"name": "Arne Blankerts",
- "email": "arne@blankerts.de",
- "role": "Developer"
+ "role": "Developer",
+ "email": "arne@blankerts.de"
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
- "time": "2017-04-07T12:08:54+00:00"
+ "time": "2019-06-13T22:48:21+00:00"
},
{
"name": "webmozart/assert",
- "version": "1.4.0",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
"shasum": ""
},
"require": {
@@ -6433,8 +7372,7 @@
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
- "phpunit/phpunit": "^4.6",
- "sebastian/version": "^1.0.1"
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
},
"type": "library",
"extra": {
@@ -6463,28 +7401,89 @@
"check",
"validate"
],
- "time": "2018-12-25T11:19:39+00:00"
+ "time": "2019-08-24T08:43:50+00:00"
+ },
+ {
+ "name": "wnx/laravel-stats",
+ "version": "v2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stefanzweifel/laravel-stats.git",
+ "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+ "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "~5.8.0|^6.0",
+ "illuminate/support": "~5.8.0|^6.0",
+ "php": ">=7.2.0",
+ "phploc/phploc": "~4.0|~5.0",
+ "symfony/finder": "~3.3|~4.0"
+ },
+ "require-dev": {
+ "laravel/browser-kit-testing": "~2.0|~3.0|~4.0|~5.0",
+ "laravel/dusk": "~3.0|~4.0|~5.0",
+ "mockery/mockery": "^1.1",
+ "orchestra/testbench": "^3.8",
+ "phpunit/phpunit": "6.*|7.*|8.*"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Wnx\\LaravelStats\\StatsServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Wnx\\LaravelStats\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Stefan Zweifel",
+ "email": "hello@stefanzweifel.io",
+ "homepage": "https://stefanzweifel.io",
+ "role": "Developer"
+ }
+ ],
+ "description": "Get insights about your Laravel Project",
+ "homepage": "https://github.com/stefanzweifel/laravel-stats",
+ "keywords": [
+ "laravel",
+ "statistics",
+ "stats",
+ "wnx"
+ ],
+ "time": "2019-09-01T14:18:49+00:00"
}
],
"aliases": [],
- "minimum-stability": "stable",
- "stability-flags": {
- "laravel/socialite": 20
- },
- "prefer-stable": false,
+ "minimum-stability": "dev",
+ "stability-flags": [],
+ "prefer-stable": true,
"prefer-lowest": false,
"platform": {
- "php": ">=7.0.5",
- "ext-json": "*",
- "ext-tidy": "*",
+ "php": "^7.2",
+ "ext-curl": "*",
"ext-dom": "*",
- "ext-xml": "*",
- "ext-mbstring": "*",
"ext-gd": "*",
- "ext-curl": "*"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-tidy": "*",
+ "ext-xml": "*"
},
"platform-dev": [],
"platform-overrides": {
- "php": "7.0.5"
+ "php": "7.2.0"
}
}
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 000000000..634a5772f
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,7 @@
+project_identifier: bookstack
+base_path: .
+preserve_hierarchy: false
+files:
+ - source: /resources/lang/en/*.php
+ translation: /resources/lang/%two_letters_code%/%original_file_name%
+ type: php
diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php
index de6b0b276..ddf3c295d 100644
--- a/database/factories/ModelFactory.php
+++ b/database/factories/ModelFactory.php
@@ -15,8 +15,8 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
- 'password' => str_random(10),
- 'remember_token' => str_random(10),
+ 'password' => Str::random(10),
+ 'remember_token' => Str::random(10),
'email_confirmed' => 1
];
});
@@ -24,7 +24,7 @@ $factory->define(\BookStack\Auth\User::class, function ($faker) {
$factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
@@ -32,7 +32,7 @@ $factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
$factory->define(\BookStack\Entities\Book::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
@@ -40,7 +40,7 @@ $factory->define(\BookStack\Entities\Book::class, function ($faker) {
$factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
@@ -49,7 +49,7 @@ $factory->define(\BookStack\Entities\Page::class, function ($faker) {
$html = '' . implode('
', $faker->paragraphs(5)) . '
';
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'html' => $html,
'text' => strip_tags($html),
'revision_count' => 1
diff --git a/database/migrations/2019_07_07_112515_add_template_support.php b/database/migrations/2019_07_07_112515_add_template_support.php
new file mode 100644
index 000000000..a54508198
--- /dev/null
+++ b/database/migrations/2019_07_07_112515_add_template_support.php
@@ -0,0 +1,54 @@
+boolean('template')->default(false);
+ $table->index('template');
+ });
+
+ // Create new templates-manage permission and assign to admin role
+ $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+ $permissionId = DB::table('role_permissions')->insertGetId([
+ 'name' => 'templates-manage',
+ 'display_name' => 'Manage Page Templates',
+ 'created_at' => Carbon::now()->toDateTimeString(),
+ 'updated_at' => Carbon::now()->toDateTimeString()
+ ]);
+ DB::table('permission_role')->insert([
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId
+ ]);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropColumn('template');
+ });
+
+ // Remove templates-manage permission
+ $templatesManagePermission = DB::table('role_permissions')
+ ->where('name', '=', 'templates_manage')->first();
+
+ DB::table('permission_role')->where('permission_id', '=', $templatesManagePermission->id)->delete();
+ DB::table('role_permissions')->where('name', '=', 'templates_manage')->delete();
+ }
+}
diff --git a/database/migrations/2019_08_17_140214_add_user_invites_table.php b/database/migrations/2019_08_17_140214_add_user_invites_table.php
new file mode 100644
index 000000000..23bd6988c
--- /dev/null
+++ b/database/migrations/2019_08_17_140214_add_user_invites_table.php
@@ -0,0 +1,33 @@
+increments('id');
+ $table->integer('user_id')->index();
+ $table->string('token')->index();
+ $table->nullableTimestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('user_invites');
+ }
+}
diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php
index 988ea2100..d86cb0ddd 100644
--- a/database/seeds/DatabaseSeeder.php
+++ b/database/seeds/DatabaseSeeder.php
@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;
class DatabaseSeeder extends Seeder
{
/**
- * Run the database seeds.
+ * Seed the application's database.
*
* @return void
*/
diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php
index ce3cd1307..deb1aa11c 100644
--- a/database/seeds/DummyContentSeeder.php
+++ b/database/seeds/DummyContentSeeder.php
@@ -1,6 +1,14 @@
create();
- $editorRole = \BookStack\Auth\Role::getRole('editor');
+ $editorUser = factory(User::class)->create();
+ $editorRole = Role::getRole('editor');
$editorUser->attachRole($editorRole);
// Create a viewer user
- $viewerUser = factory(\BookStack\Auth\User::class)->create();
- $role = \BookStack\Auth\Role::getRole('viewer');
+ $viewerUser = factory(User::class)->create();
+ $role = Role::getRole('viewer');
$viewerUser->attachRole($role);
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
factory(\BookStack\Entities\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
- $chapters = factory(\BookStack\Entities\Chapter::class, 3)->create($byData)
+ $chapters = factory(Chapter::class, 3)->create($byData)
->each(function($chapter) use ($editorUser, $book, $byData){
- $pages = factory(\BookStack\Entities\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
+ $pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
- $pages = factory(\BookStack\Entities\Page::class, 3)->make($byData);
+ $pages = factory(Page::class, 3)->make($byData);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
- $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
- $pages = factory(\BookStack\Entities\Page::class, 200)->make($byData);
- $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make($byData);
+ $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+ $pages = factory(Page::class, 200)->make($byData);
+ $chapters = factory(Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
- $shelves = factory(\BookStack\Entities\Bookshelf::class, 10)->create($byData);
+ $shelves = factory(Bookshelf::class, 10)->create($byData);
$largeBook->shelves()->attach($shelves->pluck('id'));
- app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
- app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+ app(PermissionService::class)->buildJointPermissions();
+ app(SearchService::class)->indexAllEntities();
}
}
diff --git a/database/seeds/LargeContentSeeder.php b/database/seeds/LargeContentSeeder.php
index 136b6cb6a..4db10395a 100644
--- a/database/seeds/LargeContentSeeder.php
+++ b/database/seeds/LargeContentSeeder.php
@@ -1,6 +1,13 @@
create();
- $editorRole = \BookStack\Auth\Role::getRole('editor');
+ $editorUser = factory(User::class)->create();
+ $editorRole = Role::getRole('editor');
$editorUser->attachRole($editorRole);
- $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
- $pages = factory(\BookStack\Entities\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
- $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $pages = factory(Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $chapters = factory(Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
- app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
- app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+ app(PermissionService::class)->buildJointPermissions();
+ app(SearchService::class)->indexAllEntities();
}
}
diff --git a/dev/docker/Dockerfile b/dev/docker/Dockerfile
new file mode 100644
index 000000000..8816615cf
--- /dev/null
+++ b/dev/docker/Dockerfile
@@ -0,0 +1,16 @@
+FROM php:7.3-apache
+
+ENV APACHE_DOCUMENT_ROOT /app/public
+WORKDIR /app
+
+RUN apt-get update -y \
+ && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+ && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
+ && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
+ && a2enmod rewrite \
+ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
+ && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
+ && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
+ && php composer-setup.php \
+ && mv composer.phar /usr/bin/composer \
+ && php -r "unlink('composer-setup.php');"
diff --git a/dev/docker/entrypoint.app.sh b/dev/docker/entrypoint.app.sh
new file mode 100755
index 000000000..ff44f0c8d
--- /dev/null
+++ b/dev/docker/entrypoint.app.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -e
+
+env
+
+if [[ -n "$1" ]]; then
+ exec "$@"
+else
+ wait-for-it db:3306 -t 45
+ php artisan migrate --database=mysql
+ chown -R www-data:www-data storage
+ exec apache2-foreground
+fi
\ No newline at end of file
diff --git a/dev/docker/entrypoint.node.sh b/dev/docker/entrypoint.node.sh
new file mode 100755
index 000000000..e59e1e8a0
--- /dev/null
+++ b/dev/docker/entrypoint.node.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+set -e
+
+npm install
+npm rebuild node-sass
+
+exec npm run watch
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..ea7a61ab5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,48 @@
+# This is a Docker Compose configuration
+# intended for development purposes only
+
+version: '3'
+
+volumes:
+ db: {}
+
+services:
+ db:
+ image: mysql:8
+ environment:
+ MYSQL_DATABASE: bookstack-test
+ MYSQL_USER: bookstack-test
+ MYSQL_PASSWORD: bookstack-test
+ MYSQL_RANDOM_ROOT_PASSWORD: 'true'
+ command: --default-authentication-plugin=mysql_native_password
+ volumes:
+ - db:/var/lib/mysql
+ app:
+ build:
+ context: .
+ dockerfile: ./dev/docker/Dockerfile
+ environment:
+ DB_CONNECTION: mysql
+ DB_HOST: db
+ DB_PORT: 3306
+ DB_DATABASE: bookstack-test
+ DB_USERNAME: bookstack-test
+ DB_PASSWORD: bookstack-test
+ MAIL_DRIVER: smtp
+ MAIL_HOST: mailhog
+ MAIL_PORT: 1025
+ ports:
+ - ${DEV_PORT:-8080}:80
+ volumes:
+ - ./:/app
+ entrypoint: /app/dev/docker/entrypoint.app.sh
+ node:
+ image: node:alpine
+ working_dir: /app
+ volumes:
+ - ./:/app
+ entrypoint: /app/dev/docker/entrypoint.node.sh
+ mailhog:
+ image: mailhog/mailhog
+ ports:
+ - ${DEV_MAIL_PORT:-8025}:8025
diff --git a/package-lock.json b/package-lock.json
index 352ad5ce9..47afc27a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -833,6 +833,7 @@
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
+ "fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
@@ -1908,6 +1909,554 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
+ "fsevents": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+ "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "nan": "^2.12.1",
+ "node-pre-gyp": "^0.12.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minipass": {
+ "version": "2.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.2.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.3.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^4.1.0",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.12.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
+ }
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.4.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.7.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "yallist": {
+ "version": "3.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
@@ -2682,9 +3231,9 @@
}
},
"lodash": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash.tail": {
@@ -2979,9 +3528,9 @@
}
},
"mixin-deep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
- "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true,
"requires": {
"for-in": "^1.0.2",
@@ -4250,9 +4799,9 @@
"dev": true
},
"set-value": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
- "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
@@ -5008,38 +5557,15 @@
"dev": true
},
"union-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
- "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true,
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
- "set-value": "^0.4.3"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "set-value": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
- "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.1",
- "to-object-path": "^0.3.0"
- }
- }
+ "set-value": "^2.0.1"
}
},
"uniq": {
diff --git a/phpunit.xml b/phpunit.xml
index 53722a71b..9f83e95ff 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -7,8 +7,7 @@
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
- stopOnFailure="false"
- syntaxCheck="false">
+ stopOnFailure="false">
./tests/
@@ -20,32 +19,34 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/.htaccess b/public/.htaccess
index 8eb2dd0dd..0d55354ec 100644
--- a/public/.htaccess
+++ b/public/.htaccess
@@ -1,6 +1,6 @@
- Options -MultiViews
+ Options -MultiViews -Indexes
RewriteEngine On
diff --git a/public/uploads/.gitignore b/public/uploads/.gitignore
index c96a04f00..cb7328e19 100755
--- a/public/uploads/.gitignore
+++ b/public/uploads/.gitignore
@@ -1,2 +1,3 @@
*
-!.gitignore
\ No newline at end of file
+!.gitignore
+!.htaccess
\ No newline at end of file
diff --git a/public/uploads/.htaccess b/public/uploads/.htaccess
new file mode 100755
index 000000000..45552cb63
--- /dev/null
+++ b/public/uploads/.htaccess
@@ -0,0 +1 @@
+Options -Indexes
\ No newline at end of file
diff --git a/public/web.config b/public/web.config
new file mode 100644
index 000000000..474eb6898
--- /dev/null
+++ b/public/web.config
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 276a3de20..ca90be305 100644
--- a/readme.md
+++ b/readme.md
@@ -2,7 +2,8 @@
[](https://github.com/BookStackApp/BookStack/releases/latest)
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
-[](https://travis-ci.org/BookStackApp/BookStack)
+[](https://github.com/BookStackApp/BookStack/actions)
+[](https://discord.gg/ztkBqR2)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
@@ -12,7 +13,7 @@ A platform for storing and organising information and documentation. General inf
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
* [BookStack Blog](https://www.bookstackapp.com/blog)
-## Project Definition
+## ð Project Definition
BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
@@ -20,7 +21,7 @@ BookStack is not designed as an extensible platform to be used for purposes that
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
-## Road Map
+## ð£ï¸ Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
@@ -33,7 +34,7 @@ Below is a high-level road map view for BookStack to provide a sense of directio
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
-## Release Versioning & Process
+## ð Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v..`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
@@ -41,7 +42,7 @@ Each BookStack release will have a [milestone](https://github.com/BookStackApp/B
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
-## Development & Testing
+## ð ï¸ Development & Testing
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
@@ -74,7 +75,37 @@ php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
Once done you can run `php vendor/bin/phpunit` in the application root directory to run all tests.
-## Translations
+### ð Code Standards
+
+PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+
+### ð Development using Docker
+
+This repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets.
+
+To get started, make sure you meet the following requirements:
+
+- Docker and Docker Compose are installed
+- Your user is part of the `docker` group
+
+If all the conditions are met, you can proceed with the following steps:
+
+1. Install PHP/Composer dependencies with **`docker-compose run app composer install`** (first time can take a while because the image has to be built).
+2. **Copy `.env.example` to `.env`** and change `APP_KEY` to a random 32 char string.
+3. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.
+4. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.
+5. **Run `docker-compose up`** and wait until all database migrations have been done.
+6. You can now login with `admin@admin.com` and `password` as password on `localhost:8080` (or another port if specified).
+
+If needed, You'll be able to run any artisan commands via docker-compose like so:
+
+ ```shell script
+docker-compose run app php artisan list
+```
+
+The docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable.
+
+## ð Translations
All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
@@ -93,42 +124,35 @@ php resources/lang/check.php pt_BR
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
-## Contributing & Maintenance
+## ð Contributing, Issues & Pull Requests
-Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
+Feel free to create issues to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
+
+Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/CODE_OF_CONDUCT.md).
-### Code Standards
+## ð Security
-PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
-### Pull Requests
+If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
-Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
+If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
-Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
+## â¿ Accessibility
-If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
+We want BookStack to remain accessible to as many people as possible. We aim for at least WCAG 2.1 Level A standards where possible although we do not strictly test this upon each release. If you come across any accessibility issues please feel free to open an issue.
-## Website, Docs & Blog
+## ð¥ï¸ Website, Docs & Blog
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
-## Security
-
-Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
-
-If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
-
-If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
-
-
-## License
+## âï¸ License
-The BookStack source is provided under the MIT License.
+The BookStack source is provided under the MIT License. The libraries used by, and included with, BookStack are provided under their own licenses.
-## Attribution
+## ðª Attribution
The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors).
@@ -150,3 +174,4 @@ These are the great open-source projects used to help build BookStack:
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
* [Draw.io](https://github.com/jgraph/drawio)
+* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
\ No newline at end of file
diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js
deleted file mode 100644
index 3887e8432..000000000
--- a/resources/assets/js/components/dropdown.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Dropdown
- * Provides some simple logic to create simple dropdown menus.
- */
-class DropDown {
-
- constructor(elem) {
- this.container = elem;
- this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
- this.moveMenu = elem.hasAttribute('dropdown-move-menu');
- this.toggle = elem.querySelector('[dropdown-toggle]');
- this.body = document.body;
- this.setupListeners();
- }
-
- show(event) {
- this.hideAll();
-
- this.menu.style.display = 'block';
- this.menu.classList.add('anim', 'menuIn');
-
- if (this.moveMenu) {
- // Move to body to prevent being trapped within scrollable sections
- this.rect = this.menu.getBoundingClientRect();
- this.body.appendChild(this.menu);
- this.menu.style.position = 'fixed';
- this.menu.style.left = `${this.rect.left}px`;
- this.menu.style.top = `${this.rect.top}px`;
- this.menu.style.width = `${this.rect.width}px`;
- }
-
- // Set listener to hide on mouse leave or window click
- this.menu.addEventListener('mouseleave', this.hide.bind(this));
- window.addEventListener('click', event => {
- if (!this.menu.contains(event.target)) {
- this.hide();
- }
- });
-
- // Focus on first input if existing
- let input = this.menu.querySelector('input');
- if (input !== null) input.focus();
-
- event.stopPropagation();
- }
-
- hideAll() {
- for (let dropdown of window.components.dropdown) {
- dropdown.hide();
- }
- }
-
- hide() {
- this.menu.style.display = 'none';
- this.menu.classList.remove('anim', 'menuIn');
- if (this.moveMenu) {
- this.menu.style.position = '';
- this.menu.style.left = '';
- this.menu.style.top = '';
- this.menu.style.width = '';
- this.container.appendChild(this.menu);
- }
- }
-
- setupListeners() {
- // Hide menu on option click
- this.container.addEventListener('click', event => {
- let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
- if (possibleChildren.indexOf(event.target) !== -1) this.hide();
- });
- // Show dropdown on toggle click
- this.toggle.addEventListener('click', this.show.bind(this));
- // Hide menu on enter press
- this.container.addEventListener('keypress', event => {
- if (event.keyCode !== 13) return true;
- event.preventDefault();
- this.hide();
- return false;
- });
- }
-
-}
-
-export default DropDown;
\ No newline at end of file
diff --git a/resources/assets/js/services/events.js b/resources/assets/js/services/events.js
deleted file mode 100644
index 1f97d0cb8..000000000
--- a/resources/assets/js/services/events.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Simple global events manager
- */
-class Events {
- constructor() {
- this.listeners = {};
- this.stack = [];
- }
-
- emit(eventName, eventData) {
- this.stack.push({name: eventName, data: eventData});
- if (typeof this.listeners[eventName] === 'undefined') return this;
- let eventsToStart = this.listeners[eventName];
- for (let i = 0; i < eventsToStart.length; i++) {
- let event = eventsToStart[i];
- event(eventData);
- }
- return this;
- }
-
- listen(eventName, callback) {
- if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
- this.listeners[eventName].push(callback);
- return this;
- }
-}
-
-export default Events;
\ No newline at end of file
diff --git a/resources/assets/sass/print-styles.scss b/resources/assets/sass/print-styles.scss
deleted file mode 100644
index 44107f2d4..000000000
--- a/resources/assets/sass/print-styles.scss
+++ /dev/null
@@ -1,42 +0,0 @@
-@import "variables";
-
-header {
- display: none;
-}
-
-body {
- font-size: 12px;
-}
-
-.page-content {
- margin: 0 auto;
-}
-
-.flex-fill {
- display: block;
-}
-
-.flex.sidebar + .flex.content {
- border-left: none;
-}
-
-.print-hidden {
- display: none;
-}
-
-.print-full-width {
- width: 100%;
- float: none;
- display: block;
-}
-
-h2 {
- font-size: 2em;
- line-height: 1;
- margin-top: 0.6em;
- margin-bottom: 0.3em;
-}
-
-.comments-container {
- display: none;
-}
\ No newline at end of file
diff --git a/resources/assets/icons/add-circle.svg b/resources/icons/add-circle.svg
similarity index 100%
rename from resources/assets/icons/add-circle.svg
rename to resources/icons/add-circle.svg
diff --git a/resources/assets/icons/add.svg b/resources/icons/add.svg
similarity index 100%
rename from resources/assets/icons/add.svg
rename to resources/icons/add.svg
diff --git a/resources/assets/icons/attach.svg b/resources/icons/attach.svg
similarity index 100%
rename from resources/assets/icons/attach.svg
rename to resources/icons/attach.svg
diff --git a/resources/assets/icons/auth/azure.svg b/resources/icons/auth/azure.svg
similarity index 100%
rename from resources/assets/icons/auth/azure.svg
rename to resources/icons/auth/azure.svg
diff --git a/resources/assets/icons/auth/discord.svg b/resources/icons/auth/discord.svg
similarity index 100%
rename from resources/assets/icons/auth/discord.svg
rename to resources/icons/auth/discord.svg
diff --git a/resources/assets/icons/auth/facebook.svg b/resources/icons/auth/facebook.svg
similarity index 100%
rename from resources/assets/icons/auth/facebook.svg
rename to resources/icons/auth/facebook.svg
diff --git a/resources/assets/icons/auth/github.svg b/resources/icons/auth/github.svg
similarity index 100%
rename from resources/assets/icons/auth/github.svg
rename to resources/icons/auth/github.svg
diff --git a/resources/assets/icons/auth/gitlab.svg b/resources/icons/auth/gitlab.svg
similarity index 100%
rename from resources/assets/icons/auth/gitlab.svg
rename to resources/icons/auth/gitlab.svg
diff --git a/resources/assets/icons/auth/google.svg b/resources/icons/auth/google.svg
similarity index 100%
rename from resources/assets/icons/auth/google.svg
rename to resources/icons/auth/google.svg
diff --git a/resources/assets/icons/auth/okta.svg b/resources/icons/auth/okta.svg
similarity index 100%
rename from resources/assets/icons/auth/okta.svg
rename to resources/icons/auth/okta.svg
diff --git a/resources/assets/icons/auth/slack.svg b/resources/icons/auth/slack.svg
similarity index 100%
rename from resources/assets/icons/auth/slack.svg
rename to resources/icons/auth/slack.svg
diff --git a/resources/assets/icons/auth/twitch.svg b/resources/icons/auth/twitch.svg
similarity index 100%
rename from resources/assets/icons/auth/twitch.svg
rename to resources/icons/auth/twitch.svg
diff --git a/resources/assets/icons/auth/twitter.svg b/resources/icons/auth/twitter.svg
similarity index 100%
rename from resources/assets/icons/auth/twitter.svg
rename to resources/icons/auth/twitter.svg
diff --git a/resources/assets/icons/back.svg b/resources/icons/back.svg
similarity index 100%
rename from resources/assets/icons/back.svg
rename to resources/icons/back.svg
diff --git a/resources/assets/icons/book.svg b/resources/icons/book.svg
similarity index 100%
rename from resources/assets/icons/book.svg
rename to resources/icons/book.svg
diff --git a/resources/assets/icons/books.svg b/resources/icons/books.svg
similarity index 100%
rename from resources/assets/icons/books.svg
rename to resources/icons/books.svg
diff --git a/resources/assets/icons/bookshelf.svg b/resources/icons/bookshelf.svg
similarity index 100%
rename from resources/assets/icons/bookshelf.svg
rename to resources/icons/bookshelf.svg
diff --git a/resources/assets/icons/cancel.svg b/resources/icons/cancel.svg
similarity index 100%
rename from resources/assets/icons/cancel.svg
rename to resources/icons/cancel.svg
diff --git a/resources/assets/icons/caret-down.svg b/resources/icons/caret-down.svg
similarity index 100%
rename from resources/assets/icons/caret-down.svg
rename to resources/icons/caret-down.svg
diff --git a/resources/assets/icons/caret-left-circle.svg b/resources/icons/caret-left-circle.svg
similarity index 100%
rename from resources/assets/icons/caret-left-circle.svg
rename to resources/icons/caret-left-circle.svg
diff --git a/resources/assets/icons/caret-right-circle.svg b/resources/icons/caret-right-circle.svg
similarity index 100%
rename from resources/assets/icons/caret-right-circle.svg
rename to resources/icons/caret-right-circle.svg
diff --git a/resources/assets/icons/caret-right.svg b/resources/icons/caret-right.svg
similarity index 100%
rename from resources/assets/icons/caret-right.svg
rename to resources/icons/caret-right.svg
diff --git a/resources/assets/icons/chapter.svg b/resources/icons/chapter.svg
similarity index 100%
rename from resources/assets/icons/chapter.svg
rename to resources/icons/chapter.svg
diff --git a/resources/assets/icons/check-circle.svg b/resources/icons/check-circle.svg
similarity index 100%
rename from resources/assets/icons/check-circle.svg
rename to resources/icons/check-circle.svg
diff --git a/resources/assets/icons/check.svg b/resources/icons/check.svg
similarity index 100%
rename from resources/assets/icons/check.svg
rename to resources/icons/check.svg
diff --git a/resources/icons/chevron-down.svg b/resources/icons/chevron-down.svg
new file mode 100644
index 000000000..f08dfafcb
--- /dev/null
+++ b/resources/icons/chevron-down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/assets/icons/chevron-right.svg b/resources/icons/chevron-right.svg
similarity index 100%
rename from resources/assets/icons/chevron-right.svg
rename to resources/icons/chevron-right.svg
diff --git a/resources/assets/icons/chevron-up.svg b/resources/icons/chevron-up.svg
similarity index 100%
rename from resources/assets/icons/chevron-up.svg
rename to resources/icons/chevron-up.svg
diff --git a/resources/assets/icons/close.svg b/resources/icons/close.svg
similarity index 100%
rename from resources/assets/icons/close.svg
rename to resources/icons/close.svg
diff --git a/resources/assets/icons/comment.svg b/resources/icons/comment.svg
similarity index 100%
rename from resources/assets/icons/comment.svg
rename to resources/icons/comment.svg
diff --git a/resources/assets/icons/copy.svg b/resources/icons/copy.svg
similarity index 100%
rename from resources/assets/icons/copy.svg
rename to resources/icons/copy.svg
diff --git a/resources/assets/icons/danger.svg b/resources/icons/danger.svg
similarity index 100%
rename from resources/assets/icons/danger.svg
rename to resources/icons/danger.svg
diff --git a/resources/assets/icons/delete.svg b/resources/icons/delete.svg
similarity index 100%
rename from resources/assets/icons/delete.svg
rename to resources/icons/delete.svg
diff --git a/resources/assets/icons/drawing.svg b/resources/icons/drawing.svg
similarity index 100%
rename from resources/assets/icons/drawing.svg
rename to resources/icons/drawing.svg
diff --git a/resources/assets/icons/edit.svg b/resources/icons/edit.svg
similarity index 100%
rename from resources/assets/icons/edit.svg
rename to resources/icons/edit.svg
diff --git a/resources/assets/icons/expand-text.svg b/resources/icons/expand-text.svg
similarity index 100%
rename from resources/assets/icons/expand-text.svg
rename to resources/icons/expand-text.svg
diff --git a/resources/assets/icons/export.svg b/resources/icons/export.svg
similarity index 100%
rename from resources/assets/icons/export.svg
rename to resources/icons/export.svg
diff --git a/resources/assets/icons/file.svg b/resources/icons/file.svg
similarity index 100%
rename from resources/assets/icons/file.svg
rename to resources/icons/file.svg
diff --git a/resources/assets/icons/folder.svg b/resources/icons/folder.svg
similarity index 100%
rename from resources/assets/icons/folder.svg
rename to resources/icons/folder.svg
diff --git a/resources/assets/icons/grid.svg b/resources/icons/grid.svg
similarity index 100%
rename from resources/assets/icons/grid.svg
rename to resources/icons/grid.svg
diff --git a/resources/assets/icons/grip.svg b/resources/icons/grip.svg
similarity index 100%
rename from resources/assets/icons/grip.svg
rename to resources/icons/grip.svg
diff --git a/resources/assets/icons/history.svg b/resources/icons/history.svg
similarity index 100%
rename from resources/assets/icons/history.svg
rename to resources/icons/history.svg
diff --git a/resources/assets/icons/image.svg b/resources/icons/image.svg
similarity index 100%
rename from resources/assets/icons/image.svg
rename to resources/icons/image.svg
diff --git a/resources/assets/icons/images.svg b/resources/icons/images.svg
similarity index 100%
rename from resources/assets/icons/images.svg
rename to resources/icons/images.svg
diff --git a/resources/assets/icons/include.svg b/resources/icons/include.svg
similarity index 100%
rename from resources/assets/icons/include.svg
rename to resources/icons/include.svg
diff --git a/resources/assets/icons/info-filled.svg b/resources/icons/info-filled.svg
similarity index 100%
rename from resources/assets/icons/info-filled.svg
rename to resources/icons/info-filled.svg
diff --git a/resources/assets/icons/info.svg b/resources/icons/info.svg
similarity index 100%
rename from resources/assets/icons/info.svg
rename to resources/icons/info.svg
diff --git a/resources/assets/icons/link.svg b/resources/icons/link.svg
similarity index 100%
rename from resources/assets/icons/link.svg
rename to resources/icons/link.svg
diff --git a/resources/assets/icons/list.svg b/resources/icons/list.svg
similarity index 100%
rename from resources/assets/icons/list.svg
rename to resources/icons/list.svg
diff --git a/resources/assets/icons/lock-open.svg b/resources/icons/lock-open.svg
similarity index 100%
rename from resources/assets/icons/lock-open.svg
rename to resources/icons/lock-open.svg
diff --git a/resources/assets/icons/lock.svg b/resources/icons/lock.svg
similarity index 100%
rename from resources/assets/icons/lock.svg
rename to resources/icons/lock.svg
diff --git a/resources/assets/icons/login.svg b/resources/icons/login.svg
similarity index 100%
rename from resources/assets/icons/login.svg
rename to resources/icons/login.svg
diff --git a/resources/assets/icons/logout.svg b/resources/icons/logout.svg
similarity index 100%
rename from resources/assets/icons/logout.svg
rename to resources/icons/logout.svg
diff --git a/resources/assets/icons/more.svg b/resources/icons/more.svg
similarity index 100%
rename from resources/assets/icons/more.svg
rename to resources/icons/more.svg
diff --git a/resources/assets/icons/new-user.svg b/resources/icons/new-user.svg
similarity index 100%
rename from resources/assets/icons/new-user.svg
rename to resources/icons/new-user.svg
diff --git a/resources/assets/icons/open-book.svg b/resources/icons/open-book.svg
similarity index 100%
rename from resources/assets/icons/open-book.svg
rename to resources/icons/open-book.svg
diff --git a/resources/assets/icons/page.svg b/resources/icons/page.svg
similarity index 100%
rename from resources/assets/icons/page.svg
rename to resources/icons/page.svg
diff --git a/resources/assets/icons/permission.svg b/resources/icons/permission.svg
similarity index 100%
rename from resources/assets/icons/permission.svg
rename to resources/icons/permission.svg
diff --git a/resources/assets/icons/popular.svg b/resources/icons/popular.svg
similarity index 100%
rename from resources/assets/icons/popular.svg
rename to resources/icons/popular.svg
diff --git a/resources/assets/icons/reply.svg b/resources/icons/reply.svg
similarity index 100%
rename from resources/assets/icons/reply.svg
rename to resources/icons/reply.svg
diff --git a/resources/assets/icons/save.svg b/resources/icons/save.svg
similarity index 100%
rename from resources/assets/icons/save.svg
rename to resources/icons/save.svg
diff --git a/resources/assets/icons/search.svg b/resources/icons/search.svg
similarity index 100%
rename from resources/assets/icons/search.svg
rename to resources/icons/search.svg
diff --git a/resources/assets/icons/settings.svg b/resources/icons/settings.svg
similarity index 100%
rename from resources/assets/icons/settings.svg
rename to resources/icons/settings.svg
diff --git a/resources/assets/icons/sort-down.svg b/resources/icons/sort-down.svg
similarity index 100%
rename from resources/assets/icons/sort-down.svg
rename to resources/icons/sort-down.svg
diff --git a/resources/assets/icons/sort-up.svg b/resources/icons/sort-up.svg
similarity index 100%
rename from resources/assets/icons/sort-up.svg
rename to resources/icons/sort-up.svg
diff --git a/resources/assets/icons/sort.svg b/resources/icons/sort.svg
similarity index 100%
rename from resources/assets/icons/sort.svg
rename to resources/icons/sort.svg
diff --git a/resources/assets/icons/spanner.svg b/resources/icons/spanner.svg
similarity index 100%
rename from resources/assets/icons/spanner.svg
rename to resources/icons/spanner.svg
diff --git a/resources/assets/icons/star-circle.svg b/resources/icons/star-circle.svg
similarity index 100%
rename from resources/assets/icons/star-circle.svg
rename to resources/icons/star-circle.svg
diff --git a/resources/assets/icons/star.svg b/resources/icons/star.svg
similarity index 100%
rename from resources/assets/icons/star.svg
rename to resources/icons/star.svg
diff --git a/resources/assets/icons/swap-vertical.svg b/resources/icons/swap-vertical.svg
similarity index 100%
rename from resources/assets/icons/swap-vertical.svg
rename to resources/icons/swap-vertical.svg
diff --git a/resources/assets/icons/tag.svg b/resources/icons/tag.svg
similarity index 100%
rename from resources/assets/icons/tag.svg
rename to resources/icons/tag.svg
diff --git a/resources/icons/template.svg b/resources/icons/template.svg
new file mode 100644
index 000000000..7c142124f
--- /dev/null
+++ b/resources/icons/template.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/assets/icons/time.svg b/resources/icons/time.svg
similarity index 100%
rename from resources/assets/icons/time.svg
rename to resources/icons/time.svg
diff --git a/resources/assets/icons/user.svg b/resources/icons/user.svg
similarity index 100%
rename from resources/assets/icons/user.svg
rename to resources/icons/user.svg
diff --git a/resources/assets/icons/users-add.svg b/resources/icons/users-add.svg
similarity index 100%
rename from resources/assets/icons/users-add.svg
rename to resources/icons/users-add.svg
diff --git a/resources/assets/icons/users.svg b/resources/icons/users.svg
similarity index 100%
rename from resources/assets/icons/users.svg
rename to resources/icons/users.svg
diff --git a/resources/assets/icons/view.svg b/resources/icons/view.svg
similarity index 100%
rename from resources/assets/icons/view.svg
rename to resources/icons/view.svg
diff --git a/resources/assets/icons/warning.svg b/resources/icons/warning.svg
similarity index 100%
rename from resources/assets/icons/warning.svg
rename to resources/icons/warning.svg
diff --git a/resources/assets/js/components/back-to-top.js b/resources/js/components/back-to-top.js
similarity index 100%
rename from resources/assets/js/components/back-to-top.js
rename to resources/js/components/back-to-top.js
diff --git a/resources/assets/js/components/book-sort.js b/resources/js/components/book-sort.js
similarity index 100%
rename from resources/assets/js/components/book-sort.js
rename to resources/js/components/book-sort.js
diff --git a/resources/assets/js/components/breadcrumb-listing.js b/resources/js/components/breadcrumb-listing.js
similarity index 63%
rename from resources/assets/js/components/breadcrumb-listing.js
rename to resources/js/components/breadcrumb-listing.js
index 11e1522db..7f4344b17 100644
--- a/resources/assets/js/components/breadcrumb-listing.js
+++ b/resources/js/components/breadcrumb-listing.js
@@ -7,35 +7,14 @@ class BreadcrumbListing {
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
- this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
- this.toggleElem.addEventListener('click', this.onShow.bind(this));
+ this.elem.addEventListener('show', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
- this.elem.addEventListener('keydown', this.keyDown.bind(this));
- }
-
- keyDown(event) {
- if (event.key === 'ArrowDown') {
- this.listFocusChange(1);
- event.preventDefault();
- } else if (event.key === 'ArrowUp') {
- this.listFocusChange(-1);
- event.preventDefault();
- }
- }
-
- listFocusChange(indexChange = 1) {
- const links = Array.from(this.entityListElem.querySelectorAll('a:not(.hidden)'));
- const currentFocused = this.entityListElem.querySelector('a:focus');
- const currentFocusedIndex = links.indexOf(currentFocused);
- const defaultFocus = (indexChange > 0) ? links[0] : this.searchInput;
- const nextElem = links[currentFocusedIndex + indexChange] || defaultFocus;
- nextElem.focus();
}
onShow() {
diff --git a/resources/assets/js/components/chapter-toggle.js b/resources/js/components/chapter-toggle.js
similarity index 86%
rename from resources/assets/js/components/chapter-toggle.js
rename to resources/js/components/chapter-toggle.js
index a751206d1..bfd0ac729 100644
--- a/resources/assets/js/components/chapter-toggle.js
+++ b/resources/js/components/chapter-toggle.js
@@ -11,12 +11,14 @@ class ChapterToggle {
open() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.add('open');
+ this.elem.setAttribute('aria-expanded', 'true');
slideDown(list, 240);
}
close() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.remove('open');
+ this.elem.setAttribute('aria-expanded', 'false');
slideUp(list, 240);
}
diff --git a/resources/assets/js/components/collapsible.js b/resources/js/components/collapsible.js
similarity index 84%
rename from resources/assets/js/components/collapsible.js
rename to resources/js/components/collapsible.js
index 40ab32508..464f394c1 100644
--- a/resources/assets/js/components/collapsible.js
+++ b/resources/js/components/collapsible.js
@@ -18,11 +18,13 @@ class Collapsible {
open() {
this.elem.classList.add('open');
+ this.trigger.setAttribute('aria-expanded', 'true');
slideDown(this.content, 300);
}
close() {
this.elem.classList.remove('open');
+ this.trigger.setAttribute('aria-expanded', 'false');
slideUp(this.content, 300);
}
diff --git a/resources/assets/js/components/custom-checkbox.js b/resources/js/components/custom-checkbox.js
similarity index 100%
rename from resources/assets/js/components/custom-checkbox.js
rename to resources/js/components/custom-checkbox.js
diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js
new file mode 100644
index 000000000..4de1e239b
--- /dev/null
+++ b/resources/js/components/dropdown.js
@@ -0,0 +1,152 @@
+import {onSelect} from "../services/dom";
+
+/**
+ * Dropdown
+ * Provides some simple logic to create simple dropdown menus.
+ */
+class DropDown {
+
+ constructor(elem) {
+ this.container = elem;
+ this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
+ this.moveMenu = elem.hasAttribute('dropdown-move-menu');
+ this.toggle = elem.querySelector('[dropdown-toggle]');
+ this.body = document.body;
+ this.showing = false;
+ this.setupListeners();
+ }
+
+ show(event = null) {
+ this.hideAll();
+
+ this.menu.style.display = 'block';
+ this.menu.classList.add('anim', 'menuIn');
+ this.toggle.setAttribute('aria-expanded', 'true');
+
+ if (this.moveMenu) {
+ // Move to body to prevent being trapped within scrollable sections
+ this.rect = this.menu.getBoundingClientRect();
+ this.body.appendChild(this.menu);
+ this.menu.style.position = 'fixed';
+ this.menu.style.left = `${this.rect.left}px`;
+ this.menu.style.top = `${this.rect.top}px`;
+ this.menu.style.width = `${this.rect.width}px`;
+ }
+
+ // Set listener to hide on mouse leave or window click
+ this.menu.addEventListener('mouseleave', this.hide.bind(this));
+ window.addEventListener('click', event => {
+ if (!this.menu.contains(event.target)) {
+ this.hide();
+ }
+ });
+
+ // Focus on first input if existing
+ const input = this.menu.querySelector('input');
+ if (input !== null) input.focus();
+
+ this.showing = true;
+
+ const showEvent = new Event('show');
+ this.container.dispatchEvent(showEvent);
+
+ if (event) {
+ event.stopPropagation();
+ }
+ }
+
+ hideAll() {
+ for (let dropdown of window.components.dropdown) {
+ dropdown.hide();
+ }
+ }
+
+ hide() {
+ this.menu.style.display = 'none';
+ this.menu.classList.remove('anim', 'menuIn');
+ this.toggle.setAttribute('aria-expanded', 'false');
+ if (this.moveMenu) {
+ this.menu.style.position = '';
+ this.menu.style.left = '';
+ this.menu.style.top = '';
+ this.menu.style.width = '';
+ this.container.appendChild(this.menu);
+ }
+ this.showing = false;
+ }
+
+ getFocusable() {
+ return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])'));
+ }
+
+ focusNext() {
+ const focusable = this.getFocusable();
+ const currentIndex = focusable.indexOf(document.activeElement);
+ let newIndex = currentIndex + 1;
+ if (newIndex >= focusable.length) {
+ newIndex = 0;
+ }
+
+ focusable[newIndex].focus();
+ }
+
+ focusPrevious() {
+ const focusable = this.getFocusable();
+ const currentIndex = focusable.indexOf(document.activeElement);
+ let newIndex = currentIndex - 1;
+ if (newIndex < 0) {
+ newIndex = focusable.length - 1;
+ }
+
+ focusable[newIndex].focus();
+ }
+
+ setupListeners() {
+ // Hide menu on option click
+ this.container.addEventListener('click', event => {
+ const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
+ if (possibleChildren.includes(event.target)) {
+ this.hide();
+ }
+ });
+
+ onSelect(this.toggle, event => {
+ event.stopPropagation();
+ this.show(event);
+ if (event instanceof KeyboardEvent) {
+ this.focusNext();
+ }
+ });
+
+ // Keyboard navigation
+ const keyboardNavigation = event => {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
+ this.focusNext();
+ event.preventDefault();
+ } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
+ this.focusPrevious();
+ event.preventDefault();
+ } else if (event.key === 'Escape') {
+ this.hide();
+ this.toggle.focus();
+ event.stopPropagation();
+ }
+ };
+ this.container.addEventListener('keydown', keyboardNavigation);
+ if (this.moveMenu) {
+ this.menu.addEventListener('keydown', keyboardNavigation);
+ }
+
+ // Hide menu on enter press or escape
+ this.menu.addEventListener('keydown ', event => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.hide();
+ }
+ });
+ }
+
+}
+
+export default DropDown;
\ No newline at end of file
diff --git a/resources/assets/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.js
similarity index 90%
rename from resources/assets/js/components/editor-toolbox.js
rename to resources/js/components/editor-toolbox.js
index 10678edfa..354bf0a86 100644
--- a/resources/assets/js/components/editor-toolbox.js
+++ b/resources/js/components/editor-toolbox.js
@@ -23,6 +23,8 @@ class EditorToolbox {
toggle() {
this.elem.classList.toggle('open');
+ const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+ this.toggleButton.setAttribute('aria-expanded', expanded);
}
setActiveTab(tabName, openToolbox = false) {
diff --git a/resources/assets/js/components/entity-permissions-editor.js b/resources/js/components/entity-permissions-editor.js
similarity index 100%
rename from resources/assets/js/components/entity-permissions-editor.js
rename to resources/js/components/entity-permissions-editor.js
diff --git a/resources/assets/js/components/entity-selector-popup.js b/resources/js/components/entity-selector-popup.js
similarity index 100%
rename from resources/assets/js/components/entity-selector-popup.js
rename to resources/js/components/entity-selector-popup.js
diff --git a/resources/assets/js/components/entity-selector.js b/resources/js/components/entity-selector.js
similarity index 100%
rename from resources/assets/js/components/entity-selector.js
rename to resources/js/components/entity-selector.js
diff --git a/resources/assets/js/components/expand-toggle.js b/resources/js/components/expand-toggle.js
similarity index 100%
rename from resources/assets/js/components/expand-toggle.js
rename to resources/js/components/expand-toggle.js
diff --git a/resources/assets/js/components/header-mobile-toggle.js b/resources/js/components/header-mobile-toggle.js
similarity index 100%
rename from resources/assets/js/components/header-mobile-toggle.js
rename to resources/js/components/header-mobile-toggle.js
diff --git a/resources/assets/js/components/homepage-control.js b/resources/js/components/homepage-control.js
similarity index 100%
rename from resources/assets/js/components/homepage-control.js
rename to resources/js/components/homepage-control.js
diff --git a/resources/assets/js/components/image-picker.js b/resources/js/components/image-picker.js
similarity index 100%
rename from resources/assets/js/components/image-picker.js
rename to resources/js/components/image-picker.js
diff --git a/resources/assets/js/components/index.js b/resources/js/components/index.js
similarity index 93%
rename from resources/assets/js/components/index.js
rename to resources/js/components/index.js
index 1c2abd520..14cf08ae2 100644
--- a/resources/assets/js/components/index.js
+++ b/resources/js/components/index.js
@@ -27,6 +27,8 @@ import customCheckbox from "./custom-checkbox";
import bookSort from "./book-sort";
import settingAppColorPicker from "./setting-app-color-picker";
import entityPermissionsEditor from "./entity-permissions-editor";
+import templateManager from "./template-manager";
+import newUserPassword from "./new-user-password";
const componentMapping = {
'dropdown': dropdown,
@@ -57,7 +59,9 @@ const componentMapping = {
'custom-checkbox': customCheckbox,
'book-sort': bookSort,
'setting-app-color-picker': settingAppColorPicker,
- 'entity-permissions-editor': entityPermissionsEditor
+ 'entity-permissions-editor': entityPermissionsEditor,
+ 'template-manager': templateManager,
+ 'new-user-password': newUserPassword,
};
window.components = {};
diff --git a/resources/assets/js/components/list-sort-control.js b/resources/js/components/list-sort-control.js
similarity index 100%
rename from resources/assets/js/components/list-sort-control.js
rename to resources/js/components/list-sort-control.js
diff --git a/resources/assets/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js
similarity index 82%
rename from resources/assets/js/components/markdown-editor.js
rename to resources/js/components/markdown-editor.js
index b0e4d693a..de256a846 100644
--- a/resources/assets/js/components/markdown-editor.js
+++ b/resources/js/components/markdown-editor.js
@@ -18,19 +18,24 @@ class MarkdownEditor {
this.markdown.use(mdTasksLists, {label: true});
this.display = this.elem.querySelector('.markdown-display');
+
+ this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea');
this.htmlInput = this.elem.querySelector('input[name=html]');
this.cm = code.markdownEditor(this.input);
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
- this.init();
- // Scroll to text if needed.
- const queryParams = (new URL(window.location)).searchParams;
- const scrollText = queryParams.get('content-text');
- if (scrollText) {
- this.scrollToText(scrollText);
- }
+ this.display.addEventListener('load', () => {
+ this.displayDoc = this.display.contentDocument;
+ this.init();
+ });
+
+ window.$events.emitPublic(elem, 'editor-markdown::setup', {
+ markdownIt: this.markdown,
+ displayEl: this.display,
+ codeMirrorInstance: this.cm,
+ });
}
init() {
@@ -38,7 +43,7 @@ class MarkdownEditor {
let lastClick = 0;
// Prevent markdown display link click redirect
- this.display.addEventListener('click', event => {
+ this.displayDoc.addEventListener('click', event => {
let isDblClick = Date.now() - lastClick < 300;
let link = event.target.closest('a');
@@ -91,21 +96,49 @@ class MarkdownEditor {
});
this.codeMirrorSetup();
+ this.listenForBookStackEditorEvents();
+
+ // Scroll to text if needed.
+ const queryParams = (new URL(window.location)).searchParams;
+ const scrollText = queryParams.get('content-text');
+ if (scrollText) {
+ this.scrollToText(scrollText);
+ }
}
// Update the input content and render the display.
updateAndRender() {
- let content = this.cm.getValue();
+ const content = this.cm.getValue();
this.input.value = content;
- let html = this.markdown.render(content);
+ const html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
- this.display.innerHTML = html;
+
+ // Set body content
+ this.displayDoc.body.className = 'page-content';
+ this.displayDoc.body.innerHTML = html;
this.htmlInput.value = html;
+
+ // Copy styles from page head and set custom styles for editor
+ this.loadStylesIntoDisplay();
+ }
+
+ loadStylesIntoDisplay() {
+ if (this.displayStylesLoaded) return;
+ this.displayDoc.documentElement.className = 'markdown-editor-display';
+
+ this.displayDoc.head.innerHTML = '';
+ const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
+ for (let style of styles) {
+ const copy = style.cloneNode(true);
+ this.displayDoc.head.appendChild(copy);
+ }
+
+ this.displayStylesLoaded = true;
}
onMarkdownScroll(lineCount) {
- const elems = this.display.children;
+ const elems = this.displayDoc.body.children;
if (elems.length <= lineCount) return;
const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
@@ -199,16 +232,30 @@ class MarkdownEditor {
}
});
- // Handle images on drag-drop
+ // Handle image & content drag n drop
cm.on('drop', (cm, event) => {
- event.stopPropagation();
- event.preventDefault();
- let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
- cm.setCursor(cursorPos);
- if (!event.dataTransfer || !event.dataTransfer.files) return;
- for (let i = 0; i < event.dataTransfer.files.length; i++) {
- uploadImage(event.dataTransfer.files[i]);
+
+ const templateId = event.dataTransfer.getData('bookstack/template');
+ if (templateId) {
+ const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+ cm.setCursor(cursorPos);
+ event.preventDefault();
+ window.$http.get(`/templates/${templateId}`).then(resp => {
+ const content = resp.data.markdown || resp.data.html;
+ cm.replaceSelection(content);
+ });
+ }
+
+ if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
+ const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+ cm.setCursor(cursorPos);
+ event.stopPropagation();
+ event.preventDefault();
+ for (let i = 0; i < event.dataTransfer.files.length; i++) {
+ uploadImage(event.dataTransfer.files[i]);
+ }
}
+
});
// Helper to replace editor content
@@ -461,6 +508,37 @@ class MarkdownEditor {
})
}
+ listenForBookStackEditorEvents() {
+
+ function getContentToInsert({html, markdown}) {
+ return markdown || html;
+ }
+
+ // Replace editor content
+ window.$events.listen('editor::replace', (eventContent) => {
+ const markdown = getContentToInsert(eventContent);
+ this.cm.setValue(markdown);
+ });
+
+ // Append editor content
+ window.$events.listen('editor::append', (eventContent) => {
+ const cursorPos = this.cm.getCursor('from');
+ const markdown = getContentToInsert(eventContent);
+ const content = this.cm.getValue() + '\n' + markdown;
+ this.cm.setValue(content);
+ this.cm.setCursor(cursorPos.line, cursorPos.ch);
+ });
+
+ // Prepend editor content
+ window.$events.listen('editor::prepend', (eventContent) => {
+ const cursorPos = this.cm.getCursor('from');
+ const markdown = getContentToInsert(eventContent);
+ const content = markdown + '\n' + this.cm.getValue();
+ this.cm.setValue(content);
+ const prependLineCount = markdown.split('\n').length;
+ this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
+ });
+ }
}
export default MarkdownEditor ;
diff --git a/resources/js/components/new-user-password.js b/resources/js/components/new-user-password.js
new file mode 100644
index 000000000..9c4c21c14
--- /dev/null
+++ b/resources/js/components/new-user-password.js
@@ -0,0 +1,28 @@
+
+class NewUserPassword {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.inviteOption = elem.querySelector('input[name=send_invite]');
+
+ if (this.inviteOption) {
+ this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
+ this.inviteOptionChange();
+ }
+ }
+
+ inviteOptionChange() {
+ const inviting = (this.inviteOption.value === 'true');
+ const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+ for (const input of passwordBoxes) {
+ input.disabled = inviting;
+ }
+ const container = this.elem.querySelector('#password-input-container');
+ if (container) {
+ container.style.display = inviting ? 'none' : 'block';
+ }
+ }
+
+}
+
+export default NewUserPassword;
\ No newline at end of file
diff --git a/resources/assets/js/components/notification.js b/resources/js/components/notification.js
similarity index 100%
rename from resources/assets/js/components/notification.js
rename to resources/js/components/notification.js
diff --git a/resources/assets/js/components/overlay.js b/resources/js/components/overlay.js
similarity index 73%
rename from resources/assets/js/components/overlay.js
rename to resources/js/components/overlay.js
index 1ba5efcea..ad6a01061 100644
--- a/resources/assets/js/components/overlay.js
+++ b/resources/js/components/overlay.js
@@ -6,12 +6,22 @@ class Overlay {
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
+
+ window.addEventListener('keyup', event => {
+ if (event.key === 'Escape') {
+ this.hide();
+ }
+ });
+
let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
+ hide() { this.toggle(false); }
+ show() { this.toggle(true); }
+
toggle(show = true) {
let start = Date.now();
let duration = 240;
@@ -22,6 +32,9 @@ class Overlay {
this.container.style.opacity = targetOpacity;
if (elapsedTime > duration) {
this.container.style.display = show ? 'flex' : 'none';
+ if (show) {
+ this.focusOnBody();
+ }
this.container.style.opacity = '';
} else {
requestAnimationFrame(setOpacity.bind(this));
@@ -31,8 +44,12 @@ class Overlay {
requestAnimationFrame(setOpacity.bind(this));
}
- hide() { this.toggle(false); }
- show() { this.toggle(true); }
+ focusOnBody() {
+ const body = this.container.querySelector('.popup-body');
+ if (body) {
+ body.focus();
+ }
+ }
}
diff --git a/resources/assets/js/components/page-comments.js b/resources/js/components/page-comments.js
similarity index 99%
rename from resources/assets/js/components/page-comments.js
rename to resources/js/components/page-comments.js
index cabce9139..5d8d16958 100644
--- a/resources/assets/js/components/page-comments.js
+++ b/resources/js/components/page-comments.js
@@ -26,10 +26,12 @@ class PageComments {
handleAction(event) {
let actionElem = event.target.closest('[action]');
+
if (event.target.matches('a[href^="#"]')) {
const id = event.target.href.split('#')[1];
scrollAndHighlightElement(document.querySelector('#' + id));
}
+
if (actionElem === null) return;
event.preventDefault();
diff --git a/resources/assets/js/components/page-display.js b/resources/js/components/page-display.js
similarity index 100%
rename from resources/assets/js/components/page-display.js
rename to resources/js/components/page-display.js
diff --git a/resources/assets/js/components/page-picker.js b/resources/js/components/page-picker.js
similarity index 100%
rename from resources/assets/js/components/page-picker.js
rename to resources/js/components/page-picker.js
diff --git a/resources/assets/js/components/permissions-table.js b/resources/js/components/permissions-table.js
similarity index 100%
rename from resources/assets/js/components/permissions-table.js
rename to resources/js/components/permissions-table.js
diff --git a/resources/assets/js/components/setting-app-color-picker.js b/resources/js/components/setting-app-color-picker.js
similarity index 95%
rename from resources/assets/js/components/setting-app-color-picker.js
rename to resources/js/components/setting-app-color-picker.js
index 064596d86..6c0c0b31d 100644
--- a/resources/assets/js/components/setting-app-color-picker.js
+++ b/resources/js/components/setting-app-color-picker.js
@@ -10,7 +10,7 @@ class SettingAppColorPicker {
this.colorInput.addEventListener('change', this.updateColor.bind(this));
this.colorInput.addEventListener('input', this.updateColor.bind(this));
this.resetButton.addEventListener('click', event => {
- this.colorInput.value = '#0288D1';
+ this.colorInput.value = '#206ea7';
this.updateColor();
});
}
diff --git a/resources/assets/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js
similarity index 100%
rename from resources/assets/js/components/shelf-sort.js
rename to resources/js/components/shelf-sort.js
diff --git a/resources/assets/js/components/sidebar.js b/resources/js/components/sidebar.js
similarity index 100%
rename from resources/assets/js/components/sidebar.js
rename to resources/js/components/sidebar.js
diff --git a/resources/js/components/template-manager.js b/resources/js/components/template-manager.js
new file mode 100644
index 000000000..d004a4307
--- /dev/null
+++ b/resources/js/components/template-manager.js
@@ -0,0 +1,94 @@
+import * as DOM from "../services/dom";
+
+class TemplateManager {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.list = elem.querySelector('[template-manager-list]');
+ this.searching = false;
+
+ // Template insert action buttons
+ DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
+
+ // Template list pagination click
+ DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this));
+
+ // Template list item content click
+ DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
+
+ // Template list item drag start
+ DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
+
+ this.setupSearchBox();
+ }
+
+ handleTemplateItemClick(event, templateItem) {
+ const templateId = templateItem.closest('[template-id]').getAttribute('template-id');
+ this.insertTemplate(templateId, 'replace');
+ }
+
+ handleTemplateItemDragStart(event, templateItem) {
+ const templateId = templateItem.closest('[template-id]').getAttribute('template-id');
+ event.dataTransfer.setData('bookstack/template', templateId);
+ event.dataTransfer.setData('text/plain', templateId);
+ }
+
+ handleTemplateActionClick(event, actionButton) {
+ event.stopPropagation();
+
+ const action = actionButton.getAttribute('template-action');
+ const templateId = actionButton.closest('[template-id]').getAttribute('template-id');
+ this.insertTemplate(templateId, action);
+ }
+
+ async insertTemplate(templateId, action = 'replace') {
+ const resp = await window.$http.get(`/templates/${templateId}`);
+ const eventName = 'editor::' + action;
+ window.$events.emit(eventName, resp.data);
+ }
+
+ async handlePaginationClick(event, paginationLink) {
+ event.preventDefault();
+ const paginationUrl = paginationLink.getAttribute('href');
+ const resp = await window.$http.get(paginationUrl);
+ this.list.innerHTML = resp.data;
+ }
+
+ setupSearchBox() {
+ const searchBox = this.elem.querySelector('.search-box');
+ const input = searchBox.querySelector('input');
+ const submitButton = searchBox.querySelector('button');
+ const cancelButton = searchBox.querySelector('button.search-box-cancel');
+
+ async function performSearch() {
+ const searchTerm = input.value;
+ const resp = await window.$http.get(`/templates`, {
+ search: searchTerm
+ });
+ cancelButton.style.display = searchTerm ? 'block' : 'none';
+ this.list.innerHTML = resp.data;
+ }
+ performSearch = performSearch.bind(this);
+
+ // Searchbox enter press
+ searchBox.addEventListener('keypress', event => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ performSearch();
+ }
+ });
+
+ // Submit button press
+ submitButton.addEventListener('click', event => {
+ performSearch();
+ });
+
+ // Cancel button press
+ cancelButton.addEventListener('click', event => {
+ input.value = '';
+ performSearch();
+ });
+ }
+}
+
+export default TemplateManager;
\ No newline at end of file
diff --git a/resources/assets/js/components/toggle-switch.js b/resources/js/components/toggle-switch.js
similarity index 62%
rename from resources/assets/js/components/toggle-switch.js
rename to resources/js/components/toggle-switch.js
index 3dd1ce85c..b9b96afc5 100644
--- a/resources/assets/js/components/toggle-switch.js
+++ b/resources/js/components/toggle-switch.js
@@ -11,6 +11,11 @@ class ToggleSwitch {
stateChange() {
this.input.value = (this.checkbox.checked ? 'true' : 'false');
+
+ // Dispatch change event from hidden input so they can be listened to
+ // like a normal checkbox.
+ const changeEvent = new Event('change');
+ this.input.dispatchEvent(changeEvent);
}
}
diff --git a/resources/assets/js/components/tri-layout.js b/resources/js/components/tri-layout.js
similarity index 100%
rename from resources/assets/js/components/tri-layout.js
rename to resources/js/components/tri-layout.js
diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js
similarity index 93%
rename from resources/assets/js/components/wysiwyg-editor.js
rename to resources/js/components/wysiwyg-editor.js
index eb9f025a7..60a6743ea 100644
--- a/resources/assets/js/components/wysiwyg-editor.js
+++ b/resources/js/components/wysiwyg-editor.js
@@ -378,6 +378,27 @@ function customHrPlugin() {
}
+function listenForBookStackEditorEvents(editor) {
+
+ // Replace editor content
+ window.$events.listen('editor::replace', ({html}) => {
+ editor.setContent(html);
+ });
+
+ // Append editor content
+ window.$events.listen('editor::append', ({html}) => {
+ const content = editor.getContent() + html;
+ editor.setContent(content);
+ });
+
+ // Prepend editor content
+ window.$events.listen('editor::prepend', ({html}) => {
+ const content = html + editor.getContent();
+ editor.setContent(content);
+ });
+
+}
+
class WysiwygEditor {
constructor(elem) {
@@ -391,6 +412,7 @@ class WysiwygEditor {
this.loadPlugins();
this.tinyMceConfig = this.getTinyMceConfig();
+ window.$events.emitPublic(elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
window.tinymce.init(this.tinyMceConfig);
}
@@ -553,6 +575,10 @@ class WysiwygEditor {
editor.focus();
}
+ listenForBookStackEditorEvents(editor);
+
+ // TODO - Update to standardise across both editors
+ // Use events within listenForBookStackEditorEvents instead (Different event signature)
window.$events.listen('editor-html-update', html => {
editor.setContent(html);
editor.selection.select(editor.getBody(), true);
@@ -583,6 +609,18 @@ class WysiwygEditor {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
+ // Template insertion
+ const templateId = event.dataTransfer.getData('bookstack/template');
+ if (templateId) {
+ event.preventDefault();
+ window.$http.get(`/templates/${templateId}`).then(resp => {
+ editor.selection.setRng(rng);
+ editor.undoManager.transact(function () {
+ editor.execCommand('mceInsertContent', false, resp.data.html);
+ });
+ });
+ }
+
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
@@ -617,6 +655,8 @@ class WysiwygEditor {
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context));
+ // Custom handler hook
+ window.$events.emitPublic(context.elem, 'editor-tinymce::setup', {editor});
}
};
}
diff --git a/resources/assets/js/index.js b/resources/js/index.js
similarity index 100%
rename from resources/assets/js/index.js
rename to resources/js/index.js
diff --git a/resources/assets/js/services/animations.js b/resources/js/services/animations.js
similarity index 79%
rename from resources/assets/js/services/animations.js
rename to resources/js/services/animations.js
index 8a3e9a57b..b6158ea5f 100644
--- a/resources/assets/js/services/animations.js
+++ b/resources/js/services/animations.js
@@ -1,3 +1,10 @@
+/**
+ * Used in the function below to store references of clean-up functions.
+ * Used to ensure only one transitionend function exists at any time.
+ * @type {WeakMap