diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..47c683699 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,2 @@ +>0.25% +not op_mini all \ No newline at end of file diff --git a/.env.example b/.env.example index ccafaf4fb..1005ad208 100644 --- a/.env.example +++ b/.env.example @@ -48,6 +48,7 @@ GITHUB_APP_ID=false GITHUB_APP_SECRET=false GOOGLE_APP_ID=false GOOGLE_APP_SECRET=false +GOOGLE_SELECT_ACCOUNT=false OKTA_BASE_URL=false OKTA_APP_ID=false OKTA_APP_SECRET=false @@ -56,9 +57,16 @@ TWITCH_APP_SECRET=false GITLAB_APP_ID=false GITLAB_APP_SECRET=false GITLAB_BASE_URI=false +DISCORD_APP_ID=false +DISCORD_APP_SECRET=false -# External services such as Gravatar and Draw.IO + +# Disable default services such as Gravatar and Draw.IO DISABLE_EXTERNAL_SERVICES=false +# Use custom avatar service, Sets fetch URL +# Possible placeholders: ${hash} ${size} ${email} +# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option. +# AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon # LDAP Settings LDAP_SERVER=false @@ -67,6 +75,15 @@ LDAP_DN=false LDAP_PASS=false LDAP_USER_FILTER=false LDAP_VERSION=false +# Do you want to sync LDAP groups to BookStack roles for a user +LDAP_USER_TO_GROUPS=false +# What is the LDAP attribute for group memberships +LDAP_GROUP_ATTRIBUTE="memberOf" +# Would you like to remove users from roles on BookStack if they do not match on LDAP +# If false, the ldap groups-roles sync will only add users to roles +LDAP_REMOVE_FROM_GROUPS=false +# Set this option to disable LDAPS Certificate Verification +LDAP_TLS_INSECURE=false # Mail settings MAIL_DRIVER=smtp diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 544bd4e87..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ -### For Feature Requests - -Desired Feature: - -### For Bug Reports - -* BookStack Version *(Found in settings, Please don't put 'latest')*: -* PHP Version: -* MySQL Version: - -##### Expected Behavior - - - -##### Current Behavior - - - -##### Steps to Reproduce - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..8b3d29c2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Steps To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Your Configuration (please complete the following information):** + - Exact BookStack Version (Found in settings): + - PHP Version: + - Hosting Method (Nginx/Apache/Docker): + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..7f38b9cdc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Describe the feature you'd like** +A clear description of the feature you'd like implemented in BookStack. + +**Describe the benefits this feature would bring to BookStack users** +Explain the measurable benefits this feature would achieve. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/app/Activity.php b/app/Actions/Activity.php similarity index 93% rename from app/Activity.php rename to app/Actions/Activity.php index c01da1f6c..1ae1811e1 100644 --- a/app/Activity.php +++ b/app/Actions/Activity.php @@ -1,6 +1,9 @@ permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action) ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count')) @@ -65,7 +66,7 @@ class ViewService if ($filterModel && is_array($filterModel)) { $query->whereIn('viewable_type', $filterModel); } else if ($filterModel) { - $query->where('viewable_type', '=', get_class($filterModel)); + $query->where('viewable_type', '=', $filterModel->getMorphClass()); } return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable'); @@ -89,7 +90,7 @@ class ViewService ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type'); if ($filterModel) { - $query = $query->where('viewable_type', '=', get_class($filterModel)); + $query = $query->where('viewable_type', '=', $filterModel->getMorphClass()); } $query = $query->where('user_id', '=', $user->id); diff --git a/app/Services/EmailConfirmationService.php b/app/Auth/Access/EmailConfirmationService.php similarity index 93% rename from app/Services/EmailConfirmationService.php rename to app/Auth/Access/EmailConfirmationService.php index 9ee69ef1a..4df014116 100644 --- a/app/Services/EmailConfirmationService.php +++ b/app/Auth/Access/EmailConfirmationService.php @@ -1,11 +1,11 @@ -ldap = $ldap; + $this->config = config('services.ldap'); + $this->userRepo = $userRepo; + $this->enabled = config('auth.method') === 'ldap'; + } + + /** + * Check if groups should be synced. + * @return bool + */ + public function shouldSyncGroups() + { + return $this->enabled && $this->config['user_to_groups'] !== false; + } + + /** + * Search for attributes for a specific user on the ldap + * @param string $userName + * @param array $attributes + * @return null|array + * @throws LdapException + */ + private function getUserWithAttributes($userName, $attributes) + { + $ldapConnection = $this->getConnection(); + $this->bindSystemUser($ldapConnection); + + // Find user + $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]); + $baseDn = $this->config['base_dn']; + + $followReferrals = $this->config['follow_referrals'] ? 1 : 0; + $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); + $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes); + if ($users['count'] === 0) { + return null; + } + + return $users[0]; + } + + /** + * Get the details of a user from LDAP using the given username. + * User found via configurable user filter. + * @param $userName + * @return array|null + * @throws LdapException + */ + public function getUserDetails($userName) + { + $emailAttr = $this->config['email_attribute']; + $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]); + + if ($user === null) { + return null; + } + + return [ + 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'], + 'name' => $user['cn'][0], + 'dn' => $user['dn'], + 'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null + ]; + } + + /** + * @param Authenticatable $user + * @param string $username + * @param string $password + * @return bool + * @throws LdapException + */ + public function validateUserCredentials(Authenticatable $user, $username, $password) + { + $ldapUser = $this->getUserDetails($username); + if ($ldapUser === null) { + return false; + } + + if ($ldapUser['uid'] !== $user->external_auth_id) { + return false; + } + + $ldapConnection = $this->getConnection(); + try { + $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password); + } catch (\ErrorException $e) { + $ldapBind = false; + } + + return $ldapBind; + } + + /** + * Bind the system user to the LDAP connection using the given credentials + * otherwise anonymous access is attempted. + * @param $connection + * @throws LdapException + */ + protected function bindSystemUser($connection) + { + $ldapDn = $this->config['dn']; + $ldapPass = $this->config['pass']; + + $isAnonymous = ($ldapDn === false || $ldapPass === false); + if ($isAnonymous) { + $ldapBind = $this->ldap->bind($connection); + } else { + $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); + } + + if (!$ldapBind) { + throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); + } + } + + /** + * Get the connection to the LDAP server. + * Creates a new connection if one does not exist. + * @return resource + * @throws LdapException + */ + protected function getConnection() + { + if ($this->ldapConnection !== null) { + return $this->ldapConnection; + } + + // Check LDAP extension in installed + if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { + throw new LdapException(trans('errors.ldap_extension_not_installed')); + } + + // Get port from server string and protocol if specified. + $ldapServer = explode(':', $this->config['server']); + $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1; + if (!$hasProtocol) { + array_unshift($ldapServer, ''); + } + $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1]; + $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389; + + /* + * Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of + * the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not + * per handle. + */ + if($this->config['tls_insecure']) { + $this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); + } + + $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); + + if ($ldapConnection === false) { + throw new LdapException(trans('errors.ldap_cannot_connect')); + } + + // Set any required options + if ($this->config['version']) { + $this->ldap->setVersion($ldapConnection, $this->config['version']); + } + + $this->ldapConnection = $ldapConnection; + return $this->ldapConnection; + } + + /** + * Build a filter string by injecting common variables. + * @param string $filterString + * @param array $attrs + * @return string + */ + protected function buildFilter($filterString, array $attrs) + { + $newAttrs = []; + foreach ($attrs as $key => $attrText) { + $newKey = '${' . $key . '}'; + $newAttrs[$newKey] = $this->ldap->escape($attrText); + } + return strtr($filterString, $newAttrs); + } + + /** + * Get the groups a user is a part of on ldap + * @param string $userName + * @return array + * @throws LdapException + */ + public function getUserGroups($userName) + { + $groupsAttr = $this->config['group_attribute']; + $user = $this->getUserWithAttributes($userName, [$groupsAttr]); + + if ($user === null) { + return []; + } + + $userGroups = $this->groupFilter($user); + $userGroups = $this->getGroupsRecursive($userGroups, []); + return $userGroups; + } + + /** + * Get the parent groups of an array of groups + * @param array $groupsArray + * @param array $checked + * @return array + * @throws LdapException + */ + private function getGroupsRecursive($groupsArray, $checked) + { + $groups_to_add = []; + foreach ($groupsArray as $groupName) { + if (in_array($groupName, $checked)) { + continue; + } + + $groupsToAdd = $this->getGroupGroups($groupName); + $groups_to_add = array_merge($groups_to_add, $groupsToAdd); + $checked[] = $groupName; + } + $groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR); + + if (!empty($groups_to_add)) { + return $this->getGroupsRecursive($groupsArray, $checked); + } else { + return $groupsArray; + } + } + + /** + * Get the parent groups of a single group + * @param string $groupName + * @return array + * @throws LdapException + */ + private function getGroupGroups($groupName) + { + $ldapConnection = $this->getConnection(); + $this->bindSystemUser($ldapConnection); + + $followReferrals = $this->config['follow_referrals'] ? 1 : 0; + $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); + + $baseDn = $this->config['base_dn']; + $groupsAttr = strtolower($this->config['group_attribute']); + + $groupFilter = 'CN=' . $this->ldap->escape($groupName); + $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]); + if ($groups['count'] === 0) { + return []; + } + + $groupGroups = $this->groupFilter($groups[0]); + return $groupGroups; + } + + /** + * Filter out LDAP CN and DN language in a ldap search return + * Gets the base CN (common name) of the string + * @param array $userGroupSearchResponse + * @return array + */ + protected function groupFilter(array $userGroupSearchResponse) + { + $groupsAttr = strtolower($this->config['group_attribute']); + $ldapGroups = []; + $count = 0; + + if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { + $count = (int) $userGroupSearchResponse[$groupsAttr]['count']; + } + + for ($i=0; $i<$count; $i++) { + $dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1); + if (!in_array($dnComponents[0], $ldapGroups)) { + $ldapGroups[] = $dnComponents[0]; + } + } + + return $ldapGroups; + } + + /** + * Sync the LDAP groups to the user roles for the current user + * @param \BookStack\Auth\User $user + * @param string $username + * @throws LdapException + */ + public function syncGroups(User $user, string $username) + { + $userLdapGroups = $this->getUserGroups($username); + + // Get the ids for the roles from the names + $ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups); + + // Sync groups + if ($this->config['remove_from_groups']) { + $user->roles()->sync($ldapGroupsAsRoles); + $this->userRepo->attachDefaultRole($user); + } else { + $user->roles()->syncWithoutDetaching($ldapGroupsAsRoles); + } + } + + /** + * Match an array of group names from LDAP to BookStack system roles. + * Formats LDAP group names to be lower-case and hyphenated. + * @param array $groupNames + * @return \Illuminate\Support\Collection + */ + protected function matchLdapGroupsToSystemsRoles(array $groupNames) + { + foreach ($groupNames as $i => $groupName) { + $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); + } + + $roles = Role::query()->where(function (Builder $query) use ($groupNames) { + $query->whereIn('name', $groupNames); + foreach ($groupNames as $groupName) { + $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); + } + })->get(); + + $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { + return $this->roleMatchesGroupNames($role, $groupNames); + }); + + return $matchedRoles->pluck('id'); + } + + /** + * Check a role against an array of group names to see if it matches. + * Checked against role 'external_auth_id' if set otherwise the name of the role. + * @param \BookStack\Auth\Role $role + * @param array $groupNames + * @return bool + */ + protected function roleMatchesGroupNames(Role $role, array $groupNames) + { + if ($role->external_auth_id) { + $externalAuthIds = explode(',', strtolower($role->external_auth_id)); + foreach ($externalAuthIds as $externalAuthId) { + if (in_array(trim($externalAuthId), $groupNames)) { + return true; + } + } + return false; + } + + $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); + return in_array($roleName, $groupNames); + } +} diff --git a/app/Services/SocialAuthService.php b/app/Auth/Access/SocialAuthService.php similarity index 75% rename from app/Services/SocialAuthService.php rename to app/Auth/Access/SocialAuthService.php index 02361e59b..0d46b9f88 100644 --- a/app/Services/SocialAuthService.php +++ b/app/Auth/Access/SocialAuthService.php @@ -1,13 +1,12 @@ -validateDriver($socialDriver); - return $this->socialite->driver($driver)->redirect(); + return $this->getSocialDriver($driver)->redirect(); } /** @@ -53,23 +52,18 @@ class SocialAuthService public function startRegister($socialDriver) { $driver = $this->validateDriver($socialDriver); - return $this->socialite->driver($driver)->redirect(); + return $this->getSocialDriver($driver)->redirect(); } /** * Handle the social registration process on callback. - * @param $socialDriver - * @return \Laravel\Socialite\Contracts\User - * @throws SocialDriverNotConfigured + * @param string $socialDriver + * @param SocialUser $socialUser + * @return SocialUser * @throws UserRegistrationException */ - public function handleRegistrationCallback($socialDriver) + public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser) { - $driver = $this->validateDriver($socialDriver); - - // Get user details from social driver - $socialUser = $this->socialite->driver($driver)->user(); - // Check social account has not already been used if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) { throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login'); @@ -84,17 +78,26 @@ class SocialAuthService } /** - * Handle the login process on a oAuth callback. - * @param $socialDriver - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * Get the social user details via the social driver. + * @param string $socialDriver + * @return SocialUser * @throws SocialDriverNotConfigured - * @throws SocialSignInException */ - public function handleLoginCallback($socialDriver) + public function getSocialUser(string $socialDriver) { $driver = $this->validateDriver($socialDriver); - // Get user details from social driver - $socialUser = $this->socialite->driver($driver)->user(); + return $this->socialite->driver($driver)->user(); + } + + /** + * Handle the login process on a oAuth callback. + * @param $socialDriver + * @param SocialUser $socialUser + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws SocialSignInAccountNotUsed + */ + public function handleLoginCallback($socialDriver, SocialUser $socialUser) + { $socialId = $socialUser->getId(); // Get any attached social accounts or users @@ -136,7 +139,7 @@ class SocialAuthService $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]); } - throw new SocialSignInException($message, '/login'); + throw new SocialSignInAccountNotUsed($message, '/login'); } /** @@ -199,8 +202,28 @@ class SocialAuthService } /** - * @param string $socialDriver - * @param \Laravel\Socialite\Contracts\User $socialUser + * Check if the current config for the given driver allows auto-registration. + * @param string $driver + * @return bool + */ + public function driverAutoRegisterEnabled(string $driver) + { + return config('services.' . strtolower($driver) . '.auto_register') === true; + } + + /** + * Check if the current config for the given driver allow email address auto-confirmation. + * @param string $driver + * @return bool + */ + public function driverAutoConfirmEmailEnabled(string $driver) + { + return config('services.' . strtolower($driver) . '.auto_confirm') === true; + } + + /** + * @param string $socialDriver + * @param SocialUser $socialUser * @return SocialAccount */ public function fillSocialAccount($socialDriver, $socialUser) @@ -224,4 +247,20 @@ class SocialAuthService session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)])); return redirect(user()->getEditUrl()); } + + /** + * Provide redirect options per service for the Laravel Socialite driver + * @param $driverName + * @return \Laravel\Socialite\Contracts\Provider + */ + public function getSocialDriver(string $driverName) + { + $driver = $this->socialite->driver($driverName); + + if ($driverName === 'google' && config('services.google.select_account')) { + $driver->with(['prompt' => 'select_account']); + } + + return $driver; + } } diff --git a/app/EntityPermission.php b/app/Auth/Permissions/EntityPermission.php similarity index 84% rename from app/EntityPermission.php rename to app/Auth/Permissions/EntityPermission.php index 0f49e07f5..ef61e03ce 100644 --- a/app/EntityPermission.php +++ b/app/Auth/Permissions/EntityPermission.php @@ -1,4 +1,6 @@ -db = $db; $this->jointPermission = $jointPermission; $this->entityPermission = $entityPermission; $this->role = $role; - $this->book = $book; - $this->chapter = $chapter; - $this->page = $page; - // TODO - Update so admin still goes through filters + $this->entityProvider = $entityProvider; } /** @@ -67,7 +82,7 @@ class PermissionService /** * Prepare the local entity cache and ensure it's empty - * @param Entity[] $entities + * @param \BookStack\Entities\Entity[] $entities */ protected function readyEntityCache($entities = []) { @@ -93,7 +108,7 @@ class PermissionService return $this->entityCache['book']->get($bookId); } - $book = $this->book->find($bookId); + $book = $this->entityProvider->book->find($bookId); if ($book === null) { $book = false; } @@ -104,7 +119,7 @@ class PermissionService /** * Get a chapter via ID, Checks local cache * @param $chapterId - * @return Book + * @return \BookStack\Entities\Book */ protected function getChapter($chapterId) { @@ -112,7 +127,7 @@ class PermissionService return $this->entityCache['chapter']->get($chapterId); } - $chapter = $this->chapter->find($chapterId); + $chapter = $this->entityProvider->chapter->find($chapterId); if ($chapter === null) { $chapter = false; } @@ -159,6 +174,12 @@ class PermissionService $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** @@ -167,13 +188,28 @@ class PermissionService */ protected function bookFetchQuery() { - return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { + return $this->entityProvider->book->newQuery() + ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { $query->select(['id', 'restricted', 'created_by', 'book_id']); }, 'pages' => function ($query) { $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); }]); } + /** + * @param Collection $shelves + * @param array $roles + * @param bool $deleteOld + * @throws \Throwable + */ + protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false) + { + if ($deleteOld) { + $this->deleteManyJointPermissionsForEntities($shelves->all()); + } + $this->createManyJointPermissions($shelves, $roles); + } + /** * Build joint permissions for an array of books * @param Collection $books @@ -203,7 +239,8 @@ class PermissionService /** * Rebuild the entity jointPermissions for a particular entity. - * @param Entity $entity + * @param \BookStack\Entities\Entity $entity + * @throws \Throwable */ public function buildJointPermissionsForEntity(Entity $entity) { @@ -214,7 +251,9 @@ class PermissionService return; } - $entities[] = $entity->book; + if ($entity->book) { + $entities[] = $entity->book; + } if ($entity->isA('page') && $entity->chapter_id) { $entities[] = $entity->chapter; @@ -226,13 +265,13 @@ class PermissionService } } - $this->deleteManyJointPermissionsForEntities($entities); $this->buildJointPermissionsForEntities(collect($entities)); } /** * Rebuild the entity jointPermissions for a collection of entities. * @param Collection $entities + * @throws \Throwable */ public function buildJointPermissionsForEntities(Collection $entities) { @@ -254,6 +293,12 @@ class PermissionService $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) { $this->buildJointPermissionsForBooks($books, $roles); }); + + // Chunk through all bookshelves + $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + ->chunk(50, function ($shelves) use ($roles) { + $this->buildJointPermissionsForShelves($shelves, $roles); + }); } /** @@ -289,7 +334,7 @@ class PermissionService /** * Delete all of the entity jointPermissions for a list of entities. - * @param Entity[] $entities + * @param \BookStack\Entities\Entity[] $entities * @throws \Throwable */ protected function deleteManyJointPermissionsForEntities($entities) @@ -370,7 +415,7 @@ class PermissionService /** * Get the actions related to an entity. - * @param Entity $entity + * @param \BookStack\Entities\Entity $entity * @return array */ protected function getActions(Entity $entity) @@ -412,7 +457,7 @@ class PermissionService return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); } - if ($entity->isA('book')) { + if ($entity->isA('book') || $entity->isA('bookshelf')) { return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn); } @@ -456,7 +501,7 @@ class PermissionService /** * Create an array of data with the information of an entity jointPermissions. * Used to build data for bulk insertion. - * @param Entity $entity + * @param \BookStack\Entities\Entity $entity * @param Role $role * @param $action * @param $permissionAll @@ -484,11 +529,6 @@ class PermissionService */ public function checkOwnableUserAccess(Ownable $ownable, $permission) { - if ($this->isAdmin()) { - $this->clean(); - return true; - } - $explodedPermission = explode('-', $permission); $baseQuery = $ownable->where('id', '=', $ownable->id); @@ -519,7 +559,7 @@ class PermissionService /** * Check if an entity has restrictions set on itself or its * parent tree. - * @param Entity $entity + * @param \BookStack\Entities\Entity $entity * @param $action * @return bool|mixed */ @@ -569,7 +609,9 @@ class PermissionService */ public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { - $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) { + $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) { @@ -577,21 +619,20 @@ class PermissionService }); } }); - $chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_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); - if (!$this->isAdmin()) { - $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); - }); + // 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->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery); $query->orderBy('draft', 'desc')->orderBy('priority', 'asc'); $this->clean(); @@ -601,7 +642,7 @@ class PermissionService /** * Add restrictions for a generic entity * @param string $entityType - * @param Builder|Entity $query + * @param Builder|\BookStack\Entities\Entity $query * @param string $action * @return Builder */ @@ -619,11 +660,6 @@ class PermissionService }); } - if ($this->isAdmin()) { - $this->clean(); - return $query; - } - $this->currentAction = $action; return $this->entityRestrictionQuery($query); } @@ -639,10 +675,6 @@ class PermissionService */ public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view') { - if ($this->isAdmin()) { - $this->clean(); - return $query; - } $this->currentAction = $action; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn]; @@ -675,20 +707,16 @@ class PermissionService */ public function filterRelatedPages($query, $tableName, $entityIdColumn) { - if ($this->isAdmin()) { - $this->clean(); - return $query; - } - $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - $q = $query->where(function ($query) use ($tableDetails) { - $query->where(function ($query) use (&$tableDetails) { - $query->whereExists(function ($permissionQuery) use (&$tableDetails) { + $pageMorphClass = $this->entityProvider->page->getMorphClass(); + $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { + $query->where(function ($query) use (&$tableDetails, $pageMorphClass) { + $query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) { $permissionQuery->select('id')->from('joint_permissions') ->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn']) - ->where('entity_type', '=', 'Bookstack\\Page') + ->where('entity_type', '=', $pageMorphClass) ->where('action', '=', $this->currentAction) ->whereIn('role_id', $this->getRoles()) ->where(function ($query) { @@ -704,22 +732,9 @@ class PermissionService return $q; } - /** - * Check if the current user is an admin. - * @return bool - */ - private function isAdmin() - { - if ($this->isAdminUser === null) { - $this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false; - } - - return $this->isAdminUser; - } - /** * Get the current user - * @return User + * @return \BookStack\Auth\User */ private function currentUser() { diff --git a/app/Repos/PermissionsRepo.php b/app/Auth/Permissions/PermissionsRepo.php similarity index 88% rename from app/Repos/PermissionsRepo.php rename to app/Auth/Permissions/PermissionsRepo.php index 6f7ea1dc8..18d5089be 100644 --- a/app/Repos/PermissionsRepo.php +++ b/app/Auth/Permissions/PermissionsRepo.php @@ -1,10 +1,8 @@ -permission = $permission; $this->role = $role; @@ -80,7 +78,7 @@ class PermissionsRepo /** * Updates an existing role. - * Ensure Admin role always has all permissions. + * Ensure Admin role always have core permissions. * @param $roleId * @param $roleData * @throws PermissionsException @@ -90,13 +88,18 @@ class PermissionsRepo $role = $this->role->findOrFail($roleId); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; - $this->assignRolePermissions($role, $permissions); - if ($role->system_name === 'admin') { - $permissions = $this->permission->all()->pluck('id')->toArray(); - $role->permissions()->sync($permissions); + $permissions = array_merge($permissions, [ + 'users-manage', + 'user-roles-manage', + 'restrictions-manage-all', + 'restrictions-manage-own', + 'settings-manage', + ]); } + $this->assignRolePermissions($role, $permissions); + $role->fill($roleData); $role->save(); $this->permissionService->buildJointPermissionForRole($role); diff --git a/app/RolePermission.php b/app/Auth/Permissions/RolePermission.php similarity index 83% rename from app/RolePermission.php rename to app/Auth/Permissions/RolePermission.php index 366c16749..8b07b3073 100644 --- a/app/RolePermission.php +++ b/app/Auth/Permissions/RolePermission.php @@ -1,4 +1,7 @@ -belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id'); + return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id'); } /** @@ -48,18 +51,18 @@ class Role extends Model /** * Add a permission to this role. - * @param RolePermission $permission + * @param \BookStack\Auth\Permissions\RolePermission $permission */ - public function attachPermission(RolePermission $permission) + public function attachPermission(Permissions\RolePermission $permission) { $this->permissions()->attach($permission->id); } /** * Detach a single permission from this role. - * @param RolePermission $permission + * @param \BookStack\Auth\Permissions\RolePermission $permission */ - public function detachPermission(RolePermission $permission) + public function detachPermission(Permissions\RolePermission $permission) { $this->permissions()->detach($permission->id); } diff --git a/app/SocialAccount.php b/app/Auth/SocialAccount.php similarity index 78% rename from app/SocialAccount.php rename to app/Auth/SocialAccount.php index fdba6a04f..804dbe629 100644 --- a/app/SocialAccount.php +++ b/app/Auth/SocialAccount.php @@ -1,4 +1,6 @@ -user->findOrFail($id); + return $this->user->newQuery()->findOrFail($id); } /** @@ -76,33 +76,31 @@ class UserRepo return $query->paginate($count); } - /** + /** * Creates a new user and attaches a role to them. * @param array $data - * @return User + * @param boolean $verifyEmail + * @return \BookStack\Auth\User */ - public function registerNew(array $data) + public function registerNew(array $data, $verifyEmail = false) { - $user = $this->create($data); + $user = $this->create($data, $verifyEmail); $this->attachDefaultRole($user); - - // Get avatar from gravatar and save - $this->downloadGravatarToUserAvatar($user); + $this->downloadAndAssignUserAvatar($user); return $user; } /** * Give a user the default role. Used when creating a new user. - * @param $user + * @param User $user */ - public function attachDefaultRole($user) + public function attachDefaultRole(User $user) { $roleId = setting('registration-role'); - if ($roleId === false) { - $roleId = $this->role->first()->id; + if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) { + $user->attachRoleId($roleId); } - $user->attachRoleId($roleId); } /** @@ -122,7 +120,7 @@ class UserRepo /** * Checks if the give user is the only admin. - * @param User $user + * @param \BookStack\Auth\User $user * @return bool */ public function isOnlyAdmin(User $user) @@ -138,24 +136,59 @@ class UserRepo return true; } + /** + * Set the assigned user roles via an array of role IDs. + * @param User $user + * @param array $roles + * @throws UserUpdateException + */ + public function setUserRoles(User $user, array $roles) + { + if ($this->demotingLastAdmin($user, $roles)) { + throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl()); + } + + $user->roles()->sync($roles); + } + + /** + * Check if the given user is the last admin and their new roles no longer + * contains the admin role. + * @param User $user + * @param array $newRoles + * @return bool + */ + protected function demotingLastAdmin(User $user, array $newRoles) : bool + { + if ($this->isOnlyAdmin($user)) { + $adminRole = $this->role->getSystemRole('admin'); + if (!in_array(strval($adminRole->id), $newRoles)) { + return true; + } + } + + return false; + } + /** * Create a new basic instance of user. * @param array $data - * @return User + * @param boolean $verifyEmail + * @return \BookStack\Auth\User */ - public function create(array $data) + public function create(array $data, $verifyEmail = false) { return $this->user->forceCreate([ 'name' => $data['name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), - 'email_confirmed' => false + 'email_confirmed' => $verifyEmail ]); } /** * Remove the given user from storage, Delete all related content. - * @param User $user + * @param \BookStack\Auth\User $user * @throws Exception */ public function destroy(User $user) @@ -172,7 +205,7 @@ class UserRepo /** * Get the latest activity for a user. - * @param User $user + * @param \BookStack\Auth\User $user * @param int $count * @param int $page * @return array @@ -184,7 +217,7 @@ class UserRepo /** * Get the recently created content for this given user. - * @param User $user + * @param \BookStack\Auth\User $user * @param int $count * @return mixed */ @@ -205,15 +238,15 @@ class UserRepo /** * Get asset created counts for the give user. - * @param User $user + * @param \BookStack\Auth\User $user * @return array */ public function getAssetCounts(User $user) { return [ - 'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(), - 'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(), - 'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(), + 'pages' => $this->entityRepo->getUserTotalCreated('page', $user), + 'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user), + 'books' => $this->entityRepo->getUserTotalCreated('book', $user), ]; } @@ -237,25 +270,24 @@ class UserRepo } /** - * Get a gravatar image for a user and set it as their avatar. - * Does not run if gravatar disabled in config. + * Get an avatar image for a user and set it as their avatar. + * Returns early if avatars disabled or not set in config. * @param User $user * @return bool */ - public function downloadGravatarToUserAvatar(User $user) + public function downloadAndAssignUserAvatar(User $user) { - // Get avatar from gravatar and save - if (!config('services.gravatar')) { + if (!Images::avatarFetchEnabled()) { return false; } try { - $avatar = Images::saveUserGravatar($user); + $avatar = Images::saveUserAvatar($user); $user->avatar()->associate($avatar); $user->save(); return true; } catch (Exception $e) { - \Log::error('Failed to save user gravatar image'); + \Log::error('Failed to save user avatar image'); return false; } } diff --git a/app/Console/Commands/CleanupImages.php b/app/Console/Commands/CleanupImages.php index e05508d5e..f2e2d9fbd 100644 --- a/app/Console/Commands/CleanupImages.php +++ b/app/Console/Commands/CleanupImages.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\Services\ImageService; +use BookStack\Uploads\ImageService; use Illuminate\Console\Command; use Symfony\Component\Console\Output\OutputInterface; @@ -30,7 +30,7 @@ class CleanupImages extends Command /** * Create a new command instance. - * @param ImageService $imageService + * @param \BookStack\Uploads\ImageService $imageService */ public function __construct(ImageService $imageService) { @@ -72,7 +72,9 @@ class CleanupImages extends Command protected function showDeletedImages($paths) { - if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return; + if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) { + return; + } if (count($paths) > 0) { $this->line('Images to delete:'); } diff --git a/app/Console/Commands/ClearActivity.php b/app/Console/Commands/ClearActivity.php index 66babd9a9..932ba7ddd 100644 --- a/app/Console/Commands/ClearActivity.php +++ b/app/Console/Commands/ClearActivity.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\Activity; +use BookStack\Actions\Activity; use Illuminate\Console\Command; class ClearActivity extends Command diff --git a/app/Console/Commands/ClearRevisions.php b/app/Console/Commands/ClearRevisions.php index f0c8a5e85..15f1fcc0a 100644 --- a/app/Console/Commands/ClearRevisions.php +++ b/app/Console/Commands/ClearRevisions.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\PageRevision; +use BookStack\Entities\PageRevision; use Illuminate\Console\Command; class ClearRevisions extends Command diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php index c7a9969e8..90c1ddb1c 100644 --- a/app/Console/Commands/CreateAdmin.php +++ b/app/Console/Commands/CreateAdmin.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\Repos\UserRepo; +use BookStack\Auth\UserRepo; use Illuminate\Console\Command; class CreateAdmin extends Command @@ -76,7 +76,7 @@ class CreateAdmin extends Command $user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]); $this->userRepo->attachSystemRole($user, 'admin'); - $this->userRepo->downloadGravatarToUserAvatar($user); + $this->userRepo->downloadAndAssignUserAvatar($user); $user->email_confirmed = true; $user->save(); diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php index 6dba83e13..68c5bb738 100644 --- a/app/Console/Commands/DeleteUsers.php +++ b/app/Console/Commands/DeleteUsers.php @@ -2,8 +2,8 @@ namespace BookStack\Console\Commands; -use BookStack\User; -use BookStack\Repos\UserRepo; +use BookStack\Auth\User; +use BookStack\Auth\UserRepo; use Illuminate\Console\Command; class DeleteUsers extends Command diff --git a/app/Console/Commands/RegeneratePermissions.php b/app/Console/Commands/RegeneratePermissions.php index 9cd577a17..430b8fcb0 100644 --- a/app/Console/Commands/RegeneratePermissions.php +++ b/app/Console/Commands/RegeneratePermissions.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\Services\PermissionService; +use BookStack\Auth\Permissions\PermissionService; use Illuminate\Console\Command; class RegeneratePermissions extends Command @@ -31,7 +31,7 @@ class RegeneratePermissions extends Command /** * Create a new command instance. * - * @param PermissionService $permissionService + * @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService */ public function __construct(PermissionService $permissionService) { diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php index 1a0005544..d27d73edc 100644 --- a/app/Console/Commands/RegenerateSearch.php +++ b/app/Console/Commands/RegenerateSearch.php @@ -2,7 +2,7 @@ namespace BookStack\Console\Commands; -use BookStack\Services\SearchService; +use BookStack\Entities\SearchService; use Illuminate\Console\Command; class RegenerateSearch extends Command @@ -26,7 +26,7 @@ class RegenerateSearch extends Command /** * Create a new command instance. * - * @param SearchService $searchService + * @param \BookStack\Entities\SearchService $searchService */ public function __construct(SearchService $searchService) { diff --git a/app/Book.php b/app/Entities/Book.php similarity index 84% rename from app/Book.php rename to app/Entities/Book.php index 51ea226b4..5ab5142c9 100644 --- a/app/Book.php +++ b/app/Entities/Book.php @@ -1,4 +1,6 @@ -belongsTo(Image::class, 'image_id'); } - /* - * Get the edit url for this book. - * @return string - */ - public function getEditUrl() - { - return $this->getUrl() . '/edit'; - } /** * Get all pages within this book. @@ -75,6 +78,15 @@ class Book extends Entity return $this->hasMany(Chapter::class); } + /** + * Get the shelves this book is contained within. + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function shelves() + { + return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id'); + } + /** * Get an excerpt of this book's description to the specified length or less. * @param int $length diff --git a/app/Entities/Bookshelf.php b/app/Entities/Bookshelf.php new file mode 100644 index 000000000..c37e36b59 --- /dev/null +++ b/app/Entities/Bookshelf.php @@ -0,0 +1,94 @@ +belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc'); + } + + /** + * Get the url for this bookshelf. + * @param string|bool $path + * @return string + */ + public function getUrl($path = false) + { + if ($path !== false) { + return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/')); + } + return baseUrl('/shelves/' . urlencode($this->slug)); + } + + /** + * Returns BookShelf cover image, if cover does not exists return default cover image. + * @param int $width - Width of the image + * @param int $height - Height of the image + * @return string + */ + public function getBookCover($width = 440, $height = 250) + { + $default = baseUrl('/book_default_cover.png'); + if (!$this->image_id) { + return $default; + } + + try { + $cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default; + } catch (\Exception $err) { + $cover = $default; + } + return $cover; + } + + /** + * Get the cover image of the book + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function cover() + { + return $this->belongsTo(Image::class, 'image_id'); + } + + /** + * Get an excerpt of this book's description to the specified length or less. + * @param int $length + * @return string + */ + public function getExcerpt($length = 100) + { + $description = $this->description; + return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description; + } + + /** + * Return a generalised, common raw query that can be 'unioned' across entities. + * @return string + */ + public function entityRawQuery() + { + 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"; + } +} diff --git a/app/Chapter.php b/app/Entities/Chapter.php similarity index 90% rename from app/Chapter.php rename to app/Entities/Chapter.php index 88b4c134c..079105ba9 100644 --- a/app/Chapter.php +++ b/app/Entities/Chapter.php @@ -1,4 +1,4 @@ -name) <= $length) { + if (mb_strlen($this->name) <= $length) { return $this->name; } - return substr($this->name, 0, $length - 3) . '...'; + return mb_substr($this->name, 0, $length - 3) . '...'; } /** diff --git a/app/Entities/EntityProvider.php b/app/Entities/EntityProvider.php new file mode 100644 index 000000000..46a883ec4 --- /dev/null +++ b/app/Entities/EntityProvider.php @@ -0,0 +1,89 @@ +bookshelf = $bookshelf; + $this->book = $book; + $this->chapter = $chapter; + $this->page = $page; + $this->pageRevision = $pageRevision; + } + + /** + * Fetch all core entity types as an associated array + * with their basic names as the keys. + * @return Entity[] + */ + public function all() + { + return [ + 'bookshelf' => $this->bookshelf, + 'book' => $this->book, + 'chapter' => $this->chapter, + 'page' => $this->page, + ]; + } + + /** + * Get an entity instance by it's basic name. + * @param string $type + * @return Entity + */ + public function get(string $type) + { + $type = strtolower($type); + return $this->all()[$type]; + } + + +} \ No newline at end of file diff --git a/app/Services/ExportService.php b/app/Entities/ExportService.php similarity index 96% rename from app/Services/ExportService.php rename to app/Entities/ExportService.php index c35600d73..d07c093f1 100644 --- a/app/Services/ExportService.php +++ b/app/Entities/ExportService.php @@ -1,9 +1,7 @@ -belongsTo(Book::class); } + /** + * Get the parent item + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function parent() + { + return $this->chapter_id ? $this->chapter() : $this->book(); + } + /** * Get the chapter that this page is in, If applicable. * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -103,4 +123,13 @@ class Page extends Entity $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"; } + + /** + * Get the current revision for the page if existing + * @return \BookStack\Entities\PageRevision|null + */ + public function getCurrentRevision() + { + return $this->revisions()->first(); + } } diff --git a/app/PageRevision.php b/app/Entities/PageRevision.php similarity index 94% rename from app/PageRevision.php rename to app/Entities/PageRevision.php index ffcc4f9d2..d30147bfc 100644 --- a/app/PageRevision.php +++ b/app/Entities/PageRevision.php @@ -1,4 +1,7 @@ -book = $book; - $this->chapter = $chapter; - $this->page = $page; - $this->pageRevision = $pageRevision; - $this->entities = [ - 'page' => $this->page, - 'chapter' => $this->chapter, - 'book' => $this->book - ]; + $this->entityProvider = $entityProvider; $this->viewService = $viewService; $this->permissionService = $permissionService; $this->tagRepo = $tagRepo; $this->searchService = $searchService; } - /** - * Get an entity instance via type. - * @param $type - * @return Entity - */ - protected function getEntity($type) - { - return $this->entities[strtolower($type)]; - } - /** * 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->getEntity($type), $permission); + $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission); if (strtolower($type) === 'page' && !$allowDrafts) { $q = $q->where('draft', '=', false); } @@ -143,15 +101,35 @@ class EntityRepo * @param integer $id * @param bool $allowDrafts * @param bool $ignorePermissions - * @return Entity + * @return \BookStack\Entities\Entity */ public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false) { + $query = $this->entityQuery($type, $allowDrafts); + if ($ignorePermissions) { - $entity = $this->getEntity($type); - return $entity->newQuery()->find($id); + $query = $this->entityProvider->get($type)->newQuery(); } - return $this->entityQuery($type, $allowDrafts)->find($id); + + return $query->find($id); + } + + /** + * @param string $type + * @param []int $ids + * @param bool $allowDrafts + * @param bool $ignorePermissions + * @return \Illuminate\Database\Eloquent\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(); } /** @@ -159,7 +137,7 @@ class EntityRepo * @param string $type * @param string $slug * @param string|bool $bookSlug - * @return Entity + * @return \BookStack\Entities\Entity * @throws NotFoundException */ public function getBySlug($type, $slug, $bookSlug = false) @@ -169,7 +147,7 @@ class EntityRepo if (strtolower($type) === 'chapter' || strtolower($type) === 'page') { $q = $q->where('book_id', '=', function ($query) use ($bookSlug) { $query->select('id') - ->from($this->book->getTable()) + ->from($this->entityProvider->book->getTable()) ->where('slug', '=', $bookSlug)->limit(1); }); } @@ -181,26 +159,6 @@ class EntityRepo } - /** - * 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 - */ - public function getPageByOldSlug($pageSlug, $bookSlug) - { - $revision = $this->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; - } - /** * Get all entities of a type with the given permission, limited by count unless count is false. * @param string $type @@ -238,7 +196,7 @@ class EntityRepo */ public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false) { - $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) + $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type)) ->orderBy('created_at', 'desc'); if (strtolower($type) === 'page') { $query = $query->where('draft', '=', false); @@ -259,7 +217,7 @@ class EntityRepo */ public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false) { - $query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)) + $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type)) ->orderBy('updated_at', 'desc'); if (strtolower($type) === 'page') { $query = $query->where('draft', '=', false); @@ -279,7 +237,7 @@ class EntityRepo */ public function getRecentlyViewed($type, $count = 10, $page = 0) { - $filter = is_bool($type) ? false : $this->getEntity($type); + $filter = is_bool($type) ? false : $this->entityProvider->get($type); return $this->viewService->getUserRecentlyViewed($count, $page, $filter); } @@ -314,7 +272,7 @@ class EntityRepo */ public function getPopular($type, $count = 10, $page = 0) { - $filter = is_bool($type) ? false : $this->getEntity($type); + $filter = is_bool($type) ? false : $this->entityProvider->get($type); return $this->viewService->getPopular($count, $page, $filter); } @@ -322,20 +280,44 @@ class EntityRepo * 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->page->where('draft', '=', true) + 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 \BookStack\Entities\Bookshelf $bookshelf + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ + public function getBookshelfChildren(Bookshelf $bookshelf) + { + return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get(); + } + /** * 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 \BookStack\Entities\Book $book * @param bool $filterDrafts * @param bool $renderPages * @return mixed @@ -348,14 +330,14 @@ class EntityRepo $tree = []; foreach ($q as $index => $rawEntity) { - if ($rawEntity->entity_type === 'BookStack\\Page') { - $entities[$index] = $this->page->newFromBuilder($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 === 'BookStack\\Chapter') { - $entities[$index] = $this->chapter->newFromBuilder($rawEntity); + } 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()); @@ -370,7 +352,7 @@ class EntityRepo if ($entity->chapter_id === 0 || $entity->chapter_id === '0') { continue; } - $parentKey = 'BookStack\\Chapter:' . $entity->chapter_id; + $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id; if (!isset($parents[$parentKey])) { $tree[] = $entity; continue; @@ -385,7 +367,7 @@ class EntityRepo /** * Get the child items for a chapter sorted by priority but * with draft items floated to the top. - * @param Chapter $chapter + * @param \BookStack\Entities\Chapter $chapter * @return \Illuminate\Database\Eloquent\Collection|static[] */ public function getChapterChildren(Chapter $chapter) @@ -397,7 +379,7 @@ class EntityRepo /** * Get the next sequential priority for a new child element in the given book. - * @param Book $book + * @param \BookStack\Entities\Book $book * @return int */ public function getNewBookPriority(Book $book) @@ -408,7 +390,7 @@ class EntityRepo /** * Get a new priority for a new page to be added to the given chapter. - * @param Chapter $chapter + * @param \BookStack\Entities\Chapter $chapter * @return int */ public function getNewChapterPriority(Chapter $chapter) @@ -444,7 +426,7 @@ class EntityRepo */ protected function slugExists($type, $slug, $currentId = false, $bookId = false) { - $query = $this->getEntity($type)->where('slug', '=', $slug); + $query = $this->entityProvider->get($type)->where('slug', '=', $slug); if (strtolower($type) === 'page' || strtolower($type) === 'chapter') { $query = $query->where('book_id', '=', $bookId); } @@ -456,10 +438,11 @@ class EntityRepo /** * Updates entity restrictions from a request - * @param $request - * @param Entity $entity + * @param Request $request + * @param \BookStack\Entities\Entity $entity + * @throws \Throwable */ - public function updateEntityPermissionsFromRequest($request, Entity $entity) + public function updateEntityPermissionsFromRequest(Request $request, Entity $entity) { $entity->restricted = $request->get('restricted', '') === 'true'; $entity->permissions()->delete(); @@ -487,12 +470,12 @@ class EntityRepo * @param string $type * @param array $input * @param bool|Book $book - * @return Entity + * @return \BookStack\Entities\Entity */ public function createFromInput($type, $input = [], $book = false) { $isChapter = strtolower($type) === 'chapter'; - $entityModel = $this->getEntity($type)->newInstance($input); + $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; @@ -511,9 +494,9 @@ class EntityRepo * Update entity details from request input. * Used for books and chapters * @param string $type - * @param Entity $entityModel + * @param \BookStack\Entities\Entity $entityModel * @param array $input - * @return Entity + * @return \BookStack\Entities\Entity */ public function updateFromInput($type, Entity $entityModel, $input = []) { @@ -533,13 +516,35 @@ class EntityRepo return $entityModel; } + /** + * Sync the books assigned to a shelf from a comma-separated list + * of book IDs. + * @param \BookStack\Entities\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); + } + /** * Change the book that an entity belongs to. * @param string $type * @param integer $newBookId * @param Entity $entity * @param bool $rebuildPermissions - * @return Entity + * @return \BookStack\Entities\Entity */ public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false) { @@ -593,188 +598,6 @@ class EntityRepo return $slug; } - /** - * Get a new draft page instance. - * @param Book $book - * @param Chapter|bool $chapter - * @return Page - */ - public function getDraftPage(Book $book, $chapter = false) - { - $page = $this->page->newInstance(); - $page->name = trans('entities.pages_initial_name'); - $page->created_by = user()->id; - $page->updated_by = user()->id; - $page->draft = true; - - if ($chapter) { - $page->chapter_id = $chapter->id; - } - - $book->pages()->save($page); - $page = $this->page->find($page->id); - $this->permissionService->buildJointPermissionsForEntity($page); - return $page; - } - - /** - * 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 - */ - public function publishPageDraft(Page $draftPage, array $input) - { - $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; - } - - /** - * Create a copy of a page in a new location with a new name. - * @param Page $page - * @param Entity $newParent - * @param string $newName - * @return Page - */ - public function copyPage(Page $page, Entity $newParent, $newName = '') - { - $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); - } - - /** - * Saves a page revision into the system. - * @param Page $page - * @param null|string $summary - * @return PageRevision - */ - public function savePageRevision(Page $page, $summary = null) - { - $revision = $this->pageRevision->newInstance($page->toArray()); - if (setting('app-editor') !== 'markdown') { - $revision->markdown = ''; - } - $revision->page_id = $page->id; - $revision->slug = $page->slug; - $revision->book_slug = $page->book->slug; - $revision->created_by = user()->id; - $revision->created_at = $page->updated_at; - $revision->type = 'version'; - $revision->summary = $summary; - $revision->revision_number = $page->revision_count; - $revision->save(); - - // Clear old revisions - if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { - $this->pageRevision->where('page_id', '=', $page->id) - ->orderBy('created_at', 'desc')->skip(50)->take(5)->delete(); - } - - return $revision; - } - - /** - * Formats a page's html to be tagged correctly - * within the system. - * @param string $htmlText - * @return string - */ - protected function formatHtml($htmlText) - { - 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; - - // Ensure no duplicate ids are used - $idArray = []; - - foreach ($childNodes as $index => $childNode) { - /** @var \DOMElement $childNode */ - if (get_class($childNode) !== 'DOMElement') { - continue; - } - - // Overwrite id if not a BookStack custom id - if ($childNode->hasAttribute('id')) { - $id = $childNode->getAttribute('id'); - if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { - $idArray[] = $id; - continue; - }; - } - - // 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-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); - $newId = urlencode($contentId); - $loopIndex = 0; - while (in_array($newId, $idArray)) { - $newId = urlencode($contentId . '-' . $loopIndex); - $loopIndex++; - } - - $childNode->setAttribute('id', $newId); - $idArray[] = $newId; - } - - // Generate inner html as a string - $html = ''; - foreach ($childNodes as $childNode) { - $html .= $doc->saveHTML($childNode); - } - - return $html; - } - - /** * Render the page for viewing, Parsing and performing features such as page transclusion. * @param Page $page @@ -855,17 +678,6 @@ class EntityRepo return $html; } - /** - * Get the plain text version of a page's content. - * @param Page $page - * @return string - */ - public function pageToPlainText(Page $page) - { - $html = $this->renderPage($page); - return strip_tags($html); - } - /** * Search for image usage within page content. * @param $imageString @@ -883,280 +695,21 @@ class EntityRepo } /** - * Parse the headers on the page to get a navigation menu - * @param String $pageContent - * @return array + * Destroy a bookshelf instance + * @param \BookStack\Entities\Bookshelf $shelf + * @throws \Throwable */ - public function getPageNav($pageContent) + public function destroyBookshelf(Bookshelf $shelf) { - 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 []; - } - - $tree = collect([]); - foreach ($headers as $header) { - $text = $header->nodeValue; - $tree->push([ - 'nodeName' => strtolower($header->nodeName), - 'level' => intval(str_replace('h', '', $header->nodeName)), - 'link' => '#' . $header->getAttribute('id'), - 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text - ]); - } - - // Normalise headers if only smaller headers have been used - if (count($tree) > 0) { - $minLevel = $tree->pluck('level')->min(); - $tree = $tree->map(function ($header) use ($minLevel) { - $header['level'] -= ($minLevel - 2); - return $header; - }); - } - return $tree->toArray(); - } - - /** - * 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 - */ - public function updatePage(Page $page, $book_id, $input) - { - // Hold the old details to compare later - $oldHtml = $page->html; - $oldName = $page->name; - - // Prevent slug being updated if no name change - if ($page->name !== $input['name']) { - $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id); - } - - // 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(); - - // Remove all update drafts for this user & page. - $this->userUpdatePageDraftsQuery($page, $userId)->delete(); - - // Save a revision after updating - if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { - $this->savePageRevision($page, $input['summary']); - } - - $this->searchService->indexEntity($page); - - return $page; - } - - /** - * The base query for getting user update drafts. - * @param Page $page - * @param $userId - * @return mixed - */ - protected function userUpdatePageDraftsQuery(Page $page, $userId) - { - return $this->pageRevision->where('created_by', '=', $userId) - ->where('type', 'update_draft') - ->where('page_id', '=', $page->id) - ->orderBy('created_at', 'desc'); - } - - /** - * Checks whether a user has a draft version of a particular page or not. - * @param Page $page - * @param $userId - * @return bool - */ - public function hasUserGotPageDraft(Page $page, $userId) - { - return $this->userUpdatePageDraftsQuery($page, $userId)->count() > 0; - } - - /** - * Get the latest updated draft revision for a particular page and user. - * @param Page $page - * @param $userId - * @return mixed - */ - public function getUserPageDraft(Page $page, $userId) - { - return $this->userUpdatePageDraftsQuery($page, $userId)->first(); - } - - /** - * Get the notification message that informs the user that they are editing a draft page. - * @param PageRevision $draft - * @return string - */ - public function getUserPageDraftMessage(PageRevision $draft) - { - $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'); - } - - /** - * 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 null $minRange - * @return bool - */ - public function isPageEditingActive(Page $page, $minRange = null) - { - $draftSearch = $this->activePageEditingQuery($page, $minRange); - return $draftSearch->count() > 0; - } - - /** - * A query to check for active update drafts on a particular page. - * @param Page $page - * @param null $minRange - * @return mixed - */ - protected function activePageEditingQuery(Page $page, $minRange = null) - { - $query = $this->pageRevision->where('type', '=', 'update_draft') - ->where('page_id', '=', $page->id) - ->where('updated_at', '>', $page->updated_at) - ->where('created_by', '!=', user()->id) - ->with('createdBy'); - - if ($minRange !== null) { - $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange)); - } - - return $query; - } - - /** - * Restores a revision's content back into a page. - * @param Page $page - * @param Book $book - * @param int $revisionId - * @return Page - */ - public function restorePageRevision(Page $page, Book $book, $revisionId) - { - $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; - } - - - /** - * Save a page update draft. - * @param Page $page - * @param array $data - * @return PageRevision|Page - */ - public function updatePageDraft(Page $page, $data = []) - { - // 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->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->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); - if (setting('app-editor') !== 'markdown') { - $draft->markdown = ''; - } - - $draft->save(); - return $draft; - } - - /** - * Get a notification message concerning the editing activity on a particular page. - * @param Page $page - * @param null $minRange - * @return string - */ - public function getPageEditingActiveMessage(Page $page, $minRange = null) - { - $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get(); - - $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]); - } - - /** - * Change the page's parent to the given entity. - * @param Page $page - * @param Entity $parent - */ - public function changePageParent(Page $page, Entity $parent) - { - $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); - } - $page->load('book'); - $this->permissionService->buildJointPermissionsForEntity($book); + $this->destroyEntityCommonRelations($shelf); + $shelf->delete(); } /** * Destroy the provided book and all its child entities. - * @param Book $book + * @param \BookStack\Entities\Book $book + * @throws NotifyException + * @throws \Throwable */ public function destroyBook(Book $book) { @@ -1166,17 +719,14 @@ class EntityRepo foreach ($book->chapters as $chapter) { $this->destroyChapter($chapter); } - \Activity::removeEntity($book); - $book->views()->delete(); - $book->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($book); - $this->searchService->deleteEntityTerms($book); + $this->destroyEntityCommonRelations($book); $book->delete(); } /** * Destroy a chapter and its relations. - * @param Chapter $chapter + * @param \BookStack\Entities\Chapter $chapter + * @throws \Throwable */ public function destroyChapter(Chapter $chapter) { @@ -1186,11 +736,7 @@ class EntityRepo $page->save(); } } - \Activity::removeEntity($chapter); - $chapter->views()->delete(); - $chapter->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($chapter); - $this->searchService->deleteEntityTerms($chapter); + $this->destroyEntityCommonRelations($chapter); $chapter->delete(); } @@ -1198,23 +744,18 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException + * @throws \Throwable */ public function destroyPage(Page $page) { - \Activity::removeEntity($page); - $page->views()->delete(); - $page->tags()->delete(); - $page->revisions()->delete(); - $page->permissions()->delete(); - $this->permissionService->deleteJointPermissionsForEntity($page); - $this->searchService->deleteEntityTerms($page); - // Check if set as custom homepage $customHome = setting('app-homepage', '0:'); if (intval($page->id) === intval(explode(':', $customHome)[0])) { throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl()); } + $this->destroyEntityCommonRelations($page); + // Delete Attached Files $attachmentService = app(AttachmentService::class); foreach ($page->attachments as $attachment) { @@ -1223,4 +764,48 @@ class EntityRepo $page->delete(); } + + /** + * Destroy or handle the common relations connected to an entity. + * @param \BookStack\Entities\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 \BookStack\Entities\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 new file mode 100644 index 000000000..d9f9c2720 --- /dev/null +++ b/app/Entities/Repos/PageRepo.php @@ -0,0 +1,508 @@ +getBySlug('page', $pageSlug, $bookSlug); + } + + /** + * 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 + */ + public function getPageByOldSlug(string $pageSlug, string $bookSlug) + { + $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; + } + + /** + * 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 + */ + public function updatePage(Page $page, int $book_id, array $input) + { + // Hold the old details to compare later + $oldHtml = $page->html; + $oldName = $page->name; + + // Prevent slug being updated if no name change + if ($page->name !== $input['name']) { + $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id); + } + + // 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(); + + // Remove all update drafts for this user & page. + $this->userUpdatePageDraftsQuery($page, $userId)->delete(); + + // Save a revision after updating + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { + $this->savePageRevision($page, $input['summary']); + } + + $this->searchService->indexEntity($page); + + return $page; + } + + /** + * Saves a page revision into the system. + * @param Page $page + * @param null|string $summary + * @return PageRevision + * @throws \Exception + */ + public function savePageRevision(Page $page, string $summary = null) + { + $revision = $this->entityProvider->pageRevision->newInstance($page->toArray()); + if (setting('app-editor') !== 'markdown') { + $revision->markdown = ''; + } + $revision->page_id = $page->id; + $revision->slug = $page->slug; + $revision->book_slug = $page->book->slug; + $revision->created_by = 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 $revision; + } + + /** + * Formats a page's html to be tagged correctly + * within the system. + * @param string $htmlText + * @return string + */ + protected function formatHtml(string $htmlText) + { + 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; + + // Ensure no duplicate ids are used + $idArray = []; + + foreach ($childNodes as $index => $childNode) { + /** @var \DOMElement $childNode */ + if (get_class($childNode) !== 'DOMElement') { + continue; + } + + // Overwrite id if not a BookStack custom id + if ($childNode->hasAttribute('id')) { + $id = $childNode->getAttribute('id'); + if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) { + $idArray[] = $id; + continue; + }; + } + + // 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-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20); + $newId = urlencode($contentId); + $loopIndex = 0; + while (in_array($newId, $idArray)) { + $newId = urlencode($contentId . '-' . $loopIndex); + $loopIndex++; + } + + $childNode->setAttribute('id', $newId); + $idArray[] = $newId; + } + + // Generate inner html as a string + $html = ''; + foreach ($childNodes as $childNode) { + $html .= $doc->saveHTML($childNode); + } + + return $html; + } + + /** + * Get the plain text version of a page's content. + * @param \BookStack\Entities\Page $page + * @return string + */ + public function pageToPlainText(Page $page) + { + $html = $this->renderPage($page); + return strip_tags($html); + } + + /** + * Get a new draft page instance. + * @param Book $book + * @param Chapter|null $chapter + * @return \BookStack\Entities\Page + * @throws \Throwable + */ + public function getDraftPage(Book $book, Chapter $chapter = 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; + + if ($chapter) { + $page->chapter_id = $chapter->id; + } + + $book->pages()->save($page); + $page = $this->entityProvider->page->find($page->id); + $this->permissionService->buildJointPermissionsForEntity($page); + return $page; + } + + /** + * Save a page update draft. + * @param Page $page + * @param array $data + * @return PageRevision|Page + */ + public function updatePageDraft(Page $page, array $data = []) + { + // 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->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); + if (setting('app-editor') !== 'markdown') { + $draft->markdown = ''; + } + + $draft->save(); + return $draft; + } + + /** + * 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 + */ + public function publishPageDraft(Page $draftPage, array $input) + { + $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; + } + + /** + * The base query for getting user update drafts. + * @param Page $page + * @param $userId + * @return mixed + */ + protected function userUpdatePageDraftsQuery(Page $page, int $userId) + { + return $this->entityProvider->pageRevision->where('created_by', '=', $userId) + ->where('type', 'update_draft') + ->where('page_id', '=', $page->id) + ->orderBy('created_at', 'desc'); + } + + /** + * Get the latest updated draft revision for a particular page and user. + * @param Page $page + * @param $userId + * @return PageRevision|null + */ + public function getUserPageDraft(Page $page, int $userId) + { + return $this->userUpdatePageDraftsQuery($page, $userId)->first(); + } + + /** + * Get the notification message that informs the user that they are editing a draft page. + * @param PageRevision $draft + * @return string + */ + public function getUserPageDraftMessage(PageRevision $draft) + { + $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. + * @param Page $page + * @param int $minRange + * @return mixed + */ + protected function activePageEditingQuery(Page $page, int $minRange = null) + { + $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'); + + if ($minRange !== null) { + $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange)); + } + + return $query; + } + + /** + * 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 + */ + public function isPageEditingActive(Page $page, int $minRange = null) + { + $draftSearch = $this->activePageEditingQuery($page, $minRange); + return $draftSearch->count() > 0; + } + + /** + * Get a notification message concerning the editing activity on a particular page. + * @param Page $page + * @param int $minRange + * @return string + */ + public function getPageEditingActiveMessage(Page $page, int $minRange = null) + { + $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get(); + + $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]); + } + + /** + * Parse the headers on the page to get a navigation menu + * @param string $pageContent + * @return array + */ + public function getPageNav(string $pageContent) + { + 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 []; + } + + $tree = collect([]); + foreach ($headers as $header) { + $text = $header->nodeValue; + $tree->push([ + 'nodeName' => strtolower($header->nodeName), + 'level' => intval(str_replace('h', '', $header->nodeName)), + 'link' => '#' . $header->getAttribute('id'), + 'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text + ]); + } + + // Normalise headers if only smaller headers have been used + if (count($tree) > 0) { + $minLevel = $tree->pluck('level')->min(); + $tree = $tree->map(function ($header) use ($minLevel) { + $header['level'] -= ($minLevel - 2); + return $header; + }); + } + return $tree->toArray(); + } + + /** + * Restores a revision's content back into a page. + * @param Page $page + * @param Book $book + * @param int $revisionId + * @return Page + * @throws \Exception + */ + public function restorePageRevision(Page $page, Book $book, int $revisionId) + { + $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; + } + + /** + * Change the page's parent to the given entity. + * @param Page $page + * @param Entity $parent + * @throws \Throwable + */ + public function changePageParent(Page $page, Entity $parent) + { + $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); + } + $page->load('book'); + $this->permissionService->buildJointPermissionsForEntity($book); + } + + /** + * 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 + */ + public function copyPage(Page $page, Entity $newParent, string $newName = '') + { + $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); + } +} \ No newline at end of file diff --git a/app/Services/SearchService.php b/app/Entities/SearchService.php similarity index 80% rename from app/Services/SearchService.php rename to app/Entities/SearchService.php index 6390b8bc4..9e7cfdd0c 100644 --- a/app/Services/SearchService.php +++ b/app/Entities/SearchService.php @@ -1,24 +1,34 @@ -searchTerm = $searchTerm; - $this->book = $book; - $this->chapter = $chapter; - $this->page = $page; + $this->entityProvider = $entityProvider; $this->db = $db; - $this->entities = [ - 'page' => $this->page, - 'chapter' => $this->chapter, - 'book' => $this->book - ]; $this->permissionService = $permissionService; } @@ -65,12 +66,13 @@ class SearchService * @param string $entityType * @param int $page * @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed. + * @param string $action * @return array[int, Collection]; */ public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view') { $terms = $this->parseSearchString($searchString); - $entityTypes = array_keys($this->entities); + $entityTypes = array_keys($this->entityProvider->all()); $entityTypesToSearch = $entityTypes; if ($entityType !== 'all') { @@ -167,17 +169,17 @@ class SearchService * @param array $terms * @param string $entityType * @param string $action - * @return \Illuminate\Database\Eloquent\Builder + * @return EloquentBuilder */ protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view') { - $entity = $this->getEntity($entityType); + $entity = $this->entityProvider->get($entityType); $entitySelect = $entity->newQuery(); // Handle normal search terms if (count($terms['search']) > 0) { $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); - $subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType)); + $subQuery->where('entity_type', '=', $entity->getMorphClass()); $subQuery->where(function (Builder $query) use ($terms) { foreach ($terms['search'] as $inputTerm) { $query->orWhere('term', 'like', $inputTerm .'%'); @@ -191,9 +193,9 @@ class SearchService // Handle exact term matching if (count($terms['exact']) > 0) { - $entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) { + $entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) { foreach ($terms['exact'] as $inputTerm) { - $query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) { + $query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) { $query->where('name', 'like', '%'.$inputTerm .'%') ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); }); @@ -281,14 +283,14 @@ class SearchService /** * Apply a tag search term onto a entity query. - * @param \Illuminate\Database\Eloquent\Builder $query + * @param EloquentBuilder $query * @param string $tagTerm * @return mixed */ - protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) + protected function applyTagSearch(EloquentBuilder $query, $tagTerm) { preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); - $query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) { + $query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) { $tagName = $tagSplit[1]; $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; $tagValue = count($tagSplit) > 3 ? $tagSplit[4] : ''; @@ -313,16 +315,6 @@ class SearchService return $query; } - /** - * Get an entity instance via type. - * @param $type - * @return Entity - */ - protected function getEntity($type) - { - return $this->entities[strtolower($type)]; - } - /** * Index the given entity. * @param Entity $entity @@ -342,7 +334,7 @@ class SearchService /** * Index multiple Entities at once - * @param Entity[] $entities + * @param \BookStack\Entities\Entity[] $entities */ protected function indexEntities($entities) { @@ -370,20 +362,12 @@ class SearchService { $this->searchTerm->truncate(); - // Chunk through all books - $this->book->chunk(1000, function ($books) { - $this->indexEntities($books); - }); - - // Chunk through all chapters - $this->chapter->chunk(1000, function ($chapters) { - $this->indexEntities($chapters); - }); - - // Chunk through all pages - $this->page->chunk(1000, function ($pages) { - $this->indexEntities($pages); - }); + foreach ($this->entityProvider->all() as $entityModel) { + $selectFields = ['id', 'name', $entityModel->textField]; + $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) { + $this->indexEntities($entities); + }); + } } /** @@ -432,7 +416,7 @@ class SearchService * Custom entity search filters */ - protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input) { try { $date = date_create($input); @@ -442,7 +426,7 @@ class SearchService $query->where('updated_at', '>=', $date); } - protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterUpdatedBefore(EloquentBuilder $query, Entity $model, $input) { try { $date = date_create($input); @@ -452,7 +436,7 @@ class SearchService $query->where('updated_at', '<', $date); } - protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterCreatedAfter(EloquentBuilder $query, Entity $model, $input) { try { $date = date_create($input); @@ -462,7 +446,7 @@ class SearchService $query->where('created_at', '>=', $date); } - protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $input) { try { $date = date_create($input); @@ -472,7 +456,7 @@ class SearchService $query->where('created_at', '<', $date); } - protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input) { if (!is_numeric($input) && $input !== 'me') { return; @@ -483,7 +467,7 @@ class SearchService $query->where('created_by', '=', $input); } - protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input) { if (!is_numeric($input) && $input !== 'me') { return; @@ -494,41 +478,41 @@ class SearchService $query->where('updated_by', '=', $input); } - protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterInName(EloquentBuilder $query, Entity $model, $input) { $query->where('name', 'like', '%' .$input. '%'); } - protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterInTitle(EloquentBuilder $query, Entity $model, $input) { $this->filterInName($query, $model, $input); } - protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterInBody(EloquentBuilder $query, Entity $model, $input) { $query->where($model->textField, 'like', '%' .$input. '%'); } - protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input) { $query->where('restricted', '=', true); } - protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterViewedByMe(EloquentBuilder $query, Entity $model, $input) { $query->whereHas('views', function ($query) { $query->where('user_id', '=', user()->id); }); } - protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterNotViewedByMe(EloquentBuilder $query, Entity $model, $input) { $query->whereDoesntHave('views', function ($query) { $query->where('user_id', '=', user()->id); }); } - protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) + protected function filterSortBy(EloquentBuilder $query, Entity $model, $input) { $functionName = camel_case('sort_by_' . $input); if (method_exists($this, $functionName)) { @@ -541,7 +525,7 @@ class SearchService * Sorting filter options */ - protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model) + protected function sortByLastCommented(EloquentBuilder $query, Entity $model) { $commentsTable = $this->db->getTablePrefix() . 'comments'; $morphClass = str_replace('\\', '\\\\', $model->getMorphClass()); diff --git a/app/SearchTerm.php b/app/Entities/SearchTerm.php similarity index 85% rename from app/SearchTerm.php rename to app/Entities/SearchTerm.php index ee6c72190..886c4dbc1 100644 --- a/app/SearchTerm.php +++ b/app/Entities/SearchTerm.php @@ -1,4 +1,6 @@ -message = $message; $this->redirectLocation = $redirectLocation; diff --git a/app/Exceptions/SocialSignInAccountNotUsed.php b/app/Exceptions/SocialSignInAccountNotUsed.php new file mode 100644 index 000000000..7eaa72bd5 --- /dev/null +++ b/app/Exceptions/SocialSignInAccountNotUsed.php @@ -0,0 +1,6 @@ +attachmentService->getAttachmentFromStorage($attachment); - return response($attachmentContents, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"' - ]); + return $this->downloadResponse($attachmentContents, $attachment->getFileName()); } /** diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 106b90524..e820154e7 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -2,10 +2,11 @@ namespace BookStack\Http\Controllers\Auth; +use BookStack\Auth\Access\LdapService; +use BookStack\Auth\Access\SocialAuthService; +use BookStack\Auth\UserRepo; use BookStack\Exceptions\AuthException; use BookStack\Http\Controllers\Controller; -use BookStack\Repos\UserRepo; -use BookStack\Services\SocialAuthService; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; @@ -36,18 +37,21 @@ class LoginController extends Controller protected $redirectAfterLogout = '/login'; protected $socialAuthService; + protected $ldapService; protected $userRepo; /** * Create a new controller instance. * - * @param SocialAuthService $socialAuthService - * @param UserRepo $userRepo + * @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService + * @param LdapService $ldapService + * @param \BookStack\Auth\UserRepo $userRepo */ - public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo) + public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo) { $this->middleware('guest', ['only' => ['getLogin', 'postLogin']]); $this->socialAuthService = $socialAuthService; + $this->ldapService = $ldapService; $this->userRepo = $userRepo; $this->redirectPath = baseUrl('/'); $this->redirectAfterLogout = baseUrl('/login'); @@ -66,6 +70,7 @@ class LoginController extends Controller * @param Authenticatable $user * @return \Illuminate\Http\RedirectResponse * @throws AuthException + * @throws \BookStack\Exceptions\LdapException */ protected function authenticated(Request $request, Authenticatable $user) { @@ -96,6 +101,11 @@ class LoginController extends Controller auth()->login($user); } + // Sync LDAP groups if required + if ($this->ldapService->shouldSyncGroups()) { + $this->ldapService->syncGroups($user, $request->get($this->username())); + } + $path = session()->pull('url.intended', '/'); $path = baseUrl($path, true); return redirect($path); @@ -125,6 +135,7 @@ class LoginController extends Controller * Redirect to the relevant social site. * @param $socialDriver * @return \Symfony\Component\HttpFoundation\RedirectResponse + * @throws \BookStack\Exceptions\SocialDriverNotConfigured */ public function getSocialLogin($socialDriver) { diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 1bbd5e2ba..cdcca116c 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -2,20 +2,19 @@ namespace BookStack\Http\Controllers\Auth; -use BookStack\Exceptions\ConfirmationEmailException; +use BookStack\Auth\SocialAccount; +use BookStack\Auth\User; +use BookStack\Auth\UserRepo; +use BookStack\Exceptions\SocialSignInAccountNotUsed; use BookStack\Exceptions\SocialSignInException; use BookStack\Exceptions\UserRegistrationException; -use BookStack\Repos\UserRepo; -use BookStack\Services\EmailConfirmationService; -use BookStack\Services\SocialAuthService; -use BookStack\SocialAccount; -use BookStack\User; +use BookStack\Http\Controllers\Controller; use Exception; +use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Laravel\Socialite\Contracts\User as SocialUser; use Validator; -use BookStack\Http\Controllers\Controller; -use Illuminate\Foundation\Auth\RegistersUsers; class RegisterController extends Controller { @@ -47,11 +46,11 @@ class RegisterController extends Controller /** * Create a new controller instance. * - * @param SocialAuthService $socialAuthService - * @param EmailConfirmationService $emailConfirmationService - * @param UserRepo $userRepo + * @param \BookStack\Auth\Access\SocialAuthService $socialAuthService + * @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService + * @param \BookStack\Auth\UserRepo $userRepo */ - public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) + public function __construct(\BookStack\Auth\Access\SocialAuthService $socialAuthService, \BookStack\Auth\Access\EmailConfirmationService $emailConfirmationService, UserRepo $userRepo) { $this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']); $this->socialAuthService = $socialAuthService; @@ -118,7 +117,7 @@ class RegisterController extends Controller /** * Create a new user instance after a valid registration. * @param array $data - * @return User + * @return \BookStack\Auth\User */ protected function create(array $data) { @@ -133,25 +132,28 @@ class RegisterController extends Controller * The registrations flow for all users. * @param array $userData * @param bool|false|SocialAccount $socialAccount + * @param bool $emailVerified * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException */ - protected function registerUser(array $userData, $socialAccount = false) + protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false) { - if (setting('registration-restrict')) { - $restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict'))); + $registrationRestrict = setting('registration-restrict'); + + if ($registrationRestrict) { + $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict)); $userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1); if (!in_array($userEmailDomain, $restrictedEmailDomains)) { throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register'); } } - $newUser = $this->userRepo->registerNew($userData); + $newUser = $this->userRepo->registerNew($userData, $emailVerified); if ($socialAccount) { $newUser->socialAccounts()->save($socialAccount); } - if (setting('registration-confirmation') || setting('registration-restrict')) { + if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) { $newUser->save(); try { @@ -250,7 +252,6 @@ class RegisterController extends Controller * @throws SocialSignInException * @throws UserRegistrationException * @throws \BookStack\Exceptions\SocialDriverNotConfigured - * @throws ConfirmationEmailException */ public function socialCallback($socialDriver, Request $request) { @@ -267,12 +268,24 @@ class RegisterController extends Controller } $action = session()->pull('social-callback'); + + // Attempt login or fall-back to register if allowed. + $socialUser = $this->socialAuthService->getSocialUser($socialDriver); if ($action == 'login') { - return $this->socialAuthService->handleLoginCallback($socialDriver); + try { + return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser); + } catch (SocialSignInAccountNotUsed $exception) { + if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) { + return $this->socialRegisterCallback($socialDriver, $socialUser); + } + throw $exception; + } } + if ($action == 'register') { - return $this->socialRegisterCallback($socialDriver); + return $this->socialRegisterCallback($socialDriver, $socialUser); } + return redirect()->back(); } @@ -288,15 +301,16 @@ class RegisterController extends Controller /** * Register a new user after a registration callback. - * @param $socialDriver + * @param string $socialDriver + * @param SocialUser $socialUser * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @throws UserRegistrationException - * @throws \BookStack\Exceptions\SocialDriverNotConfigured */ - protected function socialRegisterCallback($socialDriver) + protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser) { - $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver); + $socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser); $socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser); + $emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver); // Create an array of the user data to create a new user instance $userData = [ @@ -304,6 +318,6 @@ class RegisterController extends Controller 'email' => $socialUser->getEmail(), 'password' => str_random(30) ]; - return $this->registerUser($userData, $socialAccount); + return $this->registerUser($userData, $socialAccount, $emailVerified); } } diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 2c3946239..44368a9c4 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -1,10 +1,10 @@ unique()->toArray(); - $booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get(); + $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(); @@ -299,10 +299,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $pdfContent = $this->exportService->bookToPdf($book); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $bookSlug . '.pdf'); } /** @@ -314,10 +311,7 @@ class BookController extends Controller { $book = $this->entityRepo->getBySlug('book', $bookSlug); $htmlContent = $this->exportService->bookToContainedHtml($book); - return response()->make($htmlContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html' - ]); + return $this->downloadResponse($htmlContent, $bookSlug . '.html'); } /** @@ -328,10 +322,7 @@ class BookController extends Controller public function exportPlainText($bookSlug) { $book = $this->entityRepo->getBySlug('book', $bookSlug); - $htmlContent = $this->exportService->bookToPlainText($book); - return response()->make($htmlContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt' - ]); + $textContent = $this->exportService->bookToPlainText($book); + return $this->downloadResponse($textContent, $bookSlug . '.txt'); } } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php new file mode 100644 index 000000000..5c2898786 --- /dev/null +++ b/app/Http/Controllers/BookshelfController.php @@ -0,0 +1,242 @@ +entityRepo = $entityRepo; + $this->userRepo = $userRepo; + $this->exportService = $exportService; + parent::__construct(); + } + + /** + * Display a listing of the book. + * @return Response + */ + public function index() + { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false; + $popular = $this->entityRepo->getPopular('bookshelf', 4, 0); + $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + + $this->setPageTitle(trans('entities.shelves')); + return view('shelves/index', [ + 'shelves' => $shelves, + 'recents' => $recents, + 'popular' => $popular, + 'new' => $new, + 'shelvesViewType' => $shelvesViewType + ]); + } + + /** + * 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'); + $this->setPageTitle(trans('entities.shelves_create')); + return view('shelves/create', ['books' => $books]); + } + + /** + * Store a newly created bookshelf in storage. + * @param Request $request + * @return Response + */ + public function store(Request $request) + { + $this->checkPermission('bookshelf-create-all'); + $this->validate($request, [ + 'name' => 'required|string|max:255', + 'description' => 'string|max:1000', + ]); + + $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); + $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); + Activity::add($bookshelf, 'bookshelf_create'); + + return redirect($bookshelf->getUrl()); + } + + + /** + * Display the specified bookshelf. + * @param String $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function show(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('book-view', $bookshelf); + + $books = $this->entityRepo->getBookshelfChildren($bookshelf); + Views::add($bookshelf); + + $this->setPageTitle($bookshelf->getShortName()); + return view('shelves/show', [ + 'shelf' => $bookshelf, + 'books' => $books, + 'activity' => Activity::entityActivity($bookshelf, 20, 0) + ]); + } + + /** + * Show the form for editing the specified bookshelf. + * @param $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function edit(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $bookshelf); + + $shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf); + $shelfBookIds = $shelfBooks->pluck('id'); + $books = $this->entityRepo->getAll('book', false, 'update'); + $books = $books->filter(function ($book) use ($shelfBookIds) { + return !$shelfBookIds->contains($book->id); + }); + + $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()])); + return view('shelves/edit', [ + 'shelf' => $bookshelf, + 'books' => $books, + 'shelfBooks' => $shelfBooks, + ]); + } + + + /** + * Update the specified bookshelf in storage. + * @param Request $request + * @param string $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + */ + public function update(Request $request, string $slug) + { + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $shelf); + $this->validate($request, [ + 'name' => 'required|string|max:255', + 'description' => 'string|max:1000', + ]); + + $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all()); + $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + Activity::add($shelf, 'bookshelf_update'); + + 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) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + + $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()])); + return view('shelves/delete', ['shelf' => $bookshelf]); + } + + /** + * Remove the specified bookshelf from storage. + * @param string $slug + * @return Response + * @throws \BookStack\Exceptions\NotFoundException + * @throws \Throwable + */ + public function destroy(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + Activity::addMessage('bookshelf_delete', 0, $bookshelf->name); + $this->entityRepo->destroyBookshelf($bookshelf); + return redirect('/shelves'); + } + + /** + * Show the Restrictions view. + * @param $slug + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @throws \BookStack\Exceptions\NotFoundException + */ + public function showRestrict(string $slug) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $roles = $this->userRepo->getRestrictableRoles(); + return view('shelves.restrictions', [ + 'shelf' => $bookshelf, + 'roles' => $roles + ]); + } + + /** + * Set the restrictions for this bookshelf. + * @param $slug + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws \BookStack\Exceptions\NotFoundException + */ + public function restrict(string $slug, Request $request) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf); + session()->flash('success', trans('entities.shelves_permissions_updated')); + return redirect($bookshelf->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) + { + $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $bookshelf); + + $updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf); + session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount])); + return redirect($bookshelf->getUrl()); + } +} diff --git a/app/Http/Controllers/ChapterController.php b/app/Http/Controllers/ChapterController.php index b737afc6d..a50306552 100644 --- a/app/Http/Controllers/ChapterController.php +++ b/app/Http/Controllers/ChapterController.php @@ -1,9 +1,9 @@ entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $pdfContent = $this->exportService->chapterToPdf($chapter); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf'); } /** @@ -266,10 +263,7 @@ class ChapterController extends Controller { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); $containedHtml = $this->exportService->chapterToContainedHtml($chapter); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html' - ]); + return $this->downloadResponse($containedHtml, $chapterSlug . '.html'); } /** @@ -281,10 +275,7 @@ class ChapterController extends Controller public function exportPlainText($bookSlug, $chapterSlug) { $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug); - $containedHtml = $this->exportService->chapterToPlainText($chapter); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt' - ]); + $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 7bf0a2aac..2039ce7fe 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -1,8 +1,8 @@ withInput($request->input()) ->withErrors($errors, $this->errorBag()); } + + /** + * Create a response that forces a download in the browser. + * @param string $content + * @param string $fileName + * @return \Illuminate\Http\Response + */ + protected function downloadResponse(string $content, string $fileName) + { + return response()->make($content, 200, [ + 'Content-Type' => 'application/octet-stream', + 'Content-Disposition' => 'attachment; filename="' . $fileName . '"' + ]); + } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 2077f6888..5a5f34e4a 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -1,8 +1,7 @@ signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor); $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12); - - $customHomepage = false; - $books = false; - $booksViewType = false; - - // Check book homepage - $bookHomepageSetting = setting('app-book-homepage'); - if ($bookHomepageSetting) { - $books = $this->entityRepo->getAllPaginated('book', 18); - $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); - } else { - // Check custom homepage - $homepageSetting = setting('app-homepage'); - if ($homepageSetting) { - $id = intval(explode(':', $homepageSetting)[0]); - $customHomepage = $this->entityRepo->getById('page', $id, false, true); - $this->entityRepo->renderPage($customHomepage, true); - } + $homepageOptions = ['default', 'books', 'bookshelves', 'page']; + $homepageOption = setting('app-homepage-type', 'default'); + if (!in_array($homepageOption, $homepageOptions)) { + $homepageOption = 'default'; } - $view = 'home'; - if ($bookHomepageSetting) { - $view = 'home-book'; - } else if ($customHomepage) { - $view = 'home-custom'; - } - - return view('common/' . $view, [ + $commonData = [ 'activity' => $activity, 'recents' => $recents, 'recentlyUpdatedPages' => $recentlyUpdatedPages, 'draftPages' => $draftPages, - 'customHomepage' => $customHomepage, - 'books' => $books, - 'booksViewType' => $booksViewType - ]); + ]; + + if ($homepageOption === 'bookshelves') { + $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18); + $shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid')); + $data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]); + return view('common.home-shelves', $data); + } + + if ($homepageOption === 'books') { + $books = $this->entityRepo->getAllPaginated('book', 18); + $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list')); + $data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]); + return view('common.home-book', $data); + } + + 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); + return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage])); + } + + return view('common.home', $commonData); } /** @@ -80,6 +79,7 @@ class HomeController extends Controller { $locale = app()->getLocale(); $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; + if (cache()->has($cacheKey) && config('app.env') !== 'development') { $resp = cache($cacheKey); } else { @@ -90,15 +90,6 @@ class HomeController extends Controller 'entities' => trans('entities'), 'errors' => trans('errors') ]; - if ($locale !== 'en') { - $enTrans = [ - 'common' => trans('common', [], 'en'), - 'components' => trans('components', [], 'en'), - 'entities' => trans('entities', [], 'en'), - 'errors' => trans('errors', [], 'en') - ]; - $translations = array_replace_recursive($enTrans, $translations); - } $resp = 'window.translations = ' . json_encode($translations); cache()->put($cacheKey, $resp, 120); } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index eb92ae9a8..4bd1b479c 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -1,13 +1,12 @@ entityRepo = $entityRepo; + $this->pageRepo = $pageRepo; $this->exportService = $exportService; $this->userRepo = $userRepo; parent::__construct(); @@ -38,21 +38,28 @@ class PageController extends Controller * @param string $chapterSlug * @return Response * @internal param bool $pageSlug + * @throws NotFoundException */ public function create($bookSlug, $chapterSlug = null) { - $book = $this->entityRepo->getBySlug('book', $bookSlug); - $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : 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; $this->checkOwnablePermission('page-create', $parent); // Redirect to draft edit screen if signed in if ($this->signedIn) { - $draft = $this->entityRepo->getDraftPage($book, $chapter); + $draft = $this->pageRepo->getDraftPage($book, $chapter); return redirect($draft->getUrl()); } - // Otherwise show edit view + // Otherwise show the edit view if they're a guest $this->setPageTitle(trans('entities.pages_new')); return view('pages/guest-create', ['parent' => $parent]); } @@ -71,13 +78,19 @@ class PageController extends Controller 'name' => 'required|string|max:255' ]); - $book = $this->entityRepo->getBySlug('book', $bookSlug); - $chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : 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; $this->checkOwnablePermission('page-create', $parent); - $page = $this->entityRepo->getDraftPage($book, $chapter); - $this->entityRepo->publishPageDraft($page, [ + $page = $this->pageRepo->getDraftPage($book, $chapter); + $this->pageRepo->publishPageDraft($page, [ 'name' => $request->get('name'), 'html' => '' ]); @@ -92,8 +105,8 @@ class PageController extends Controller */ public function editDraft($bookSlug, $pageId) { - $draft = $this->entityRepo->getById('page', $pageId, true); - $this->checkOwnablePermission('page-create', $draft->book); + $draft = $this->pageRepo->getById('page', $pageId, true); + $this->checkOwnablePermission('page-create', $draft->parent); $this->setPageTitle(trans('entities.pages_edit_draft')); $draftsEnabled = $this->signedIn; @@ -119,21 +132,19 @@ class PageController extends Controller ]); $input = $request->all(); - $book = $this->entityRepo->getBySlug('book', $bookSlug); + $draftPage = $this->pageRepo->getById('page', $pageId, true); + $book = $draftPage->book; - $draftPage = $this->entityRepo->getById('page', $pageId, true); - - $chapterId = intval($draftPage->chapter_id); - $parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book; + $parent = $draftPage->parent; $this->checkOwnablePermission('page-create', $parent); if ($parent->isA('chapter')) { - $input['priority'] = $this->entityRepo->getNewChapterPriority($parent); + $input['priority'] = $this->pageRepo->getNewChapterPriority($parent); } else { - $input['priority'] = $this->entityRepo->getNewBookPriority($parent); + $input['priority'] = $this->pageRepo->getNewBookPriority($parent); } - $page = $this->entityRepo->publishPageDraft($draftPage, $input); + $page = $this->pageRepo->publishPageDraft($draftPage, $input); Activity::add($page, 'page_create', $book->id); return redirect($page->getUrl()); @@ -150,9 +161,9 @@ class PageController extends Controller public function show($bookSlug, $pageSlug) { try { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); } catch (NotFoundException $e) { - $page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug); + $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug); if ($page === null) { throw $e; } @@ -161,9 +172,9 @@ class PageController extends Controller $this->checkOwnablePermission('page-view', $page); - $page->html = $this->entityRepo->renderPage($page); - $sidebarTree = $this->entityRepo->getBookChildren($page->book); - $pageNav = $this->entityRepo->getPageNav($page->html); + $page->html = $this->pageRepo->renderPage($page); + $sidebarTree = $this->pageRepo->getBookChildren($page->book); + $pageNav = $this->pageRepo->getPageNav($page->html); // check if the comment's are enabled $commentsEnabled = !setting('app-disable-comments'); @@ -189,7 +200,7 @@ class PageController extends Controller */ public function getPageAjax($pageId) { - $page = $this->entityRepo->getById('page', $pageId); + $page = $this->pageRepo->getById('page', $pageId); return response()->json($page); } @@ -198,28 +209,29 @@ class PageController extends Controller * @param string $bookSlug * @param string $pageSlug * @return Response + * @throws NotFoundException */ public function edit($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()])); $page->isDraft = false; // Check for active editing $warnings = []; - if ($this->entityRepo->isPageEditingActive($page, 60)) { - $warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60); + if ($this->pageRepo->isPageEditingActive($page, 60)) { + $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60); } // Check for a current draft version for this user - if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) { - $draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id); - $page->name = $draft->name; - $page->html = $draft->html; - $page->markdown = $draft->markdown; + $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); + if ($userPageDraft !== null) { + $page->name = $userPageDraft->name; + $page->html = $userPageDraft->html; + $page->markdown = $userPageDraft->markdown; $page->isDraft = true; - $warnings [] = $this->entityRepo->getUserPageDraftMessage($draft); + $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft); } if (count($warnings) > 0) { @@ -247,9 +259,9 @@ class PageController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255' ]); - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $this->entityRepo->updatePage($page, $page->book->id, $request->all()); + $this->pageRepo->updatePage($page, $page->book->id, $request->all()); Activity::add($page, 'page_update', $page->book->id); return redirect($page->getUrl()); } @@ -262,7 +274,7 @@ class PageController extends Controller */ public function saveDraft(Request $request, $pageId) { - $page = $this->entityRepo->getById('page', $pageId, true); + $page = $this->pageRepo->getById('page', $pageId, true); $this->checkOwnablePermission('page-update', $page); if (!$this->signedIn) { @@ -272,7 +284,7 @@ class PageController extends Controller ], 500); } - $draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); + $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); $updateTime = $draft->updated_at->timestamp; return response()->json([ @@ -290,7 +302,7 @@ class PageController extends Controller */ public function redirectFromLink($pageId) { - $page = $this->entityRepo->getById('page', $pageId); + $page = $this->pageRepo->getById('page', $pageId); return redirect($page->getUrl()); } @@ -302,7 +314,7 @@ class PageController extends Controller */ public function showDelete($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $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]); @@ -318,7 +330,7 @@ class PageController extends Controller */ public function showDeleteDraft($bookSlug, $pageId) { - $page = $this->entityRepo->getById('page', $pageId, true); + $page = $this->pageRepo->getById('page', $pageId, true); $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]); @@ -333,10 +345,10 @@ class PageController extends Controller */ public function destroy($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $book = $page->book; $this->checkOwnablePermission('page-delete', $page); - $this->entityRepo->destroyPage($page); + $this->pageRepo->destroyPage($page); Activity::addMessage('page_delete', $book->id, $page->name); session()->flash('success', trans('entities.pages_delete_success')); @@ -352,11 +364,11 @@ class PageController extends Controller */ public function destroyDraft($bookSlug, $pageId) { - $page = $this->entityRepo->getById('page', $pageId, true); + $page = $this->pageRepo->getById('page', $pageId, true); $book = $page->book; $this->checkOwnablePermission('page-update', $page); session()->flash('success', trans('entities.pages_delete_draft_success')); - $this->entityRepo->destroyPage($page); + $this->pageRepo->destroyPage($page); return redirect($book->getUrl()); } @@ -368,7 +380,7 @@ class PageController extends Controller */ public function showRevisions($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()])); return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]); } @@ -382,7 +394,7 @@ class PageController extends Controller */ public function showRevision($bookSlug, $pageSlug, $revisionId) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $revision = $page->revisions()->where('id', '=', $revisionId)->first(); if ($revision === null) { abort(404); @@ -407,7 +419,7 @@ class PageController extends Controller */ public function showRevisionChanges($bookSlug, $pageSlug, $revisionId) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $revision = $page->revisions()->where('id', '=', $revisionId)->first(); if ($revision === null) { abort(404); @@ -437,13 +449,47 @@ class PageController extends Controller */ public function restoreRevision($bookSlug, $pageSlug, $revisionId) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); - $page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId); + $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 @@ -453,13 +499,10 @@ class PageController extends Controller */ public function exportPdf($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $page->html = $this->entityRepo->renderPage($page); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); + $page->html = $this->pageRepo->renderPage($page); $pdfContent = $this->exportService->pageToPdf($page); - return response()->make($pdfContent, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf' - ]); + return $this->downloadResponse($pdfContent, $pageSlug . '.pdf'); } /** @@ -470,13 +513,10 @@ class PageController extends Controller */ public function exportHtml($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $page->html = $this->entityRepo->renderPage($page); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); + $page->html = $this->pageRepo->renderPage($page); $containedHtml = $this->exportService->pageToContainedHtml($page); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html' - ]); + return $this->downloadResponse($containedHtml, $pageSlug . '.html'); } /** @@ -487,12 +527,9 @@ class PageController extends Controller */ public function exportPlainText($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); - $containedHtml = $this->exportService->pageToPlainText($page); - return response()->make($containedHtml, 200, [ - 'Content-Type' => 'application/octet-stream', - 'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt' - ]); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); + $pageText = $this->exportService->pageToPlainText($page); + return $this->downloadResponse($pageText, $pageSlug . '.txt'); } /** @@ -501,7 +538,7 @@ class PageController extends Controller */ public function showRecentlyCreated() { - $pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created')); + $pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created')); return view('pages/detailed-listing', [ 'title' => trans('entities.recently_created_pages'), 'pages' => $pages @@ -514,7 +551,7 @@ class PageController extends Controller */ public function showRecentlyUpdated() { - $pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated')); + $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated')); return view('pages/detailed-listing', [ 'title' => trans('entities.recently_updated_pages'), 'pages' => $pages @@ -529,7 +566,7 @@ class PageController extends Controller */ public function showRestrict($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $page); $roles = $this->userRepo->getRestrictableRoles(); return view('pages/restrictions', [ @@ -547,7 +584,7 @@ class PageController extends Controller */ public function showMove($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); return view('pages/move', [ 'book' => $page->book, @@ -565,7 +602,7 @@ class PageController extends Controller */ public function move($bookSlug, $pageSlug, Request $request) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); $entitySelection = $request->get('entity_selection', null); @@ -579,7 +616,7 @@ class PageController extends Controller try { - $parent = $this->entityRepo->getById($entityType, $entityId); + $parent = $this->pageRepo->getById($entityType, $entityId); } catch (\Exception $e) { session()->flash(trans('entities.selected_book_chapter_not_found')); return redirect()->back(); @@ -587,7 +624,7 @@ class PageController extends Controller $this->checkOwnablePermission('page-create', $parent); - $this->entityRepo->changePageParent($page, $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])); @@ -603,7 +640,7 @@ class PageController extends Controller */ public function showCopy($bookSlug, $pageSlug) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); session()->flashInput(['name' => $page->name]); return view('pages/copy', [ @@ -622,7 +659,7 @@ class PageController extends Controller */ public function copy($bookSlug, $pageSlug, Request $request) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('page-update', $page); $entitySelection = $request->get('entity_selection', null); @@ -634,7 +671,7 @@ class PageController extends Controller $entityId = intval($stringExploded[1]); try { - $parent = $this->entityRepo->getById($entityType, $entityId); + $parent = $this->pageRepo->getById($entityType, $entityId); } catch (\Exception $e) { session()->flash(trans('entities.selected_book_chapter_not_found')); return redirect()->back(); @@ -643,7 +680,7 @@ class PageController extends Controller $this->checkOwnablePermission('page-create', $parent); - $pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', '')); + $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', '')); Activity::add($pageCopy, 'page_create', $pageCopy->book->id); session()->flash('success', trans('entities.pages_copy_success')); @@ -661,9 +698,9 @@ class PageController extends Controller */ public function restrict($bookSlug, $pageSlug, Request $request) { - $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug); + $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug); $this->checkOwnablePermission('restrictions-manage', $page); - $this->entityRepo->updateEntityPermissionsFromRequest($request, $page); + $this->pageRepo->updateEntityPermissionsFromRequest($request, $page); session()->flash('success', trans('entities.pages_permissions_success')); return redirect($page->getUrl()); } diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php index c4c7fe972..9be343c9a 100644 --- a/app/Http/Controllers/PermissionController.php +++ b/app/Http/Controllers/PermissionController.php @@ -1,7 +1,7 @@ searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results']; } else { $entityNames = $entityTypes->map(function ($type) { - return 'BookStack\\' . ucfirst($type); + return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy })->toArray(); $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission); } diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index d9d66042e..01fb68fe0 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,6 +1,6 @@ filled('roles')) { $roles = $request->get('roles'); - $user->roles()->sync($roles); + $this->userRepo->setUserRoles($user, $roles); } - $this->userRepo->downloadGravatarToUserAvatar($user); + $this->userRepo->downloadAndAssignUserAvatar($user); return redirect('/settings/users'); } @@ -101,7 +102,7 @@ class UserController extends Controller /** * Show the form for editing the specified user. * @param int $id - * @param SocialAuthService $socialAuthService + * @param \BookStack\Auth\Access\SocialAuthService $socialAuthService * @return Response */ public function edit($id, SocialAuthService $socialAuthService) @@ -123,8 +124,9 @@ class UserController extends Controller /** * Update the specified user in storage. * @param Request $request - * @param int $id + * @param int $id * @return Response + * @throws UserUpdateException */ public function update(Request $request, $id) { @@ -141,13 +143,13 @@ class UserController extends Controller 'setting' => 'array' ]); - $user = $this->user->findOrFail($id); + $user = $this->userRepo->getById($id); $user->fill($request->all()); // Role updates if (userCan('users-manage') && $request->filled('roles')) { $roles = $request->get('roles'); - $user->roles()->sync($roles); + $this->userRepo->setUserRoles($user, $roles); } // Password updates @@ -186,7 +188,7 @@ class UserController extends Controller return $this->currentUser->id == $id; }); - $user = $this->user->findOrFail($id); + $user = $this->userRepo->getById($id); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); return view('users/delete', ['user' => $user]); } @@ -195,6 +197,7 @@ class UserController extends Controller * Remove the specified user from storage. * @param int $id * @return Response + * @throws \Exception */ public function destroy($id) { @@ -252,7 +255,7 @@ class UserController extends Controller return $this->currentUser->id == $id; }); - $viewType = $request->get('book_view_type'); + $viewType = $request->get('view_type'); if (!in_array($viewType, ['grid', 'list'])) { $viewType = 'list'; } @@ -262,4 +265,27 @@ class UserController extends Controller return redirect()->back(302, [], "/settings/users/$id"); } + + /** + * Update the user's preferred shelf-list display setting. + * @param $id + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function switchShelfView($id, Request $request) + { + $this->checkPermissionOr('users-manage', function () use ($id) { + return $this->currentUser->id == $id; + }); + + $viewType = $request->get('view_type'); + if (!in_array($viewType, ['grid', 'list'])) { + $viewType = 'list'; + } + + $user = $this->userRepo->getById($id); + setting()->putUser($user, 'bookshelves_view_type', $viewType); + + return redirect()->back(302, [], "/settings/users/$id"); + } } diff --git a/app/Http/Middleware/Localization.php b/app/Http/Middleware/Localization.php index 466c1442b..528ff4047 100644 --- a/app/Http/Middleware/Localization.php +++ b/app/Http/Middleware/Localization.php @@ -2,9 +2,13 @@ use Carbon\Carbon; use Closure; +use Illuminate\Http\Request; class Localization { + + protected $rtlLocales = ['ar']; + /** * Handle an incoming request. * @@ -15,21 +19,38 @@ class Localization public function handle($request, Closure $next) { $defaultLang = config('app.locale'); - if (user()->isDefault()) { - $locale = $defaultLang; - $availableLocales = config('app.locales'); - foreach ($request->getLanguages() as $lang) { - if (!in_array($lang, $availableLocales)) { - continue; - } - $locale = $lang; - break; - } + + if (user()->isDefault() && config('app.auto_detect_locale')) { + $locale = $this->autoDetectLocale($request, $defaultLang); } else { $locale = setting()->getUser(user(), 'language', $defaultLang); } + + // Set text direction + if (in_array($locale, $this->rtlLocales)) { + config()->set('app.rtl', true); + } + app()->setLocale($locale); Carbon::setLocale($locale); return $next($request); } + + /** + * Autodetect the visitors locale by matching locales in their headers + * against the locales supported by BookStack. + * @param Request $request + * @param string $default + * @return string + */ + protected function autoDetectLocale(Request $request, string $default) + { + $availableLocales = config('app.locales'); + foreach ($request->getLanguages() as $lang) { + if (in_array($lang, $availableLocales)) { + return $lang; + } + } + return $default; + } } diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index c3102571d..73c11a827 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -3,8 +3,8 @@ namespace BookStack\Http\Middleware; use Closure; -use Illuminate\Http\Request; use Fideloper\Proxy\TrustProxies as Middleware; +use Illuminate\Http\Request; class TrustProxies extends Middleware { diff --git a/app/Notifications/ConfirmEmail.php b/app/Notifications/ConfirmEmail.php index 858b12166..7ecadc298 100644 --- a/app/Notifications/ConfirmEmail.php +++ b/app/Notifications/ConfirmEmail.php @@ -1,17 +1,7 @@ -token = $token; } - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * @return array - */ - public function via($notifiable) - { - return ['mail']; - } - /** * Get the mail representation of the notification. * @@ -43,10 +22,10 @@ class ConfirmEmail extends Notification implements ShouldQueue public function toMail($notifiable) { $appName = ['appName' => setting('app-name')]; - return (new MailMessage) - ->subject(trans('auth.email_confirm_subject', $appName)) - ->greeting(trans('auth.email_confirm_greeting', $appName)) - ->line(trans('auth.email_confirm_text')) - ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token)); + return $this->newMailMessage() + ->subject(trans('auth.email_confirm_subject', $appName)) + ->greeting(trans('auth.email_confirm_greeting', $appName)) + ->line(trans('auth.email_confirm_text')) + ->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token)); } } diff --git a/app/Notifications/MailNotification.php b/app/Notifications/MailNotification.php new file mode 100644 index 000000000..413ac6d73 --- /dev/null +++ b/app/Notifications/MailNotification.php @@ -0,0 +1,35 @@ +view([ + 'html' => 'vendor.notifications.email', + 'text' => 'vendor.notifications.email-plain' + ]); + } + +} \ No newline at end of file diff --git a/app/Notifications/ResetPassword.php b/app/Notifications/ResetPassword.php index affd8f076..282aa335a 100644 --- a/app/Notifications/ResetPassword.php +++ b/app/Notifications/ResetPassword.php @@ -1,11 +1,7 @@ -token = $token; } - /** - * Get the notification's channels. - * - * @param mixed $notifiable - * @return array|string - */ - public function via($notifiable) - { - return ['mail']; - } - /** * Build the mail representation of the notification. * @@ -42,7 +27,7 @@ class ResetPassword extends Notification */ public function toMail() { - return (new MailMessage) + return $this->newMailMessage() ->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')])) ->line(trans('auth.email_reset_text')) ->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token)) diff --git a/app/Ownable.php b/app/Ownable.php index fe58e05ed..e660a0500 100644 --- a/app/Ownable.php +++ b/app/Ownable.php @@ -1,5 +1,7 @@ getMimeType(), $imageMimes); }); - \Blade::directive('icon', function ($expression) { + // Custom blade view directives + Blade::directive('icon', function ($expression) { return ""; }); // Allow longer string lengths after upgrade to utf8mb4 - \Schema::defaultStringLength(191); + Schema::defaultStringLength(191); + + // Set morph-map due to namespace changes + Relation::morphMap([ + 'BookStack\\Bookshelf' => Bookshelf::class, + 'BookStack\\Book' => Book::class, + 'BookStack\\Chapter' => Chapter::class, + 'BookStack\\Page' => Page::class, + ]); } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index d1fac56e6..6e5b6ffde 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,7 +3,7 @@ namespace BookStack\Providers; use Auth; -use BookStack\Services\LdapService; +use BookStack\Auth\Access\LdapService; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php index 11e3cc6d2..69925e945 100644 --- a/app/Providers/BroadcastServiceProvider.php +++ b/app/Providers/BroadcastServiceProvider.php @@ -3,7 +3,6 @@ namespace BookStack\Providers; use Illuminate\Support\ServiceProvider; -use Illuminate\Support\Facades\Broadcast; class BroadcastServiceProvider extends ServiceProvider { diff --git a/app/Providers/CustomFacadeProvider.php b/app/Providers/CustomFacadeProvider.php index c81a5529d..5508ee9cd 100644 --- a/app/Providers/CustomFacadeProvider.php +++ b/app/Providers/CustomFacadeProvider.php @@ -2,18 +2,19 @@ namespace BookStack\Providers; -use BookStack\Activity; -use BookStack\Image; -use BookStack\Services\ImageService; -use BookStack\Services\PermissionService; -use BookStack\Services\ViewService; -use BookStack\Setting; -use BookStack\View; +use BookStack\Actions\Activity; +use BookStack\Actions\ActivityService; +use BookStack\Actions\View; +use BookStack\Actions\ViewService; +use BookStack\Auth\Permissions\PermissionService; +use BookStack\Settings\Setting; +use BookStack\Settings\SettingService; +use BookStack\Uploads\HttpFetcher; +use BookStack\Uploads\Image; +use BookStack\Uploads\ImageService; use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Support\ServiceProvider; -use BookStack\Services\ActivityService; -use BookStack\Services\SettingService; use Intervention\Image\ImageManager; class CustomFacadeProvider extends ServiceProvider @@ -61,7 +62,8 @@ class CustomFacadeProvider extends ServiceProvider $this->app->make(Image::class), $this->app->make(ImageManager::class), $this->app->make(Factory::class), - $this->app->make(Repository::class) + $this->app->make(Repository::class), + $this->app->make(HttpFetcher::class) ); }); } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index cc3e4d993..a826185d8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,7 +2,6 @@ namespace BookStack\Providers; -use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use SocialiteProviders\Manager\SocialiteWasCalled; @@ -20,6 +19,7 @@ class EventServiceProvider extends ServiceProvider 'SocialiteProviders\Okta\OktaExtendSocialite@handle', 'SocialiteProviders\GitLab\GitLabExtendSocialite@handle', 'SocialiteProviders\Twitch\TwitchExtendSocialite@handle', + 'SocialiteProviders\Discord\DiscordExtendSocialite@handle', ], ]; diff --git a/app/Providers/LdapUserProvider.php b/app/Providers/LdapUserProvider.php index 1dc789c3b..9c91def2f 100644 --- a/app/Providers/LdapUserProvider.php +++ b/app/Providers/LdapUserProvider.php @@ -2,9 +2,7 @@ namespace BookStack\Providers; -use BookStack\Role; -use BookStack\Services\LdapService; -use BookStack\User; +use BookStack\Auth\Access\LdapService; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; @@ -19,7 +17,7 @@ class LdapUserProvider implements UserProvider protected $model; /** - * @var LdapService + * @var \BookStack\Auth\LdapService */ protected $ldapService; @@ -27,7 +25,7 @@ class LdapUserProvider implements UserProvider /** * LdapUserProvider constructor. * @param $model - * @param LdapService $ldapService + * @param \BookStack\Auth\LdapService $ldapService */ public function __construct($model, LdapService $ldapService) { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 88ab23526..c4c39d534 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,7 +2,6 @@ namespace BookStack\Providers; -use Illuminate\Routing\Router; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Route; diff --git a/app/Providers/TranslationServiceProvider.php b/app/Providers/TranslationServiceProvider.php new file mode 100644 index 000000000..0e628c7da --- /dev/null +++ b/app/Providers/TranslationServiceProvider.php @@ -0,0 +1,32 @@ +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; + }); + } +} \ No newline at end of file diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php deleted file mode 100644 index 3eb2f2830..000000000 --- a/app/Services/LdapService.php +++ /dev/null @@ -1,165 +0,0 @@ -ldap = $ldap; - $this->config = config('services.ldap'); - } - - /** - * Get the details of a user from LDAP using the given username. - * User found via configurable user filter. - * @param $userName - * @return array|null - * @throws LdapException - */ - public function getUserDetails($userName) - { - $ldapConnection = $this->getConnection(); - $this->bindSystemUser($ldapConnection); - - // Find user - $userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]); - $baseDn = $this->config['base_dn']; - $emailAttr = $this->config['email_attribute']; - $followReferrals = $this->config['follow_referrals'] ? 1 : 0; - $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); - $users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]); - if ($users['count'] === 0) { - return null; - } - - $user = $users[0]; - return [ - 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'], - 'name' => $user['cn'][0], - 'dn' => $user['dn'], - 'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null - ]; - } - - /** - * @param Authenticatable $user - * @param string $username - * @param string $password - * @return bool - * @throws LdapException - */ - public function validateUserCredentials(Authenticatable $user, $username, $password) - { - $ldapUser = $this->getUserDetails($username); - if ($ldapUser === null) { - return false; - } - if ($ldapUser['uid'] !== $user->external_auth_id) { - return false; - } - - $ldapConnection = $this->getConnection(); - try { - $ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password); - } catch (\ErrorException $e) { - $ldapBind = false; - } - - return $ldapBind; - } - - /** - * Bind the system user to the LDAP connection using the given credentials - * otherwise anonymous access is attempted. - * @param $connection - * @throws LdapException - */ - protected function bindSystemUser($connection) - { - $ldapDn = $this->config['dn']; - $ldapPass = $this->config['pass']; - - $isAnonymous = ($ldapDn === false || $ldapPass === false); - if ($isAnonymous) { - $ldapBind = $this->ldap->bind($connection); - } else { - $ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass); - } - - if (!$ldapBind) { - throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed'))); - } - } - - /** - * Get the connection to the LDAP server. - * Creates a new connection if one does not exist. - * @return resource - * @throws LdapException - */ - protected function getConnection() - { - if ($this->ldapConnection !== null) { - return $this->ldapConnection; - } - - // Check LDAP extension in installed - if (!function_exists('ldap_connect') && config('app.env') !== 'testing') { - throw new LdapException(trans('errors.ldap_extension_not_installed')); - } - - // Get port from server string and protocol if specified. - $ldapServer = explode(':', $this->config['server']); - $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1; - if (!$hasProtocol) { - array_unshift($ldapServer, ''); - } - $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1]; - $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389; - $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort); - - if ($ldapConnection === false) { - throw new LdapException(trans('errors.ldap_cannot_connect')); - } - - // Set any required options - if ($this->config['version']) { - $this->ldap->setVersion($ldapConnection, $this->config['version']); - } - - $this->ldapConnection = $ldapConnection; - return $this->ldapConnection; - } - - /** - * Build a filter string by injecting common variables. - * @param string $filterString - * @param array $attrs - * @return string - */ - protected function buildFilter($filterString, array $attrs) - { - $newAttrs = []; - foreach ($attrs as $key => $attrText) { - $newKey = '${' . $key . '}'; - $newAttrs[$newKey] = $attrText; - } - return strtr($filterString, $newAttrs); - } -} diff --git a/app/Setting.php b/app/Settings/Setting.php similarity index 68% rename from app/Setting.php rename to app/Settings/Setting.php index 0af3652db..1a52920ee 100644 --- a/app/Setting.php +++ b/app/Settings/Setting.php @@ -1,4 +1,6 @@ - '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; + } + +} \ No newline at end of file diff --git a/app/Attachment.php b/app/Uploads/Attachment.php similarity index 84% rename from app/Attachment.php rename to app/Uploads/Attachment.php index 6749130d9..eb9a0fe68 100644 --- a/app/Attachment.php +++ b/app/Uploads/Attachment.php @@ -1,4 +1,7 @@ - $uri, + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_CONNECTTIMEOUT => 5 + ]); + + $data = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + + if ($err) { + throw new HttpFetchException($err); + } + + return $data; + } + +} \ No newline at end of file diff --git a/app/Image.php b/app/Uploads/Image.php similarity index 88% rename from app/Image.php rename to app/Uploads/Image.php index 412beea90..df6d9fb0d 100644 --- a/app/Image.php +++ b/app/Uploads/Image.php @@ -1,5 +1,6 @@ -image = $image; $this->imageTool = $imageTool; $this->cache = $cache; + $this->http = $http; parent::__construct($fileSystem); } @@ -96,8 +99,9 @@ class ImageService extends UploadService private function saveNewFromUrl($url, $type, $imageName = false) { $imageName = $imageName ? $imageName : basename($url); - $imageData = file_get_contents($url); - if ($imageData === false) { + try { + $imageData = $this->http->fetch($url); + } catch (HttpFetchException $exception) { throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url])); } return $this->saveNew($imageName, $imageData, $type); @@ -280,24 +284,57 @@ class ImageService extends UploadService } /** - * Save a gravatar image and set a the profile image for a user. - * @param User $user + * Save an avatar image from an external service. + * @param \BookStack\Auth\User $user * @param int $size - * @return mixed + * @return Image * @throws Exception */ - public function saveUserGravatar(User $user, $size = 500) + public function saveUserAvatar(User $user, $size = 500) { - $emailHash = md5(strtolower(trim($user->email))); - $url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon'; - $imageName = str_replace(' ', '-', $user->name . '-gravatar.png'); - $image = $this->saveNewFromUrl($url, 'user', $imageName); + $avatarUrl = $this->getAvatarUrl(); + $email = strtolower(trim($user->email)); + + $replacements = [ + '${hash}' => md5($email), + '${size}' => $size, + '${email}' => urlencode($email), + ]; + + $userAvatarUrl = strtr($avatarUrl, $replacements); + $imageName = str_replace(' ', '-', $user->name . '-avatar.png'); + $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName); $image->created_by = $user->id; $image->updated_by = $user->id; $image->save(); + return $image; } + /** + * Check if fetching external avatars is enabled. + * @return bool + */ + public function avatarFetchEnabled() + { + $fetchUrl = $this->getAvatarUrl(); + return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0; + } + + /** + * Get the URL to fetch avatars from. + * @return string|mixed + */ + protected function getAvatarUrl() + { + $url = trim(config('services.avatar_url')); + + if (empty($url) && !config('services.disable_services')) { + $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon'; + } + + return $url; + } /** * Delete gallery and drawings that are not within HTML content of pages or page revisions. @@ -316,25 +353,25 @@ class ImageService extends UploadService $deletedPaths = []; $this->image->newQuery()->whereIn('type', $types) - ->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) { - foreach ($images as $image) { - $searchQuery = '%' . basename($image->path) . '%'; - $inPage = DB::table('pages') + ->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) { + foreach ($images as $image) { + $searchQuery = '%' . basename($image->path) . '%'; + $inPage = DB::table('pages') ->where('html', 'like', $searchQuery)->count() > 0; - $inRevision = false; - if ($checkRevisions) { - $inRevision = DB::table('page_revisions') + $inRevision = false; + if ($checkRevisions) { + $inRevision = DB::table('page_revisions') ->where('html', 'like', $searchQuery)->count() > 0; - } + } - if (!$inPage && !$inRevision) { - $deletedPaths[] = $image->path; - if (!$dryRun) { - $this->destroy($image); - } - } - } - }); + if (!$inPage && !$inRevision) { + $deletedPaths[] = $image->path; + if (!$dryRun) { + $this->destroy($image); + } + } + } + }); return $deletedPaths; } @@ -366,14 +403,7 @@ class ImageService extends UploadService } } else { try { - $ch = curl_init(); - curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]); - $imageData = curl_exec($ch); - $err = curl_error($ch); - curl_close($ch); - if ($err) { - throw new \Exception("Image fetch failed, Received error: " . $err); - } + $imageData = $this->http->fetch($uri); } catch (\Exception $e) { } } diff --git a/app/Services/UploadService.php b/app/Uploads/UploadService.php similarity index 93% rename from app/Services/UploadService.php rename to app/Uploads/UploadService.php index df597a40f..292e61e30 100644 --- a/app/Services/UploadService.php +++ b/app/Uploads/UploadService.php @@ -1,9 +1,9 @@ -user() ?: \BookStack\User::getDefault(); + return auth()->user() ?: \BookStack\Auth\User::getDefault(); } /** @@ -61,7 +61,7 @@ function userCan($permission, Ownable $ownable = null) } // Check permission on ownable item - $permissionService = app(\BookStack\Services\PermissionService::class); + $permissionService = app(\BookStack\Auth\Permissions\PermissionService::class); return $permissionService->checkOwnableUserAccess($ownable, $permission); } @@ -69,11 +69,11 @@ function userCan($permission, Ownable $ownable = null) * Helper to access system settings. * @param $key * @param bool $default - * @return bool|string|\BookStack\Services\SettingService + * @return bool|string|\BookStack\Settings\SettingService */ function setting($key = null, $default = false) { - $settingService = resolve(\BookStack\Services\SettingService::class); + $settingService = resolve(\BookStack\Settings\SettingService::class); if (is_null($key)) { return $settingService; } @@ -92,10 +92,15 @@ function baseUrl($path, $forceAppDomain = false) if ($isFullUrl && !$forceAppDomain) { return $path; } + $path = trim($path, '/'); + $base = rtrim(config('app.url'), '/'); // Remove non-specified domain if forced and we have a domain if ($isFullUrl && $forceAppDomain) { + if (!empty($base) && strpos($path, $base) === 0) { + $path = trim(substr($path, strlen($base) - 1)); + } $explodedPath = explode('/', $path); $path = implode('/', array_splice($explodedPath, 3)); } @@ -105,7 +110,7 @@ function baseUrl($path, $forceAppDomain = false) return url($path); } - return rtrim(config('app.url'), '/') . '/' . $path; + return $base . '/' . $path; } /** diff --git a/composer.json b/composer.json index 3de0cb5f7..48b977e23 100644 --- a/composer.json +++ b/composer.json @@ -5,10 +5,16 @@ "license": "MIT", "type": "project", "require": { - "php": ">=7.0.0", - "laravel/framework": "~5.5.22", - "fideloper/proxy": "~3.3", + "php": ">=7.0.5", + "ext-json": "*", "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", "league/flysystem-aws-s3-v3": "^1.0", @@ -20,7 +26,9 @@ "socialiteproviders/microsoft-azure": "^3.0", "socialiteproviders/okta": "^1.0", "socialiteproviders/gitlab": "^3.0", - "socialiteproviders/twitch": "^3.0" + "socialiteproviders/twitch": "^3.0", + "socialiteproviders/discord": "^2.0", + "doctrine/dbal": "^2.5" }, "require-dev": { "filp/whoops": "~2.0", @@ -79,7 +87,7 @@ "optimize-autoloader": true, "preferred-install": "dist", "platform": { - "php": "7.0" + "php": "7.0.5" } } } diff --git a/composer.lock b/composer.lock index 6e0a35323..c524c0999 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "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": "3bf33ab103b15b06ca06c85fd8ae3b78", + "content-hash": "06219a5c2419ca23ec2924eb31f4ed16", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.56.4", + "version": "3.82.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee" + "reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee", - "reference": "03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7", + "reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7", "shasum": "" }, "require": { @@ -25,7 +25,7 @@ "ext-pcre": "*", "ext-simplexml": "*", "ext-spl": "*", - "guzzlehttp/guzzle": "^5.3.1|^6.2.1", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1", "guzzlehttp/promises": "~1.0", "guzzlehttp/psr7": "^1.4.1", "mtdowling/jmespath.php": "~2.2", @@ -38,6 +38,8 @@ "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", "nette/neon": "^2.3", "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0" @@ -46,7 +48,8 @@ "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", "doctrine/cache": "To use the DoctrineCacheAdapter", "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages" + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" }, "type": "library", "extra": { @@ -84,26 +87,26 @@ "s3", "sdk" ], - "time": "2018-05-18T19:53:15+00:00" + "time": "2018-12-21T22:21:50+00:00" }, { "name": "barryvdh/laravel-dompdf", - "version": "v0.8.2", + "version": "v0.8.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-dompdf.git", - "reference": "7dcdecfa125c174d0abe723603633dc2756ea3af" + "reference": "46781d0304277845a19c09c169bc595fd182cce4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7dcdecfa125c174d0abe723603633dc2756ea3af", - "reference": "7dcdecfa125c174d0abe723603633dc2756ea3af", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/46781d0304277845a19c09c169bc595fd182cce4", + "reference": "46781d0304277845a19c09c169bc595fd182cce4", "shasum": "" }, "require": { "dompdf/dompdf": "^0.8", - "illuminate/support": "5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", - "php": ">=5.5.9" + "illuminate/support": "5.5.x|5.6.x|5.7.x", + "php": ">=7" }, "type": "library", "extra": { @@ -140,27 +143,27 @@ "laravel", "pdf" ], - "time": "2018-02-07T17:43:25+00:00" + "time": "2018-08-31T13:25:44+00:00" }, { "name": "barryvdh/laravel-snappy", - "version": "v0.4.1", + "version": "v0.4.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-snappy.git", - "reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe" + "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/5f6e7f3ba15c867d1b8e2885d454110270616ebe", - "reference": "5f6e7f3ba15c867d1b8e2885d454110270616ebe", + "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/62bb5017b7004bf3e48bfed3d5c00d3dc6e60478", + "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478", "shasum": "" }, "require": { - "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", - "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x|5.4.x|5.5.x|5.6.x", + "illuminate/filesystem": "5.5.x|5.6.x|5.7.x", + "illuminate/support": "5.5.x|5.6.x|5.7.x", "knplabs/knp-snappy": "^1", - "php": ">=5.4.0" + "php": ">=7" }, "type": "library", "extra": { @@ -201,7 +204,7 @@ "wkhtmltoimage", "wkhtmltopdf" ], - "time": "2018-02-08T15:58:26+00:00" + "time": "2018-09-06T10:14:15+00:00" }, { "name": "cogpowered/finediff", @@ -254,6 +257,355 @@ ], "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", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", + "shasum": "" + }, + "require": { + "php": "~5.5|~7.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.8|~5.0", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "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": "Caching library offering an object-oriented API for many cache backends", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "cache", + "caching" + ], + "time": "2017-07-22T12:49:21+00:00" + }, + { + "name": "doctrine/collections", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/collections.git", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Collections\\": "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": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Collections Abstraction library", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "array", + "collections", + "iterator" + ], + "time": "2017-01-03T10:49:41+00:00" + }, + { + "name": "doctrine/common", + "version": "v2.7.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9", + "shasum": "" + }, + "require": { + "doctrine/annotations": "1.*", + "doctrine/cache": "1.*", + "doctrine/collections": "1.*", + "doctrine/inflector": "1.*", + "doctrine/lexer": "1.*", + "php": "~5.6|~7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "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": "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" + } + ], + "description": "Database Abstraction Layer", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "database", + "dbal", + "persistence", + "queryobject" + ], + "time": "2017-07-22T20:44:48+00:00" + }, { "name": "doctrine/inflector", "version": "v1.2.0", @@ -377,30 +729,34 @@ }, { "name": "dompdf/dompdf", - "version": "v0.8.2", + "version": "v0.8.3", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "5113accd9ae5d466077cce5208dcf3fb871bf8f6" + "reference": "75f13c700009be21a1965dc2c5b68a8708c22ba2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/5113accd9ae5d466077cce5208dcf3fb871bf8f6", - "reference": "5113accd9ae5d466077cce5208dcf3fb871bf8f6", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/75f13c700009be21a1965dc2c5b68a8708c22ba2", + "reference": "75f13c700009be21a1965dc2c5b68a8708c22ba2", "shasum": "" }, "require": { "ext-dom": "*", - "ext-gd": "*", "ext-mbstring": "*", "phenx/php-font-lib": "0.5.*", "phenx/php-svg-lib": "0.3.*", "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "4.8.*", + "phpunit/phpunit": "^4.8|^5.5|^6.5", "squizlabs/php_codesniffer": "2.*" }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance" + }, "type": "library", "extra": { "branch-alias": { @@ -435,20 +791,20 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", - "time": "2017-11-26T14:49:08+00:00" + "time": "2018-12-14T02:40:31+00:00" }, { "name": "egulias/email-validator", - "version": "2.1.4", + "version": "2.1.7", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "8790f594151ca6a2010c6218e09d96df67173ad3" + "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/8790f594151ca6a2010c6218e09d96df67173ad3", - "reference": "8790f594151ca6a2010c6218e09d96df67173ad3", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e", + "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e", "shasum": "" }, "require": { @@ -492,7 +848,7 @@ "validation", "validator" ], - "time": "2018-04-10T10:11:19+00:00" + "time": "2018-12-04T22:38:24+00:00" }, { "name": "erusev/parsedown", @@ -764,32 +1120,33 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.4.2", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" + "reference": "9f83dded91781a01c63574e387eaa769be769115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", - "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -819,26 +1176,27 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-03-20T17:10:46+00:00" + "time": "2018-12-04T20:46:45+00:00" }, { "name": "intervention/image", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "3603dbcc9a17d307533473246a6c58c31cf17919" + "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/3603dbcc9a17d307533473246a6c58c31cf17919", - "reference": "3603dbcc9a17d307533473246a6c58c31cf17919", + "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb", + "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb", "shasum": "" }, "require": { @@ -858,7 +1216,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.4-dev" }, "laravel": { "providers": [ @@ -895,7 +1253,7 @@ "thumbnail", "watermark" ], - "time": "2017-09-21T16:29:17+00:00" + "time": "2018-05-29T14:19:03+00:00" }, { "name": "knplabs/knp-snappy", @@ -965,16 +1323,16 @@ }, { "name": "laravel/framework", - "version": "v5.5.40", + "version": "v5.5.44", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "d724ce0aa61bbd9adf658215eec484f5dd6711d6" + "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/d724ce0aa61bbd9adf658215eec484f5dd6711d6", - "reference": "d724ce0aa61bbd9adf658215eec484f5dd6711d6", + "url": "https://api.github.com/repos/laravel/framework/zipball/00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b", + "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b", "shasum": "" }, "require": { @@ -1095,20 +1453,20 @@ "framework", "laravel" ], - "time": "2018-03-30T13:29:30+00:00" + "time": "2018-10-04T14:51:24+00:00" }, { "name": "laravel/socialite", - "version": "v3.0.11", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "4d29ba66fdb38ec994b778e5e51657555cc10511" + "reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/4d29ba66fdb38ec994b778e5e51657555cc10511", - "reference": "4d29ba66fdb38ec994b778e5e51657555cc10511", + "url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481", + "reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481", "shasum": "" }, "require": { @@ -1117,7 +1475,7 @@ "illuminate/http": "~5.4", "illuminate/support": "~5.4", "league/oauth1-client": "~1.0", - "php": ">=5.4.0" + "php": ">=5.6.4" }, "require-dev": { "mockery/mockery": "~0.9", @@ -1153,36 +1511,37 @@ } ], "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", "keywords": [ "laravel", "oauth" ], - "time": "2018-05-12T17:44:53+00:00" + "time": "2018-10-18T03:39:04+00:00" }, { "name": "league/flysystem", - "version": "1.0.45", + "version": "1.0.49", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6" + "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a99f94e63b512d75f851b181afcdf0ee9ebef7e6", - "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd", + "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd", "shasum": "" }, "require": { + "ext-fileinfo": "*", "php": ">=5.5.9" }, "conflict": { "league/flysystem-sftp": "<1.0.6" }, "require-dev": { - "ext-fileinfo": "*", "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^5.7.10" }, "suggest": { "ext-fileinfo": "Required for MimeType", @@ -1241,20 +1600,20 @@ "sftp", "storage" ], - "time": "2018-05-07T08:44:23+00:00" + "time": "2018-11-23T23:41:29+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "1.0.19", + "version": "1.0.21", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "f135691ef6761542af301b7c9880f140fb12dc74" + "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/f135691ef6761542af301b7c9880f140fb12dc74", - "reference": "f135691ef6761542af301b7c9880f140fb12dc74", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/43523fec10a831ea48bedb3277e3f3fa218f4e49", + "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49", "shasum": "" }, "require": { @@ -1288,7 +1647,7 @@ } ], "description": "Flysystem adapter for the AWS S3 SDK v3.x", - "time": "2018-03-27T20:33:59+00:00" + "time": "2018-10-08T07:53:55+00:00" }, { "name": "league/oauth1-client", @@ -1355,16 +1714,16 @@ }, { "name": "monolog/monolog", - "version": "1.23.0", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", - "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", "shasum": "" }, "require": { @@ -1429,7 +1788,7 @@ "logging", "psr-3" ], - "time": "2017-06-19T01:22:40+00:00" + "time": "2018-11-05T09:00:11+00:00" }, { "name": "mtdowling/cron-expression", @@ -1532,16 +1891,16 @@ }, { "name": "nesbot/carbon", - "version": "1.27.0", + "version": "1.36.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9" + "reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/ef81c39b67200dcd7401c24363dcac05ac3a4fe9", - "reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/63da8cdf89d7a5efe43aabc794365f6e7b7b8983", + "reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983", "shasum": "" }, "require": { @@ -1549,10 +1908,20 @@ "symfony/translation": "~2.6 || ~3.0 || ~4.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2", "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." + }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + } + }, "autoload": { "psr-4": { "": "src/" @@ -1576,37 +1945,33 @@ "datetime", "time" ], - "time": "2018-04-23T09:02:57+00:00" + "time": "2018-11-22T18:23:02+00:00" }, { "name": "paragonie/random_compat", - "version": "v2.0.12", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb", - "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1621,10 +1986,11 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2018-04-04T21:24:14+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { "name": "phenx/php-font-lib", @@ -1665,16 +2031,16 @@ }, { "name": "phenx/php-svg-lib", - "version": "v0.3", + "version": "v0.3.2", "source": { "type": "git", "url": "https://github.com/PhenX/php-svg-lib.git", - "reference": "a85f7fe9fe08d093a4a8583cdd306b553ff918aa" + "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/a85f7fe9fe08d093a4a8583cdd306b553ff918aa", - "reference": "a85f7fe9fe08d093a4a8583cdd306b553ff918aa", + "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/ccc46ef6340d4b8a4a68047e68d8501ea961442c", + "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c", "shasum": "" }, "require": { @@ -1701,7 +2067,7 @@ ], "description": "A library to read, parse and export to PDF SVG files.", "homepage": "https://github.com/PhenX/php-svg-lib", - "time": "2017-05-24T10:07:27+00:00" + "time": "2018-06-03T10:10:03+00:00" }, { "name": "predis/predis", @@ -1854,16 +2220,16 @@ }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -1897,7 +2263,7 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "psr/simple-cache", @@ -1948,22 +2314,63 @@ "time": "2017-10-23T01:57:42+00:00" }, { - "name": "ramsey/uuid", - "version": "3.7.3", + "name": "ralouphie/getallheaders", + "version": "2.0.5", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/44abcdad877d9a46685a3a4d221e3b2c4b87cb76", - "reference": "44abcdad877d9a46685a3a4d221e3b2c4b87cb76", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", "shasum": "" }, "require": { - "paragonie/random_compat": "^1.0|^2.0", - "php": "^5.4 || ^7.0" + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, + { + "name": "ramsey/uuid", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "^1.0|^2.0|9.99.99", + "php": "^5.4 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "replace": { "rhumsaa/uuid": "self.version" @@ -1971,16 +2378,17 @@ "require-dev": { "codeception/aspect-mock": "^1.0 | ~2.0.0", "doctrine/annotations": "~1.2.0", - "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", "ircmaxell/random-lib": "^1.1", "jakub-onderka/php-parallel-lint": "^0.9.0", "mockery/mockery": "^0.9.9", "moontoast/math": "^1.1", "php-mock/php-mock-phpunit": "^0.3|^1.1", - "phpunit/phpunit": "^4.7|^5.0", + "phpunit/phpunit": "^4.7|^5.0|^6.5", "squizlabs/php_codesniffer": "^2.3" }, "suggest": { + "ext-ctype": "Provides support for PHP Ctype functions", "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", @@ -2025,7 +2433,7 @@ "identifier", "uuid" ], - "time": "2018-01-20T00:28:24+00:00" + "time": "2018-07-19T23:38:55+00:00" }, { "name": "sabberworm/php-css-parser", @@ -2072,17 +2480,54 @@ "time": "2016-07-19T19:14:21+00:00" }, { - "name": "socialiteproviders/gitlab", - "version": "v3.0.2", + "name": "socialiteproviders/discord", + "version": "v2.0.2", "source": { "type": "git", - "url": "https://github.com/SocialiteProviders/GitLab.git", - "reference": "bab80e8e16853e062c58013b1c1f474bd5a5c49a" + "url": "https://github.com/SocialiteProviders/Discord.git", + "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/bab80e8e16853e062c58013b1c1f474bd5a5c49a", - "reference": "bab80e8e16853e062c58013b1c1f474bd5a5c49a", + "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/e0cd8895f321943b36f533e7bf21ad29bcdece9a", + "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "socialiteproviders/manager": "~2.0 || ~3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Discord\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Discord OAuth2 Provider for Laravel Socialite", + "time": "2018-05-26T03:40:07+00:00" + }, + { + "name": "socialiteproviders/gitlab", + "version": "v3.1", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/GitLab.git", + "reference": "69e537f6192ca15483e98b8662495384f44299ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/69e537f6192ca15483e98b8662495384f44299ca", + "reference": "69e537f6192ca15483e98b8662495384f44299ca", "shasum": "" }, "require": { @@ -2106,7 +2551,7 @@ } ], "description": "GitLab OAuth2 Provider for Laravel Socialite", - "time": "2018-05-11T03:10:27+00:00" + "time": "2018-06-27T05:10:32+00:00" }, { "name": "socialiteproviders/manager", @@ -2307,16 +2752,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v6.0.2", + "version": "v6.1.3", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "412333372fb6c8ffb65496a2bbd7321af75733fc" + "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/412333372fb6c8ffb65496a2bbd7321af75733fc", - "reference": "412333372fb6c8ffb65496a2bbd7321af75733fc", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8ddcb66ac10c392d3beb54829eef8ac1438595f4", + "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4", "shasum": "" }, "require": { @@ -2327,10 +2772,14 @@ "mockery/mockery": "~0.9.1", "symfony/phpunit-bridge": "~3.3@dev" }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses", + "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.0-dev" + "dev-master": "6.1-dev" } }, "autoload": { @@ -2352,13 +2801,13 @@ } ], "description": "Swiftmailer, free feature-rich PHP mailer", - "homepage": "http://swiftmailer.symfony.com", + "homepage": "https://swiftmailer.symfony.com", "keywords": [ "email", "mail", "mailer" ], - "time": "2017-09-30T22:39:41+00:00" + "time": "2018-09-11T07:12:52+00:00" }, { "name": "symfony/console", @@ -2790,17 +3239,75 @@ "time": "2017-08-01T10:25:59+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "name": "symfony/polyfill-ctype", + "version": "v1.10.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2018-08-06T14:22:27+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494", + "reference": "c79c051f5b3a46be09205c73b80b346e4153e494", "shasum": "" }, "require": { @@ -2812,7 +3319,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -2846,7 +3353,7 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-09-21T13:07:52+00:00" }, { "name": "symfony/process", @@ -3157,28 +3664,28 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.4.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c" + "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", - "reference": "3cc116adbe4b11be5ec557bf1d24dc5e3a21d18c", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", + "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" + "phpunit/phpunit": "^4.8.35 || ^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -3188,7 +3695,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause-Attribution" + "BSD-3-Clause" ], "authors": [ { @@ -3203,35 +3710,35 @@ "env", "environment" ], - "time": "2016-09-01T10:05:43+00:00" + "time": "2018-07-29T20:33:41+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.1.4", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "7a91480cc6e597caed5117a3c5d685f06d35c5a1" + "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/7a91480cc6e597caed5117a3c5d685f06d35c5a1", - "reference": "7a91480cc6e597caed5117a3c5d685f06d35c5a1", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c", + "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c", "shasum": "" }, "require": { - "illuminate/routing": "5.5.x|5.6.x", - "illuminate/session": "5.5.x|5.6.x", - "illuminate/support": "5.5.x|5.6.x", + "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", "maximebf/debugbar": "~1.15.0", "php": ">=7.0", "symfony/debug": "^3|^4", "symfony/finder": "^3|^4" }, "require-dev": { - "illuminate/framework": "5.5.x" + "laravel/framework": "5.5.x" }, "type": "library", "extra": { @@ -3273,37 +3780,38 @@ "profiler", "webprofiler" ], - "time": "2018-03-06T08:35:31+00:00" + "time": "2018-11-09T08:37:55+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.4.3", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd" + "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5c304db44fba8e9c4aa0c09739e59f7be7736fdd", - "reference": "5c304db44fba8e9c4aa0c09739e59f7be7736fdd", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2", + "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2", "shasum": "" }, "require": { - "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.7", - "illuminate/filesystem": "^5.0,<5.7", - "illuminate/support": "^5.0,<5.7", - "php": ">=5.4.0", - "symfony/class-loader": "^2.3|^3.0" + "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", + "php": ">=7" }, "require-dev": { "doctrine/dbal": "~2.3", - "illuminate/config": "^5.0,<5.7", - "illuminate/view": "^5.0,<5.7", + "illuminate/config": "^5.1,<5.8", + "illuminate/view": "^5.1,<5.8", + "phpro/grumphp": "^0.14", "phpunit/phpunit": "4.*", "scrutinizer/ocular": "~1.1", - "squizlabs/php_codesniffer": "~2.3" + "squizlabs/php_codesniffer": "^3" }, "suggest": { "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" @@ -3311,7 +3819,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.5-dev" }, "laravel": { "providers": [ @@ -3346,20 +3854,20 @@ "phpstorm", "sublime" ], - "time": "2018-02-08T07:56:07+00:00" + "time": "2018-12-19T12:12:05+00:00" }, { "name": "barryvdh/reflection-docblock", - "version": "v2.0.4", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/barryvdh/ReflectionDocBlock.git", - "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c" + "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/3dcbd98b5d9384a5357266efba8fd29884458e5c", - "reference": "3dcbd98b5d9384a5357266efba8fd29884458e5c", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/6b69015d83d3daf9004a71a89f26e27d27ef6a16", + "reference": "6b69015d83d3daf9004a71a89f26e27d27ef6a16", "shasum": "" }, "require": { @@ -3395,7 +3903,310 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2016-06-13T19:28:20+00:00" + "time": "2018-12-13T10:34:14+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2018-10-18T06:09:13+00:00" + }, + { + "name": "composer/composer", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "d8aef3af866b28786ce9b8647e52c42496436669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669", + "reference": "d8aef3af866b28786ce9b8647e52c42496436669", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/semver": "^1.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^1.1", + "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0" + }, + "conflict": { + "symfony/console": "2.8.38" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7", + "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "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" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "time": "2018-12-03T09:31:16+00:00" + }, + { + "name": "composer/semver", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "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": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2016-08-30T16:08:34+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2", + "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "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": "2018-11-01T09:45:54+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "dc523135366eb68f22268d069ea7749486458562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562", + "reference": "dc523135366eb68f22268d069ea7749486458562", + "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": "2018-11-29T10:59:02+00:00" }, { "name": "doctrine/instantiator", @@ -3453,16 +4264,16 @@ }, { "name": "filp/whoops", - "version": "2.1.14", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "c6081b8838686aa04f1e83ba7e91f78b7b2a23e6" + "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/c6081b8838686aa04f1e83ba7e91f78b7b2a23e6", - "reference": "c6081b8838686aa04f1e83ba7e91f78b7b2a23e6", + "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7", + "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7", "shasum": "" }, "require": { @@ -3470,9 +4281,9 @@ "psr/log": "^1.0.1" }, "require-dev": { - "mockery/mockery": "0.9.*", + "mockery/mockery": "^0.9 || ^1.0", "phpunit/phpunit": "^4.8.35 || ^5.7", - "symfony/var-dumper": "^2.6 || ^3.0" + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0" }, "suggest": { "symfony/var-dumper": "Pretty print complex values better with var-dumper available", @@ -3481,7 +4292,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.2-dev" } }, "autoload": { @@ -3510,20 +4321,20 @@ "throwable", "whoops" ], - "time": "2017-11-23T18:22:44+00:00" + "time": "2018-10-23T09:00:00+00:00" }, { "name": "fzaninotto/faker", - "version": "v1.7.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/fzaninotto/Faker.git", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", - "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de", + "reference": "f72816b43e74063c8b10357394b6bba8cb1c10de", "shasum": "" }, "require": { @@ -3531,7 +4342,7 @@ }, "require-dev": { "ext-intl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.7", "squizlabs/php_codesniffer": "^1.5" }, "type": "library", @@ -3560,7 +4371,7 @@ "faker", "fixtures" ], - "time": "2017-08-15T16:48:10+00:00" + "time": "2018-07-12T10:23:15+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -3610,6 +4421,72 @@ ], "time": "2016-01-20T08:20:44+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.7", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2018-02-14T22:26:30+00:00" + }, { "name": "laravel/browser-kit-testing", "version": "v2.0.1", @@ -3721,16 +4598,16 @@ }, { "name": "mockery/mockery", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "99e29d3596b16dabe4982548527d5ddf90232e99" + "reference": "100633629bf76d57430b86b7098cd6beb996a35a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/99e29d3596b16dabe4982548527d5ddf90232e99", - "reference": "99e29d3596b16dabe4982548527d5ddf90232e99", + "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a", + "reference": "100633629bf76d57430b86b7098cd6beb996a35a", "shasum": "" }, "require": { @@ -3739,8 +4616,7 @@ "php": ">=5.6.0" }, "require-dev": { - "phpdocumentor/phpdocumentor": "^2.9", - "phpunit/phpunit": "~5.7.10|~6.5" + "phpunit/phpunit": "~5.7.10|~6.5|~7.0" }, "type": "library", "extra": { @@ -3783,7 +4659,7 @@ "test double", "testing" ], - "time": "2018-05-08T08:54:48+00:00" + "time": "2018-10-02T21:52:37+00:00" }, { "name": "myclabs/deep-copy", @@ -4086,16 +4962,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -4107,12 +4983,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -4145,7 +5021,7 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4398,16 +5274,16 @@ }, { "name": "phpunit/phpunit", - "version": "6.5.8", + "version": "6.5.13", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b" + "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b", - "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", + "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", "shasum": "" }, "require": { @@ -4425,7 +5301,7 @@ "phpunit/php-file-iterator": "^1.4.3", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^1.0.9", - "phpunit/phpunit-mock-objects": "^5.0.5", + "phpunit/phpunit-mock-objects": "^5.0.9", "sebastian/comparator": "^2.1", "sebastian/diff": "^2.0", "sebastian/environment": "^3.1", @@ -4478,20 +5354,20 @@ "testing", "xunit" ], - "time": "2018-04-10T11:38:34+00:00" + "time": "2018-09-08T15:10:43+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "5.0.6", + "version": "5.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf" + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/33fd41a76e746b8fa96d00b49a23dadfa8334cdf", - "reference": "33fd41a76e746b8fa96d00b49a23dadfa8334cdf", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", + "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", "shasum": "" }, "require": { @@ -4504,7 +5380,7 @@ "phpunit/phpunit": "<6.0" }, "require-dev": { - "phpunit/phpunit": "^6.5" + "phpunit/phpunit": "^6.5.11" }, "suggest": { "ext-soap": "*" @@ -4537,7 +5413,7 @@ "mock", "xunit" ], - "time": "2018-01-06T05:45:45+00:00" + "time": "2018-08-09T05:50:03+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -5099,17 +5975,110 @@ "time": "2016-10-03T07:35:21+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.2.3", + "name": "seld/jsonlint", + "version": "1.7.1", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "4842476c434e375f9d3182ff7b89059583aa8b27" + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4842476c434e375f9d3182ff7b89059583aa8b27", - "reference": "4842476c434e375f9d3182ff7b89059583aa8b27", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2018-01-24T12:46:19+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phra" + ], + "time": "2015-10-13T18:44:15+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "379deb987e26c7cd103a7b387aea178baec96e48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48", + "reference": "379deb987e26c7cd103a7b387aea178baec96e48", "shasum": "" }, "require": { @@ -5147,63 +6116,7 @@ "phpcs", "standards" ], - "time": "2018-02-20T21:35:23+00:00" - }, - { - "name": "symfony/class-loader", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/class-loader.git", - "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/386a294d621576302e7cc36965d6ed53b8c73c4f", - "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "require-dev": { - "symfony/finder": "~2.8|~3.0", - "symfony/polyfill-apcu": "~1.1" - }, - "suggest": { - "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\ClassLoader\\": "" - }, - "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": "Symfony ClassLoader Component", - "homepage": "https://symfony.com", - "time": "2017-06-02T09:51:43+00:00" + "time": "2018-12-19T23:57:18+00:00" }, { "name": "symfony/dom-crawler", @@ -5261,6 +6174,55 @@ "homepage": "https://symfony.com", "time": "2017-01-21T17:13:55+00:00" }, + { + "name": "symfony/filesystem", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2017-07-11T07:17:58+00:00" + }, { "name": "theseer/tokenizer", "version": "1.1.0", @@ -5358,11 +6320,16 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.0.0", - "ext-tidy": "*" + "php": ">=7.0.5", + "ext-json": "*", + "ext-tidy": "*", + "ext-dom": "*", + "ext-xml": "*", + "ext-mbstring": "*", + "ext-gd": "*" }, "platform-dev": [], "platform-overrides": { - "php": "7.0" + "php": "7.0.5" } } diff --git a/config/app.php b/config/app.php index 69e2380e8..e2885d196 100755 --- a/config/app.php +++ b/config/app.php @@ -1,143 +1,84 @@ env('APP_ENV', 'production'), - /** - * Set the default view type for various lists. Can be overridden by user preferences. - * This will be used for public viewers and users that have not set a preference. - */ + // Enter the application in debug mode. + // Shows much more verbose error messages. Has potential to show + // private configuration variables so should remain disabled in public. + 'debug' => env('APP_DEBUG', false), + + // Set the default view type for various lists. Can be overridden by user preferences. + // These will be used for public viewers and users that have not set a preference. 'views' => [ 'books' => env('APP_VIEWS_BOOKS', 'list') ], - /** - * Allow