mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-10-29 16:09:29 +03:00 
			
		
		
		
	Merge pull request #2820 from BookStackApp/analysis-6470L9
Apply fixes from StyleCI
This commit is contained in:
		| @@ -11,16 +11,15 @@ use Illuminate\Support\Str; | ||||
|  | ||||
| /** | ||||
|  * @property string $type | ||||
|  * @property User $user | ||||
|  * @property User   $user | ||||
|  * @property Entity $entity | ||||
|  * @property string $detail | ||||
|  * @property string $entity_type | ||||
|  * @property int $entity_id | ||||
|  * @property int $user_id | ||||
|  * @property int    $entity_id | ||||
|  * @property int    $user_id | ||||
|  */ | ||||
| class Activity extends Model | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Get the entity for this activity. | ||||
|      */ | ||||
| @@ -29,6 +28,7 @@ class Activity extends Model | ||||
|         if ($this->entity_type === '') { | ||||
|             $this->entity_type = null; | ||||
|         } | ||||
|  | ||||
|         return $this->morphTo('entity'); | ||||
|     } | ||||
|  | ||||
| @@ -54,7 +54,7 @@ class Activity extends Model | ||||
|     public function isForEntity(): bool | ||||
|     { | ||||
|         return Str::startsWith($this->type, [ | ||||
|             'page_', 'chapter_', 'book_', 'bookshelf_' | ||||
|             'page_', 'chapter_', 'book_', 'bookshelf_', | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Auth\Permissions\PermissionService; | ||||
| use BookStack\Auth\User; | ||||
| @@ -33,6 +35,7 @@ class ActivityService | ||||
|  | ||||
|     /** | ||||
|      * Add a generic activity event to the database. | ||||
|      * | ||||
|      * @param string|Loggable $detail | ||||
|      */ | ||||
|     public function add(string $type, $detail = '') | ||||
| @@ -54,7 +57,7 @@ class ActivityService | ||||
|     { | ||||
|         return $this->activity->newInstance()->forceFill([ | ||||
|             'type'     => strtolower($type), | ||||
|             'user_id' => user()->id, | ||||
|             'user_id'  => user()->id, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| @@ -67,8 +70,8 @@ class ActivityService | ||||
|     { | ||||
|         $entity->activity()->update([ | ||||
|             'detail'       => $entity->name, | ||||
|             'entity_id'   => null, | ||||
|             'entity_type' => null, | ||||
|             'entity_id'    => null, | ||||
|             'entity_type'  => null, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| @@ -98,10 +101,10 @@ class ActivityService | ||||
|         $queryIds = [$entity->getMorphClass() => [$entity->id]]; | ||||
|  | ||||
|         if ($entity->isA('book')) { | ||||
|             $queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id'); | ||||
|             $queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id'); | ||||
|         } | ||||
|         if ($entity->isA('book') || $entity->isA('chapter')) { | ||||
|             $queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id'); | ||||
|             $queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id'); | ||||
|         } | ||||
|  | ||||
|         $query = $this->activity->newQuery(); | ||||
| @@ -143,7 +146,9 @@ class ActivityService | ||||
|  | ||||
|     /** | ||||
|      * Filters out similar activity. | ||||
|      * | ||||
|      * @param Activity[] $activities | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     protected function filterSimilar(iterable $activities): array | ||||
| @@ -185,7 +190,7 @@ class ActivityService | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $message = str_replace("%u", $username, $message); | ||||
|         $message = str_replace('%u', $username, $message); | ||||
|         $channel = config('logging.failed_login.channel'); | ||||
|         Log::channel($channel)->warning($message); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| class ActivityType | ||||
| { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Model; | ||||
| use BookStack\Traits\HasCreatorAndUpdater; | ||||
| @@ -18,7 +20,7 @@ class Comment extends Model | ||||
|     protected $appends = ['created', 'updated']; | ||||
|  | ||||
|     /** | ||||
|      * Get the entity that this comment belongs to | ||||
|      * Get the entity that this comment belongs to. | ||||
|      */ | ||||
|     public function entity(): MorphTo | ||||
|     { | ||||
| @@ -35,6 +37,7 @@ class Comment extends Model | ||||
|  | ||||
|     /** | ||||
|      * Get created date as a relative diff. | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function getCreatedAttribute() | ||||
| @@ -44,6 +47,7 @@ class Comment extends Model | ||||
|  | ||||
|     /** | ||||
|      * Get updated date as a relative diff. | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function getUpdatedAttribute() | ||||
|   | ||||
| @@ -1,21 +1,21 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Entities\Models\Entity; | ||||
| use League\CommonMark\CommonMarkConverter; | ||||
| use BookStack\Facades\Activity as ActivityService; | ||||
| use League\CommonMark\CommonMarkConverter; | ||||
|  | ||||
| /** | ||||
|  * Class CommentRepo | ||||
|  * Class CommentRepo. | ||||
|  */ | ||||
| class CommentRepo | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * @var Comment $comment | ||||
|      * @var Comment | ||||
|      */ | ||||
|     protected $comment; | ||||
|  | ||||
|  | ||||
|     public function __construct(Comment $comment) | ||||
|     { | ||||
|         $this->comment = $comment; | ||||
| @@ -46,6 +46,7 @@ class CommentRepo | ||||
|  | ||||
|         $entity->comments()->save($comment); | ||||
|         ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON); | ||||
|  | ||||
|         return $comment; | ||||
|     } | ||||
|  | ||||
| @@ -58,6 +59,7 @@ class CommentRepo | ||||
|         $comment->text = $text; | ||||
|         $comment->html = $this->commentToHtml($text); | ||||
|         $comment->save(); | ||||
|  | ||||
|         return $comment; | ||||
|     } | ||||
|  | ||||
| @@ -75,8 +77,8 @@ class CommentRepo | ||||
|     public function commentToHtml(string $commentText): string | ||||
|     { | ||||
|         $converter = new CommonMarkConverter([ | ||||
|             'html_input' => 'strip', | ||||
|             'max_nesting_level' => 10, | ||||
|             'html_input'         => 'strip', | ||||
|             'max_nesting_level'  => 10, | ||||
|             'allow_unsafe_links' => false, | ||||
|         ]); | ||||
|  | ||||
| @@ -89,6 +91,7 @@ class CommentRepo | ||||
|     protected function getNextLocalId(Entity $entity): int | ||||
|     { | ||||
|         $comments = $entity->comments(false)->orderBy('local_id', 'desc')->first(); | ||||
|  | ||||
|         return ($comments->local_id ?? 0) + 1; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\MorphTo; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\MorphTo; | ||||
| @@ -9,7 +11,7 @@ class Tag extends Model | ||||
|     protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at']; | ||||
|  | ||||
|     /** | ||||
|      * Get the entity that this tag belongs to | ||||
|      * Get the entity that this tag belongs to. | ||||
|      */ | ||||
|     public function entity(): MorphTo | ||||
|     { | ||||
| @@ -21,7 +23,7 @@ class Tag extends Model | ||||
|      */ | ||||
|     public function nameUrl(): string | ||||
|     { | ||||
|         return url('/search?term=%5B' . urlencode($this->name) .'%5D'); | ||||
|         return url('/search?term=%5B' . urlencode($this->name) . '%5D'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -29,6 +31,6 @@ class Tag extends Model | ||||
|      */ | ||||
|     public function valueUrl(): string | ||||
|     { | ||||
|         return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D'); | ||||
|         return url('/search?term=%5B' . urlencode($this->name) . '%3D' . urlencode($this->value) . '%5D'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Auth\Permissions\PermissionService; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| @@ -7,7 +9,6 @@ use Illuminate\Support\Collection; | ||||
|  | ||||
| class TagRepo | ||||
| { | ||||
|  | ||||
|     protected $tag; | ||||
|     protected $permissionService; | ||||
|  | ||||
| @@ -37,6 +38,7 @@ class TagRepo | ||||
|         } | ||||
|  | ||||
|         $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); | ||||
|  | ||||
|         return $query->get(['name'])->pluck('name'); | ||||
|     } | ||||
|  | ||||
| @@ -62,11 +64,12 @@ class TagRepo | ||||
|         } | ||||
|  | ||||
|         $query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type'); | ||||
|  | ||||
|         return $query->get(['value'])->pluck('value'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Save an array of tags to an entity | ||||
|      * Save an array of tags to an entity. | ||||
|      */ | ||||
|     public function saveTagsToEntity(Entity $entity, array $tags = []): iterable | ||||
|     { | ||||
| @@ -89,6 +92,7 @@ class TagRepo | ||||
|     { | ||||
|         $name = trim($input['name']); | ||||
|         $value = isset($input['value']) ? trim($input['value']) : ''; | ||||
|  | ||||
|         return $this->tag->newInstance(['name' => $name, 'value' => $value]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Actions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Actions; | ||||
|  | ||||
| use BookStack\Interfaces\Viewable; | ||||
| use BookStack\Model; | ||||
| @@ -16,7 +18,6 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; | ||||
|  */ | ||||
| class View extends Model | ||||
| { | ||||
|  | ||||
|     protected $fillable = ['user_id', 'views']; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Api; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Api; | ||||
|  | ||||
| use BookStack\Http\Controllers\Api\ApiController; | ||||
| use Illuminate\Contracts\Container\BindingResolutionException; | ||||
| @@ -12,7 +14,6 @@ use ReflectionMethod; | ||||
|  | ||||
| class ApiDocsGenerator | ||||
| { | ||||
|  | ||||
|     protected $reflectionClasses = []; | ||||
|     protected $controllerClasses = []; | ||||
|  | ||||
| @@ -30,6 +31,7 @@ class ApiDocsGenerator | ||||
|             $docs = (new static())->generate(); | ||||
|             Cache::put($cacheKey, $docs, 60 * 24); | ||||
|         } | ||||
|  | ||||
|         return $docs; | ||||
|     } | ||||
|  | ||||
| @@ -42,6 +44,7 @@ class ApiDocsGenerator | ||||
|         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes); | ||||
|         $apiRoutes = $this->loadDetailsFromFiles($apiRoutes); | ||||
|         $apiRoutes = $apiRoutes->groupBy('base_model'); | ||||
|  | ||||
|         return $apiRoutes; | ||||
|     } | ||||
|  | ||||
| @@ -57,6 +60,7 @@ class ApiDocsGenerator | ||||
|                 $exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null; | ||||
|                 $route["example_{$exampleType}"] = $exampleContent; | ||||
|             } | ||||
|  | ||||
|             return $route; | ||||
|         }); | ||||
|     } | ||||
| @@ -71,12 +75,14 @@ class ApiDocsGenerator | ||||
|             $comment = $method->getDocComment(); | ||||
|             $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null; | ||||
|             $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']); | ||||
|  | ||||
|             return $route; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Load body params and their rules by inspecting the given class and method name. | ||||
|      * | ||||
|      * @throws BindingResolutionException | ||||
|      */ | ||||
|     protected function getBodyParamsFromClass(string $className, string $methodName): ?array | ||||
| @@ -92,6 +98,7 @@ class ApiDocsGenerator | ||||
|         foreach ($rules as $param => $ruleString) { | ||||
|             $rules[$param] = explode('|', $ruleString); | ||||
|         } | ||||
|  | ||||
|         return count($rules) > 0 ? $rules : null; | ||||
|     } | ||||
|  | ||||
| @@ -102,11 +109,13 @@ class ApiDocsGenerator | ||||
|     { | ||||
|         $matches = []; | ||||
|         preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches); | ||||
|  | ||||
|         return implode(' ', $matches[1] ?? []); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a reflection method from the given class name and method name. | ||||
|      * | ||||
|      * @throws ReflectionException | ||||
|      */ | ||||
|     protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod | ||||
| @@ -131,14 +140,15 @@ class ApiDocsGenerator | ||||
|             [$controller, $controllerMethod] = explode('@', $route->action['uses']); | ||||
|             $baseModelName = explode('.', explode('/', $route->uri)[1])[0]; | ||||
|             $shortName = $baseModelName . '-' . $controllerMethod; | ||||
|  | ||||
|             return [ | ||||
|                 'name' => $shortName, | ||||
|                 'uri' => $route->uri, | ||||
|                 'method' => $route->methods[0], | ||||
|                 'controller' => $controller, | ||||
|                 'controller_method' => $controllerMethod, | ||||
|                 'name'                    => $shortName, | ||||
|                 'uri'                     => $route->uri, | ||||
|                 'method'                  => $route->methods[0], | ||||
|                 'controller'              => $controller, | ||||
|                 'controller_method'       => $controllerMethod, | ||||
|                 'controller_method_kebab' => Str::kebab($controllerMethod), | ||||
|                 'base_model' => $baseModelName, | ||||
|                 'base_model'              => $baseModelName, | ||||
|             ]; | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Api; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Api; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Interfaces\Loggable; | ||||
| @@ -7,19 +9,20 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| use Illuminate\Support\Carbon; | ||||
|  | ||||
| /** | ||||
|  * Class ApiToken | ||||
|  * @property int $id | ||||
|  * Class ApiToken. | ||||
|  * | ||||
|  * @property int    $id | ||||
|  * @property string $token_id | ||||
|  * @property string $secret | ||||
|  * @property string $name | ||||
|  * @property Carbon $expires_at | ||||
|  * @property User $user | ||||
|  * @property User   $user | ||||
|  */ | ||||
| class ApiToken extends Model implements Loggable | ||||
| { | ||||
|     protected $fillable = ['name', 'expires_at']; | ||||
|     protected $casts = [ | ||||
|         'expires_at' => 'date:Y-m-d' | ||||
|         'expires_at' => 'date:Y-m-d', | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -12,7 +12,6 @@ use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| class ApiTokenGuard implements Guard | ||||
| { | ||||
|  | ||||
|     use GuardHelpers; | ||||
|  | ||||
|     /** | ||||
| @@ -20,9 +19,9 @@ class ApiTokenGuard implements Guard | ||||
|      */ | ||||
|     protected $request; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * The last auth exception thrown in this request. | ||||
|      * | ||||
|      * @var ApiAuthException | ||||
|      */ | ||||
|     protected $lastAuthException; | ||||
| @@ -34,7 +33,7 @@ class ApiTokenGuard implements Guard | ||||
|     { | ||||
|         $this->request = $request; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     /** | ||||
|      * @inheritDoc | ||||
|      */ | ||||
| @@ -47,6 +46,7 @@ class ApiTokenGuard implements Guard | ||||
|         } | ||||
|  | ||||
|         $user = null; | ||||
|  | ||||
|         try { | ||||
|             $user = $this->getAuthorisedUserFromRequest(); | ||||
|         } catch (ApiAuthException $exception) { | ||||
| @@ -54,19 +54,20 @@ class ApiTokenGuard implements Guard | ||||
|         } | ||||
|  | ||||
|         $this->user = $user; | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Determine if current user is authenticated. If not, throw an exception. | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable | ||||
|      * | ||||
|      * @throws ApiAuthException | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable | ||||
|      */ | ||||
|     public function authenticate() | ||||
|     { | ||||
|         if (! is_null($user = $this->user())) { | ||||
|         if (!is_null($user = $this->user())) { | ||||
|             return $user; | ||||
|         } | ||||
|  | ||||
| @@ -79,6 +80,7 @@ class ApiTokenGuard implements Guard | ||||
|  | ||||
|     /** | ||||
|      * Check the API token in the request and fetch a valid authorised user. | ||||
|      * | ||||
|      * @throws ApiAuthException | ||||
|      */ | ||||
|     protected function getAuthorisedUserFromRequest(): Authenticatable | ||||
| @@ -98,6 +100,7 @@ class ApiTokenGuard implements Guard | ||||
|  | ||||
|     /** | ||||
|      * Validate the format of the token header value string. | ||||
|      * | ||||
|      * @throws ApiAuthException | ||||
|      */ | ||||
|     protected function validateTokenHeaderValue(string $authToken): void | ||||
| @@ -114,6 +117,7 @@ class ApiTokenGuard implements Guard | ||||
|     /** | ||||
|      * Validate the given secret against the given token and ensure the token | ||||
|      * currently has access to the instance API. | ||||
|      * | ||||
|      * @throws ApiAuthException | ||||
|      */ | ||||
|     protected function validateToken(?ApiToken $token, string $secret): void | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Api; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Api; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Collection; | ||||
| @@ -6,7 +8,6 @@ use Illuminate\Http\Request; | ||||
|  | ||||
| class ListingResponseBuilder | ||||
| { | ||||
|  | ||||
|     protected $query; | ||||
|     protected $request; | ||||
|     protected $fields; | ||||
| @@ -18,7 +19,7 @@ class ListingResponseBuilder | ||||
|         'lt'   => '<', | ||||
|         'gte'  => '>=', | ||||
|         'lte'  => '<=', | ||||
|         'like' => 'like' | ||||
|         'like' => 'like', | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
| @@ -42,7 +43,7 @@ class ListingResponseBuilder | ||||
|         $data = $this->fetchData($filteredQuery); | ||||
|  | ||||
|         return response()->json([ | ||||
|             'data' => $data, | ||||
|             'data'  => $data, | ||||
|             'total' => $total, | ||||
|         ]); | ||||
|     } | ||||
| @@ -54,6 +55,7 @@ class ListingResponseBuilder | ||||
|     { | ||||
|         $query = $this->countAndOffsetQuery($query); | ||||
|         $query = $this->sortQuery($query); | ||||
|  | ||||
|         return $query->get($this->fields); | ||||
|     } | ||||
|  | ||||
| @@ -95,6 +97,7 @@ class ListingResponseBuilder | ||||
|         } | ||||
|  | ||||
|         $queryOperator = $this->filterOperators[$filterOperator]; | ||||
|  | ||||
|         return [$field, $queryOperator, $value]; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,11 +4,11 @@ namespace BookStack; | ||||
|  | ||||
| class Application extends \Illuminate\Foundation\Application | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Get the path to the application configuration files. | ||||
|      * | ||||
|      * @param  string  $path Optionally, a path to append to the config path | ||||
|      * @param string $path Optionally, a path to append to the config path | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function configPath($path = '') | ||||
| @@ -18,6 +18,6 @@ class Application extends \Illuminate\Foundation\Application | ||||
|             . 'app' | ||||
|             . DIRECTORY_SEPARATOR | ||||
|             . 'Config' | ||||
|             . ($path ? DIRECTORY_SEPARATOR.$path : $path); | ||||
|             . ($path ? DIRECTORY_SEPARATOR . $path : $path); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Exceptions\ConfirmationEmailException; | ||||
| @@ -12,7 +14,9 @@ class EmailConfirmationService extends UserTokenService | ||||
|     /** | ||||
|      * Create new confirmation for a user, | ||||
|      * Also removes any existing old ones. | ||||
|      * | ||||
|      * @param User $user | ||||
|      * | ||||
|      * @throws ConfirmationEmailException | ||||
|      */ | ||||
|     public function sendConfirmation(User $user) | ||||
| @@ -29,9 +33,10 @@ class EmailConfirmationService extends UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Check if confirmation is required in this instance. | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function confirmationRequired() : bool | ||||
|     public function confirmationRequired(): bool | ||||
|     { | ||||
|         return setting('registration-confirmation') | ||||
|             || setting('registration-restrict'); | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Auth\Role; | ||||
| use BookStack\Auth\User; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Support\Collection; | ||||
| use Illuminate\Support\Facades\DB; | ||||
|  | ||||
| class ExternalAuthService | ||||
| { | ||||
| @@ -19,6 +19,7 @@ class ExternalAuthService | ||||
|         } | ||||
|  | ||||
|         $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); | ||||
|  | ||||
|         return in_array($roleName, $groupNames); | ||||
|     } | ||||
|  | ||||
| @@ -57,7 +58,7 @@ class ExternalAuthService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Sync the groups to the user roles for the current user | ||||
|      * Sync the groups to the user roles for the current user. | ||||
|      */ | ||||
|     public function syncWithGroups(User $user, array $userGroups): void | ||||
|     { | ||||
|   | ||||
| @@ -7,7 +7,6 @@ use Illuminate\Contracts\Auth\UserProvider; | ||||
|  | ||||
| class ExternalBaseUserProvider implements UserProvider | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * The user model. | ||||
|      * | ||||
| @@ -17,7 +16,8 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|  | ||||
|     /** | ||||
|      * LdapUserProvider constructor. | ||||
|      * @param             $model | ||||
|      * | ||||
|      * @param $model | ||||
|      */ | ||||
|     public function __construct(string $model) | ||||
|     { | ||||
| @@ -32,13 +32,15 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|     public function createModel() | ||||
|     { | ||||
|         $class = '\\' . ltrim($this->model, '\\'); | ||||
|         return new $class; | ||||
|  | ||||
|         return new $class(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieve a user by their unique identifier. | ||||
|      * | ||||
|      * @param  mixed $identifier | ||||
|      * @param mixed $identifier | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable|null | ||||
|      */ | ||||
|     public function retrieveById($identifier) | ||||
| @@ -49,8 +51,9 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|     /** | ||||
|      * Retrieve a user by their unique identifier and "remember me" token. | ||||
|      * | ||||
|      * @param  mixed  $identifier | ||||
|      * @param  string $token | ||||
|      * @param mixed  $identifier | ||||
|      * @param string $token | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable|null | ||||
|      */ | ||||
|     public function retrieveByToken($identifier, $token) | ||||
| @@ -58,12 +61,12 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Update the "remember me" token for the given user in storage. | ||||
|      * | ||||
|      * @param  \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * @param  string                                     $token | ||||
|      * @param \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * @param string                                     $token | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function updateRememberToken(Authenticatable $user, $token) | ||||
| @@ -74,13 +77,15 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|     /** | ||||
|      * Retrieve a user by the given credentials. | ||||
|      * | ||||
|      * @param  array $credentials | ||||
|      * @param array $credentials | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable|null | ||||
|      */ | ||||
|     public function retrieveByCredentials(array $credentials) | ||||
|     { | ||||
|         // Search current user base by looking up a uid | ||||
|         $model = $this->createModel(); | ||||
|  | ||||
|         return $model->newQuery() | ||||
|             ->where('external_auth_id', $credentials['external_auth_id']) | ||||
|             ->first(); | ||||
| @@ -89,8 +94,9 @@ class ExternalBaseUserProvider implements UserProvider | ||||
|     /** | ||||
|      * Validate a user against the given credentials. | ||||
|      * | ||||
|      * @param  \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * @param  array                                      $credentials | ||||
|      * @param \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * @param array                                      $credentials | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function validateCredentials(Authenticatable $user, array $credentials) | ||||
|   | ||||
| @@ -84,7 +84,7 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|         // If we've already retrieved the user for the current request we can just | ||||
|         // return it back immediately. We do not want to fetch the user data on | ||||
|         // every call to this method because that would be tremendously slow. | ||||
|         if (! is_null($this->user)) { | ||||
|         if (!is_null($this->user)) { | ||||
|             return $this->user; | ||||
|         } | ||||
|  | ||||
| @@ -92,7 +92,7 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|  | ||||
|         // First we will try to load the user using the | ||||
|         // identifier in the session if one exists. | ||||
|         if (! is_null($id)) { | ||||
|         if (!is_null($id)) { | ||||
|             $this->user = $this->provider->retrieveById($id); | ||||
|         } | ||||
|  | ||||
| @@ -118,7 +118,8 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Log a user into the application without sessions or cookies. | ||||
|      * | ||||
|      * @param  array  $credentials | ||||
|      * @param array $credentials | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function once(array $credentials = []) | ||||
| @@ -135,12 +136,13 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Log the given user ID into the application without sessions or cookies. | ||||
|      * | ||||
|      * @param  mixed  $id | ||||
|      * @param mixed $id | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable|false | ||||
|      */ | ||||
|     public function onceUsingId($id) | ||||
|     { | ||||
|         if (! is_null($user = $this->provider->retrieveById($id))) { | ||||
|         if (!is_null($user = $this->provider->retrieveById($id))) { | ||||
|             $this->setUser($user); | ||||
|  | ||||
|             return $user; | ||||
| @@ -152,7 +154,8 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Validate a user's credentials. | ||||
|      * | ||||
|      * @param  array  $credentials | ||||
|      * @param array $credentials | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function validate(array $credentials = []) | ||||
| @@ -160,12 +163,12 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Attempt to authenticate a user using the given credentials. | ||||
|      * | ||||
|      * @param  array  $credentials | ||||
|      * @param  bool  $remember | ||||
|      * @param array $credentials | ||||
|      * @param bool  $remember | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function attempt(array $credentials = [], $remember = false) | ||||
| @@ -176,13 +179,14 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Log the given user ID into the application. | ||||
|      * | ||||
|      * @param  mixed  $id | ||||
|      * @param  bool  $remember | ||||
|      * @param mixed $id | ||||
|      * @param bool  $remember | ||||
|      * | ||||
|      * @return \Illuminate\Contracts\Auth\Authenticatable|false | ||||
|      */ | ||||
|     public function loginUsingId($id, $remember = false) | ||||
|     { | ||||
|         if (! is_null($user = $this->provider->retrieveById($id))) { | ||||
|         if (!is_null($user = $this->provider->retrieveById($id))) { | ||||
|             $this->login($user, $remember); | ||||
|  | ||||
|             return $user; | ||||
| @@ -194,8 +198,9 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Log a user into the application. | ||||
|      * | ||||
|      * @param  \Illuminate\Contracts\Auth\Authenticatable  $user | ||||
|      * @param  bool  $remember | ||||
|      * @param \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * @param bool                                       $remember | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function login(AuthenticatableContract $user, $remember = false) | ||||
| @@ -208,7 +213,8 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Update the session with the given ID. | ||||
|      * | ||||
|      * @param  string  $id | ||||
|      * @param string $id | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function updateSession($id) | ||||
| @@ -262,7 +268,7 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|      */ | ||||
|     public function getName() | ||||
|     { | ||||
|         return 'login_'.$this->name.'_'.sha1(static::class); | ||||
|         return 'login_' . $this->name . '_' . sha1(static::class); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -288,7 +294,8 @@ class ExternalBaseSessionGuard implements StatefulGuard | ||||
|     /** | ||||
|      * Set the current user. | ||||
|      * | ||||
|      * @param  \Illuminate\Contracts\Auth\Authenticatable  $user | ||||
|      * @param \Illuminate\Contracts\Auth\Authenticatable $user | ||||
|      * | ||||
|      * @return $this | ||||
|      */ | ||||
|     public function setUser(AuthenticatableContract $user) | ||||
|   | ||||
| @@ -6,8 +6,8 @@ use BookStack\Auth\Access\LdapService; | ||||
| use BookStack\Auth\Access\RegistrationService; | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Exceptions\LdapException; | ||||
| use BookStack\Exceptions\LoginAttemptException; | ||||
| use BookStack\Exceptions\LoginAttemptEmailNeededException; | ||||
| use BookStack\Exceptions\LoginAttemptException; | ||||
| use BookStack\Exceptions\UserRegistrationException; | ||||
| use Illuminate\Contracts\Auth\UserProvider; | ||||
| use Illuminate\Contracts\Session\Session; | ||||
| @@ -15,7 +15,6 @@ use Illuminate\Support\Str; | ||||
|  | ||||
| class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
| { | ||||
|  | ||||
|     protected $ldapService; | ||||
|  | ||||
|     /** | ||||
| @@ -36,8 +35,10 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|      * Validate a user's credentials. | ||||
|      * | ||||
|      * @param array $credentials | ||||
|      * @return bool | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function validate(array $credentials = []) | ||||
|     { | ||||
| @@ -45,7 +46,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|  | ||||
|         if (isset($userDetails['uid'])) { | ||||
|             $this->lastAttempted = $this->provider->retrieveByCredentials([ | ||||
|                 'external_auth_id' => $userDetails['uid'] | ||||
|                 'external_auth_id' => $userDetails['uid'], | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
| @@ -56,10 +57,12 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|      * Attempt to authenticate a user using the given credentials. | ||||
|      * | ||||
|      * @param array $credentials | ||||
|      * @param bool $remember | ||||
|      * @return bool | ||||
|      * @param bool  $remember | ||||
|      * | ||||
|      * @throws LoginAttemptException | ||||
|      * @throws LdapException | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function attempt(array $credentials = [], $remember = false) | ||||
|     { | ||||
| @@ -69,7 +72,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|         $user = null; | ||||
|         if (isset($userDetails['uid'])) { | ||||
|             $this->lastAttempted = $user = $this->provider->retrieveByCredentials([ | ||||
|                 'external_auth_id' => $userDetails['uid'] | ||||
|                 'external_auth_id' => $userDetails['uid'], | ||||
|             ]); | ||||
|         } | ||||
|  | ||||
| @@ -96,11 +99,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|         } | ||||
|  | ||||
|         $this->login($user, $remember); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a new user from the given ldap credentials and login credentials | ||||
|      * Create a new user from the given ldap credentials and login credentials. | ||||
|      * | ||||
|      * @throws LoginAttemptEmailNeededException | ||||
|      * @throws LoginAttemptException | ||||
|      * @throws UserRegistrationException | ||||
| @@ -114,14 +119,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard | ||||
|         } | ||||
|  | ||||
|         $details = [ | ||||
|             'name' => $ldapUserDetails['name'], | ||||
|             'email' => $ldapUserDetails['email'] ?: $credentials['email'], | ||||
|             'name'             => $ldapUserDetails['name'], | ||||
|             'email'            => $ldapUserDetails['email'] ?: $credentials['email'], | ||||
|             'external_auth_id' => $ldapUserDetails['uid'], | ||||
|             'password' => Str::random(32), | ||||
|             'password'         => Str::random(32), | ||||
|         ]; | ||||
|  | ||||
|         $user = $this->registrationService->registerUser($details, null, false); | ||||
|         $this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails); | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| namespace BookStack\Auth\Access\Guards; | ||||
|  | ||||
| /** | ||||
|  * Saml2 Session Guard | ||||
|  * Saml2 Session Guard. | ||||
|  * | ||||
|  * The saml2 login process is async in nature meaning it does not fit very well | ||||
|  * into the default laravel 'Guard' auth flow. Instead most of the logic is done | ||||
| @@ -16,6 +16,7 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard | ||||
|      * Validate a user's credentials. | ||||
|      * | ||||
|      * @param array $credentials | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function validate(array $credentials = []) | ||||
| @@ -27,7 +28,8 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard | ||||
|      * Attempt to authenticate a user using the given credentials. | ||||
|      * | ||||
|      * @param array $credentials | ||||
|      * @param bool $remember | ||||
|      * @param bool  $remember | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function attempt(array $credentials = [], $remember = false) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| /** | ||||
|  * Class Ldap | ||||
| @@ -7,11 +9,12 @@ | ||||
|  */ | ||||
| class Ldap | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Connect to a LDAP server. | ||||
|      * | ||||
|      * @param string $hostName | ||||
|      * @param int    $port | ||||
|      * | ||||
|      * @return resource | ||||
|      */ | ||||
|     public function connect($hostName, $port) | ||||
| @@ -21,9 +24,11 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Set the value of a LDAP option for the given connection. | ||||
|      * | ||||
|      * @param resource $ldapConnection | ||||
|      * @param int $option | ||||
|      * @param mixed $value | ||||
|      * @param int      $option | ||||
|      * @param mixed    $value | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function setOption($ldapConnection, $option, $value) | ||||
| @@ -41,8 +46,10 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Set the version number for the given ldap connection. | ||||
|      * | ||||
|      * @param $ldapConnection | ||||
|      * @param $version | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function setVersion($ldapConnection, $version) | ||||
| @@ -52,10 +59,12 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Search LDAP tree using the provided filter. | ||||
|      * | ||||
|      * @param resource   $ldapConnection | ||||
|      * @param string     $baseDn | ||||
|      * @param string     $filter | ||||
|      * @param array|null $attributes | ||||
|      * | ||||
|      * @return resource | ||||
|      */ | ||||
|     public function search($ldapConnection, $baseDn, $filter, array $attributes = null) | ||||
| @@ -65,8 +74,10 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Get entries from an ldap search result. | ||||
|      * | ||||
|      * @param resource $ldapConnection | ||||
|      * @param resource $ldapSearchResult | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     public function getEntries($ldapConnection, $ldapSearchResult) | ||||
| @@ -76,23 +87,28 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Search and get entries immediately. | ||||
|      * | ||||
|      * @param resource   $ldapConnection | ||||
|      * @param string     $baseDn | ||||
|      * @param string     $filter | ||||
|      * @param array|null $attributes | ||||
|      * | ||||
|      * @return resource | ||||
|      */ | ||||
|     public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null) | ||||
|     { | ||||
|         $search = $this->search($ldapConnection, $baseDn, $filter, $attributes); | ||||
|  | ||||
|         return $this->getEntries($ldapConnection, $search); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Bind to LDAP directory. | ||||
|      * | ||||
|      * @param resource $ldapConnection | ||||
|      * @param string   $bindRdn | ||||
|      * @param string   $bindPassword | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function bind($ldapConnection, $bindRdn = null, $bindPassword = null) | ||||
| @@ -102,8 +118,10 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Explode a LDAP dn string into an array of components. | ||||
|      * | ||||
|      * @param string $dn | ||||
|      * @param int $withAttrib | ||||
|      * @param int    $withAttrib | ||||
|      * | ||||
|      * @return array | ||||
|      */ | ||||
|     public function explodeDn(string $dn, int $withAttrib) | ||||
| @@ -113,12 +131,14 @@ class Ldap | ||||
|  | ||||
|     /** | ||||
|      * Escape a string for use in an LDAP filter. | ||||
|      * | ||||
|      * @param string $value | ||||
|      * @param string $ignore | ||||
|      * @param int $flags | ||||
|      * @param int    $flags | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function escape(string $value, string $ignore = "", int $flags = 0) | ||||
|     public function escape(string $value, string $ignore = '', int $flags = 0) | ||||
|     { | ||||
|         return ldap_escape($value, $ignore, $flags); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Exceptions\JsonDebugException; | ||||
| @@ -13,7 +15,6 @@ use Illuminate\Support\Facades\Log; | ||||
|  */ | ||||
| class LdapService extends ExternalAuthService | ||||
| { | ||||
|  | ||||
|     protected $ldap; | ||||
|     protected $ldapConnection; | ||||
|     protected $userAvatars; | ||||
| @@ -33,6 +34,7 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Check if groups should be synced. | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function shouldSyncGroups() | ||||
| @@ -42,6 +44,7 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Search for attributes for a specific user on the ldap. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     private function getUserWithAttributes(string $userName, array $attributes): ?array | ||||
| @@ -73,6 +76,7 @@ class LdapService extends ExternalAuthService | ||||
|     /** | ||||
|      * Get the details of a user from LDAP using the given username. | ||||
|      * User found via configurable user filter. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     public function getUserDetails(string $userName): ?array | ||||
| @@ -92,16 +96,16 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|         $userCn = $this->getUserResponseProperty($user, 'cn', null); | ||||
|         $formatted = [ | ||||
|             'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']), | ||||
|             'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), | ||||
|             'dn' => $user['dn'], | ||||
|             'uid'   => $this->getUserResponseProperty($user, $idAttr, $user['dn']), | ||||
|             'name'  => $this->getUserResponseProperty($user, $displayNameAttr, $userCn), | ||||
|             'dn'    => $user['dn'], | ||||
|             'email' => $this->getUserResponseProperty($user, $emailAttr, null), | ||||
|             'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null, | ||||
|         ]; | ||||
|  | ||||
|         if ($this->config['dump_user_details']) { | ||||
|             throw new JsonDebugException([ | ||||
|                 'details_from_ldap' => $user, | ||||
|                 'details_from_ldap'        => $user, | ||||
|                 'details_bookstack_parsed' => $formatted, | ||||
|             ]); | ||||
|         } | ||||
| @@ -137,6 +141,7 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Check if the given credentials are valid for the given user. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     public function validateUserCredentials(?array $ldapUserDetails, string $password): bool | ||||
| @@ -146,6 +151,7 @@ class LdapService extends ExternalAuthService | ||||
|         } | ||||
|  | ||||
|         $ldapConnection = $this->getConnection(); | ||||
|  | ||||
|         try { | ||||
|             $ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password); | ||||
|         } catch (ErrorException $e) { | ||||
| @@ -158,7 +164,9 @@ class LdapService extends ExternalAuthService | ||||
|     /** | ||||
|      * 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) | ||||
| @@ -181,8 +189,10 @@ class LdapService extends ExternalAuthService | ||||
|     /** | ||||
|      * Get the connection to the LDAP server. | ||||
|      * Creates a new connection if one does not exist. | ||||
|      * @return resource | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      * | ||||
|      * @return resource | ||||
|      */ | ||||
|     protected function getConnection() | ||||
|     { | ||||
| @@ -222,6 +232,7 @@ class LdapService extends ExternalAuthService | ||||
|         } | ||||
|  | ||||
|         $this->ldapConnection = $ldapConnection; | ||||
|  | ||||
|         return $this->ldapConnection; | ||||
|     } | ||||
|  | ||||
| @@ -241,6 +252,7 @@ class LdapService extends ExternalAuthService | ||||
|         // Otherwise, extract the port out | ||||
|         $hostName = $serverNameParts[0]; | ||||
|         $ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389; | ||||
|  | ||||
|         return ['host' => $hostName, 'port' => $ldapPort]; | ||||
|     } | ||||
|  | ||||
| @@ -254,11 +266,13 @@ class LdapService extends ExternalAuthService | ||||
|             $newKey = '${' . $key . '}'; | ||||
|             $newAttrs[$newKey] = $this->ldap->escape($attrText); | ||||
|         } | ||||
|  | ||||
|         return strtr($filterString, $newAttrs); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the groups a user is a part of on ldap. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     public function getUserGroups(string $userName): array | ||||
| @@ -272,11 +286,13 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|         $userGroups = $this->groupFilter($user); | ||||
|         $userGroups = $this->getGroupsRecursive($userGroups, []); | ||||
|  | ||||
|         return $userGroups; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the parent groups of an array of groups. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     private function getGroupsRecursive(array $groupsArray, array $checked): array | ||||
| @@ -303,6 +319,7 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Get the parent groups of a single group. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     private function getGroupGroups(string $groupName): array | ||||
| @@ -336,7 +353,7 @@ class LdapService extends ExternalAuthService | ||||
|         $count = 0; | ||||
|  | ||||
|         if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { | ||||
|             $count = (int)$userGroupSearchResponse[$groupsAttr]['count']; | ||||
|             $count = (int) $userGroupSearchResponse[$groupsAttr]['count']; | ||||
|         } | ||||
|  | ||||
|         for ($i = 0; $i < $count; $i++) { | ||||
| @@ -351,6 +368,7 @@ class LdapService extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Sync the LDAP groups to the user roles for the current user. | ||||
|      * | ||||
|      * @throws LdapException | ||||
|      */ | ||||
|     public function syncGroups(User $user, string $username) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Auth\SocialAccount; | ||||
| @@ -12,7 +14,6 @@ use Exception; | ||||
|  | ||||
| class RegistrationService | ||||
| { | ||||
|  | ||||
|     protected $userRepo; | ||||
|     protected $emailConfirmationService; | ||||
|  | ||||
| @@ -27,6 +28,7 @@ class RegistrationService | ||||
|  | ||||
|     /** | ||||
|      * Check whether or not registrations are allowed in the app settings. | ||||
|      * | ||||
|      * @throws UserRegistrationException | ||||
|      */ | ||||
|     public function ensureRegistrationAllowed() | ||||
| @@ -44,11 +46,13 @@ class RegistrationService | ||||
|     { | ||||
|         $authMethod = config('auth.method'); | ||||
|         $authMethodsWithRegistration = ['standard']; | ||||
|  | ||||
|         return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled'); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The registrations flow for all users. | ||||
|      * | ||||
|      * @throws UserRegistrationException | ||||
|      */ | ||||
|     public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User | ||||
| @@ -84,6 +88,7 @@ class RegistrationService | ||||
|                 session()->flash('sent-email-confirmation', true); | ||||
|             } catch (Exception $e) { | ||||
|                 $message = trans('auth.email_confirm_send_error'); | ||||
|  | ||||
|                 throw new UserRegistrationException($message, '/register/confirm'); | ||||
|             } | ||||
|         } | ||||
| @@ -94,6 +99,7 @@ class RegistrationService | ||||
|     /** | ||||
|      * Ensure that the given email meets any active email domain registration restrictions. | ||||
|      * Throws if restrictions are active and the email does not match an allowed domain. | ||||
|      * | ||||
|      * @throws UserRegistrationException | ||||
|      */ | ||||
|     protected function ensureEmailDomainAllowed(string $userEmail): void | ||||
| @@ -105,9 +111,10 @@ class RegistrationService | ||||
|         } | ||||
|  | ||||
|         $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict)); | ||||
|         $userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1); | ||||
|         $userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1); | ||||
|         if (!in_array($userEmailDomain, $restrictedEmailDomains)) { | ||||
|             $redirect = $this->registrationAllowed() ? '/register' : '/login'; | ||||
|  | ||||
|             throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Auth\User; | ||||
| @@ -37,20 +39,23 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Initiate a login flow. | ||||
|      * | ||||
|      * @throws Error | ||||
|      */ | ||||
|     public function login(): array | ||||
|     { | ||||
|         $toolKit = $this->getToolkit(); | ||||
|         $returnRoute = url('/saml2/acs'); | ||||
|  | ||||
|         return [ | ||||
|             'url' => $toolKit->login($returnRoute, [], false, false, true), | ||||
|             'id' => $toolKit->getLastRequestID(), | ||||
|             'id'  => $toolKit->getLastRequestID(), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initiate a logout flow. | ||||
|      * | ||||
|      * @throws Error | ||||
|      */ | ||||
|     public function logout(): array | ||||
| @@ -78,6 +83,7 @@ class Saml2Service extends ExternalAuthService | ||||
|      * Process the ACS response from the idp and return the | ||||
|      * matching, or new if registration active, user matched to the idp. | ||||
|      * Returns null if not authenticated. | ||||
|      * | ||||
|      * @throws Error | ||||
|      * @throws SamlException | ||||
|      * @throws ValidationError | ||||
| @@ -92,7 +98,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         if (!empty($errors)) { | ||||
|             throw new Error( | ||||
|                 'Invalid ACS Response: '.implode(', ', $errors) | ||||
|                 'Invalid ACS Response: ' . implode(', ', $errors) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
| @@ -108,6 +114,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Process a response for the single logout service. | ||||
|      * | ||||
|      * @throws Error | ||||
|      */ | ||||
|     public function processSlsResponse(?string $requestId): ?string | ||||
| @@ -119,11 +126,12 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         if (!empty($errors)) { | ||||
|             throw new Error( | ||||
|                 'Invalid SLS Response: '.implode(', ', $errors) | ||||
|                 'Invalid SLS Response: ' . implode(', ', $errors) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         $this->actionLogout(); | ||||
|  | ||||
|         return $redirect; | ||||
|     } | ||||
|  | ||||
| @@ -138,6 +146,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Get the metadata for this service provider. | ||||
|      * | ||||
|      * @throws Error | ||||
|      */ | ||||
|     public function metadata(): string | ||||
| @@ -149,7 +158,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         if (!empty($errors)) { | ||||
|             throw new Error( | ||||
|                 'Invalid SP metadata: '.implode(', ', $errors), | ||||
|                 'Invalid SP metadata: ' . implode(', ', $errors), | ||||
|                 Error::METADATA_SP_INVALID | ||||
|             ); | ||||
|         } | ||||
| @@ -159,6 +168,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Load the underlying Onelogin SAML2 toolkit. | ||||
|      * | ||||
|      * @throws Error | ||||
|      * @throws Exception | ||||
|      */ | ||||
| @@ -178,6 +188,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         $spSettings = $this->loadOneloginServiceProviderDetails(); | ||||
|         $settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides); | ||||
|  | ||||
|         return new Auth($settings); | ||||
|     } | ||||
|  | ||||
| @@ -187,18 +198,18 @@ class Saml2Service extends ExternalAuthService | ||||
|     protected function loadOneloginServiceProviderDetails(): array | ||||
|     { | ||||
|         $spDetails = [ | ||||
|             'entityId' => url('/saml2/metadata'), | ||||
|             'entityId'                 => url('/saml2/metadata'), | ||||
|             'assertionConsumerService' => [ | ||||
|                 'url' => url('/saml2/acs'), | ||||
|             ], | ||||
|             'singleLogoutService' => [ | ||||
|                 'url' => url('/saml2/sls') | ||||
|                 'url' => url('/saml2/sls'), | ||||
|             ], | ||||
|         ]; | ||||
|  | ||||
|         return [ | ||||
|             'baseurl' => url('/saml2'), | ||||
|             'sp' => $spDetails | ||||
|             'sp'      => $spDetails, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @@ -211,7 +222,7 @@ class Saml2Service extends ExternalAuthService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Calculate the display name | ||||
|      * Calculate the display name. | ||||
|      */ | ||||
|     protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string | ||||
|     { | ||||
| @@ -261,9 +272,9 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         return [ | ||||
|             'external_id' => $externalId, | ||||
|             'name' => $this->getUserDisplayName($samlAttributes, $externalId), | ||||
|             'email' => $email, | ||||
|             'saml_id' => $samlID, | ||||
|             'name'        => $this->getUserDisplayName($samlAttributes, $externalId), | ||||
|             'email'       => $email, | ||||
|             'saml_id'     => $samlID, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @@ -297,6 +308,7 @@ class Saml2Service extends ExternalAuthService | ||||
|                 $data = $data[0]; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         return $data; | ||||
|     } | ||||
|  | ||||
| @@ -315,6 +327,7 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|     /** | ||||
|      * Get the user from the database for the specified details. | ||||
|      * | ||||
|      * @throws UserRegistrationException | ||||
|      */ | ||||
|     protected function getOrRegisterUser(array $userDetails): ?User | ||||
| @@ -325,9 +338,9 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         if (is_null($user)) { | ||||
|             $userData = [ | ||||
|                 'name' => $userDetails['name'], | ||||
|                 'email' => $userDetails['email'], | ||||
|                 'password' => Str::random(32), | ||||
|                 'name'             => $userDetails['name'], | ||||
|                 'email'            => $userDetails['email'], | ||||
|                 'password'         => Str::random(32), | ||||
|                 'external_auth_id' => $userDetails['external_id'], | ||||
|             ]; | ||||
|  | ||||
| @@ -340,6 +353,7 @@ class Saml2Service extends ExternalAuthService | ||||
|     /** | ||||
|      * Process the SAML response for a user. Login the user when | ||||
|      * they exist, optionally registering them automatically. | ||||
|      * | ||||
|      * @throws SamlException | ||||
|      * @throws JsonDebugException | ||||
|      * @throws UserRegistrationException | ||||
| @@ -351,8 +365,8 @@ class Saml2Service extends ExternalAuthService | ||||
|  | ||||
|         if ($this->config['dump_user_details']) { | ||||
|             throw new JsonDebugException([ | ||||
|                 'id_from_idp' => $samlID, | ||||
|                 'attrs_from_idp' => $samlAttributes, | ||||
|                 'id_from_idp'         => $samlID, | ||||
|                 'attrs_from_idp'      => $samlAttributes, | ||||
|                 'attrs_after_parsing' => $userDetails, | ||||
|             ]); | ||||
|         } | ||||
| @@ -378,6 +392,7 @@ class Saml2Service extends ExternalAuthService | ||||
|         auth()->login($user); | ||||
|         Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}"); | ||||
|         Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user); | ||||
|  | ||||
|         return $user; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Auth\SocialAccount; | ||||
| @@ -21,12 +23,14 @@ class SocialAuthService | ||||
| { | ||||
|     /** | ||||
|      * The core socialite library used. | ||||
|      * | ||||
|      * @var Socialite | ||||
|      */ | ||||
|     protected $socialite; | ||||
|  | ||||
|     /** | ||||
|      * The default built-in social drivers we support. | ||||
|      * | ||||
|      * @var string[] | ||||
|      */ | ||||
|     protected $validSocialDrivers = [ | ||||
| @@ -39,7 +43,7 @@ class SocialAuthService | ||||
|         'okta', | ||||
|         'gitlab', | ||||
|         'twitch', | ||||
|         'discord' | ||||
|         'discord', | ||||
|     ]; | ||||
|  | ||||
|     /** | ||||
| @@ -47,6 +51,7 @@ class SocialAuthService | ||||
|      * for an initial redirect action. | ||||
|      * Array is keyed by social driver name. | ||||
|      * Callbacks are passed an instance of the driver. | ||||
|      * | ||||
|      * @var array<string, callable> | ||||
|      */ | ||||
|     protected $configureForRedirectCallbacks = []; | ||||
| @@ -61,26 +66,31 @@ class SocialAuthService | ||||
|  | ||||
|     /** | ||||
|      * Start the social login path. | ||||
|      * | ||||
|      * @throws SocialDriverNotConfigured | ||||
|      */ | ||||
|     public function startLogIn(string $socialDriver): RedirectResponse | ||||
|     { | ||||
|         $driver = $this->validateDriver($socialDriver); | ||||
|  | ||||
|         return $this->getDriverForRedirect($driver)->redirect(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Start the social registration process | ||||
|      * Start the social registration process. | ||||
|      * | ||||
|      * @throws SocialDriverNotConfigured | ||||
|      */ | ||||
|     public function startRegister(string $socialDriver): RedirectResponse | ||||
|     { | ||||
|         $driver = $this->validateDriver($socialDriver); | ||||
|  | ||||
|         return $this->getDriverForRedirect($driver)->redirect(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle the social registration process on callback. | ||||
|      * | ||||
|      * @throws UserRegistrationException | ||||
|      */ | ||||
|     public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser | ||||
| @@ -92,6 +102,7 @@ class SocialAuthService | ||||
|  | ||||
|         if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) { | ||||
|             $email = $socialUser->getEmail(); | ||||
|  | ||||
|             throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login'); | ||||
|         } | ||||
|  | ||||
| @@ -100,16 +111,19 @@ class SocialAuthService | ||||
|  | ||||
|     /** | ||||
|      * Get the social user details via the social driver. | ||||
|      * | ||||
|      * @throws SocialDriverNotConfigured | ||||
|      */ | ||||
|     public function getSocialUser(string $socialDriver): SocialUser | ||||
|     { | ||||
|         $driver = $this->validateDriver($socialDriver); | ||||
|  | ||||
|         return $this->socialite->driver($driver)->user(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle the login process on a oAuth callback. | ||||
|      * | ||||
|      * @throws SocialSignInAccountNotUsed | ||||
|      */ | ||||
|     public function handleLoginCallback(string $socialDriver, SocialUser $socialUser) | ||||
| @@ -128,6 +142,7 @@ class SocialAuthService | ||||
|             auth()->login($socialAccount->user); | ||||
|             Activity::add(ActivityType::AUTH_LOGIN, $socialAccount); | ||||
|             Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user); | ||||
|  | ||||
|             return redirect()->intended('/'); | ||||
|         } | ||||
|  | ||||
| @@ -137,18 +152,21 @@ class SocialAuthService | ||||
|             $account = $this->newSocialAccount($socialDriver, $socialUser); | ||||
|             $currentUser->socialAccounts()->save($account); | ||||
|             session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver])); | ||||
|  | ||||
|             return redirect($currentUser->getEditUrl()); | ||||
|         } | ||||
|  | ||||
|         // When a user is logged in and the social account exists and is already linked to the current user. | ||||
|         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { | ||||
|             session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver])); | ||||
|  | ||||
|             return redirect($currentUser->getEditUrl()); | ||||
|         } | ||||
|  | ||||
|         // When a user is logged in, A social account exists but the users do not match. | ||||
|         if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { | ||||
|             session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver])); | ||||
|  | ||||
|             return redirect($currentUser->getEditUrl()); | ||||
|         } | ||||
|  | ||||
| @@ -163,6 +181,7 @@ class SocialAuthService | ||||
|  | ||||
|     /** | ||||
|      * Ensure the social driver is correct and supported. | ||||
|      * | ||||
|      * @throws SocialDriverNotConfigured | ||||
|      */ | ||||
|     protected function validateDriver(string $socialDriver): string | ||||
| @@ -188,6 +207,7 @@ class SocialAuthService | ||||
|         $lowerName = strtolower($driver); | ||||
|         $configPrefix = 'services.' . $lowerName . '.'; | ||||
|         $config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')]; | ||||
|  | ||||
|         return !in_array(false, $config) && !in_array(null, $config); | ||||
|     } | ||||
|  | ||||
| @@ -237,9 +257,9 @@ class SocialAuthService | ||||
|     public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount | ||||
|     { | ||||
|         return new SocialAccount([ | ||||
|             'driver' => $socialDriver, | ||||
|             'driver'    => $socialDriver, | ||||
|             'driver_id' => $socialUser->getId(), | ||||
|             'avatar' => $socialUser->getAvatar() | ||||
|             'avatar'    => $socialUser->getAvatar(), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| @@ -252,7 +272,7 @@ class SocialAuthService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Provide redirect options per service for the Laravel Socialite driver | ||||
|      * Provide redirect options per service for the Laravel Socialite driver. | ||||
|      */ | ||||
|     protected function getDriverForRedirect(string $driverName): Provider | ||||
|     { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Notifications\UserInvite; | ||||
| @@ -11,6 +13,7 @@ class UserInviteService extends UserTokenService | ||||
|     /** | ||||
|      * Send an invitation to a user to sign into BookStack | ||||
|      * Removes existing invitation tokens. | ||||
|      * | ||||
|      * @param User $user | ||||
|      */ | ||||
|     public function sendInvitation(User $user) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Access; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Access; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Exceptions\UserTokenExpiredException; | ||||
| @@ -10,15 +12,16 @@ use stdClass; | ||||
|  | ||||
| class UserTokenService | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Name of table where user tokens are stored. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $tokenTable = 'user_tokens'; | ||||
|  | ||||
|     /** | ||||
|      * Token expiry time in hours. | ||||
|      * | ||||
|      * @var int | ||||
|      */ | ||||
|     protected $expiryTime = 24; | ||||
| @@ -27,6 +30,7 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * UserTokenService constructor. | ||||
|      * | ||||
|      * @param Database $db | ||||
|      */ | ||||
|     public function __construct(Database $db) | ||||
| @@ -36,7 +40,9 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Delete all email confirmations that belong to a user. | ||||
|      * | ||||
|      * @param User $user | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function deleteByUser(User $user) | ||||
| @@ -48,12 +54,15 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Get the user id from a token, while check the token exists and has not expired. | ||||
|      * | ||||
|      * @param string $token | ||||
|      * @return int | ||||
|      * | ||||
|      * @throws UserTokenNotFoundException | ||||
|      * @throws UserTokenExpiredException | ||||
|      * | ||||
|      * @return int | ||||
|      */ | ||||
|     public function checkTokenAndGetUserId(string $token) : int | ||||
|     public function checkTokenAndGetUserId(string $token): int | ||||
|     { | ||||
|         $entry = $this->getEntryByToken($token); | ||||
|  | ||||
| @@ -70,40 +79,47 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Creates a unique token within the email confirmation database. | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     protected function generateToken() : string | ||||
|     protected function generateToken(): string | ||||
|     { | ||||
|         $token = Str::random(24); | ||||
|         while ($this->tokenExists($token)) { | ||||
|             $token = Str::random(25); | ||||
|         } | ||||
|  | ||||
|         return $token; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate and store a token for the given user. | ||||
|      * | ||||
|      * @param User $user | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     protected function createTokenForUser(User $user) : string | ||||
|     protected function createTokenForUser(User $user): string | ||||
|     { | ||||
|         $token = $this->generateToken(); | ||||
|         $this->db->table($this->tokenTable)->insert([ | ||||
|             'user_id' => $user->id, | ||||
|             'token' => $token, | ||||
|             'user_id'    => $user->id, | ||||
|             'token'      => $token, | ||||
|             'created_at' => Carbon::now(), | ||||
|             'updated_at' => Carbon::now() | ||||
|             'updated_at' => Carbon::now(), | ||||
|         ]); | ||||
|  | ||||
|         return $token; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if the given token exists. | ||||
|      * | ||||
|      * @param string $token | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     protected function tokenExists(string $token) : bool | ||||
|     protected function tokenExists(string $token): bool | ||||
|     { | ||||
|         return $this->db->table($this->tokenTable) | ||||
|             ->where('token', '=', $token)->exists(); | ||||
| @@ -111,7 +127,9 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Get a token entry for the given token. | ||||
|      * | ||||
|      * @param string $token | ||||
|      * | ||||
|      * @return object|null | ||||
|      */ | ||||
|     protected function getEntryByToken(string $token) | ||||
| @@ -123,10 +141,12 @@ class UserTokenService | ||||
|  | ||||
|     /** | ||||
|      * Check if the given token entry has expired. | ||||
|      * | ||||
|      * @param stdClass $tokenEntry | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     protected function entryExpired(stdClass $tokenEntry) : bool | ||||
|     protected function entryExpired(stdClass $tokenEntry): bool | ||||
|     { | ||||
|         return Carbon::now()->subHours($this->expiryTime) | ||||
|             ->gt(new Carbon($tokenEntry->created_at)); | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| <?php namespace BookStack\Auth\Permissions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Permissions; | ||||
|  | ||||
| use BookStack\Model; | ||||
|  | ||||
| class EntityPermission extends Model | ||||
| { | ||||
|  | ||||
|     protected $fillable = ['role_id', 'action']; | ||||
|     public $timestamps = false; | ||||
|  | ||||
|     /** | ||||
|      * Get all this restriction's attached entity. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Relations\MorphTo | ||||
|      */ | ||||
|     public function restrictable() | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Permissions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Permissions; | ||||
|  | ||||
| use BookStack\Auth\Role; | ||||
| use BookStack\Entities\Models\Entity; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Permissions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Permissions; | ||||
|  | ||||
| use BookStack\Auth\Role; | ||||
| use BookStack\Auth\User; | ||||
| @@ -48,7 +50,7 @@ class PermissionService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set the database connection | ||||
|      * Set the database connection. | ||||
|      */ | ||||
|     public function setConnection(Connection $connection) | ||||
|     { | ||||
| @@ -56,7 +58,8 @@ class PermissionService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Prepare the local entity cache and ensure it's empty | ||||
|      * Prepare the local entity cache and ensure it's empty. | ||||
|      * | ||||
|      * @param Entity[] $entities | ||||
|      */ | ||||
|     protected function readyEntityCache(array $entities = []) | ||||
| @@ -73,7 +76,7 @@ class PermissionService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a book via ID, Checks local cache | ||||
|      * Get a book via ID, Checks local cache. | ||||
|      */ | ||||
|     protected function getBook(int $bookId): ?Book | ||||
|     { | ||||
| @@ -85,7 +88,7 @@ class PermissionService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a chapter via ID, Checks local cache | ||||
|      * Get a chapter via ID, Checks local cache. | ||||
|      */ | ||||
|     protected function getChapter(int $chapterId): ?Chapter | ||||
|     { | ||||
| @@ -151,12 +154,13 @@ class PermissionService | ||||
|                 }, | ||||
|                 'pages' => function ($query) { | ||||
|                     $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']); | ||||
|                 } | ||||
|                 }, | ||||
|             ]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Build joint permissions for the given shelf and role combinations. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false) | ||||
| @@ -169,6 +173,7 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Build joint permissions for the given book and role combinations. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false) | ||||
| @@ -193,6 +198,7 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Rebuild the entity jointPermissions for a particular entity. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function buildJointPermissionsForEntity(Entity $entity) | ||||
| @@ -201,6 +207,7 @@ class PermissionService | ||||
|         if ($entity instanceof Book) { | ||||
|             $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get(); | ||||
|             $this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -224,6 +231,7 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Rebuild the entity jointPermissions for a collection of entities. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function buildJointPermissionsForEntities(array $entities) | ||||
| @@ -263,6 +271,7 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Delete all of the entity jointPermissions for a list of entities. | ||||
|      * | ||||
|      * @param Role[] $roles | ||||
|      */ | ||||
|     protected function deleteManyJointPermissionsForRoles($roles) | ||||
| @@ -275,7 +284,9 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Delete the entity jointPermissions for a particular entity. | ||||
|      * | ||||
|      * @param Entity $entity | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function deleteJointPermissionsForEntity(Entity $entity) | ||||
| @@ -285,7 +296,9 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Delete all of the entity jointPermissions for a list of entities. | ||||
|      * | ||||
|      * @param Entity[] $entities | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     protected function deleteManyJointPermissionsForEntities(array $entities) | ||||
| @@ -295,7 +308,6 @@ class PermissionService | ||||
|         } | ||||
|  | ||||
|         $this->db->transaction(function () use ($entities) { | ||||
|  | ||||
|             foreach (array_chunk($entities, 1000) as $entityChunk) { | ||||
|                 $query = $this->db->table('joint_permissions'); | ||||
|                 foreach ($entityChunk as $entity) { | ||||
| @@ -311,8 +323,10 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Create & Save entity jointPermissions for many entities and roles. | ||||
|      * | ||||
|      * @param Entity[] $entities | ||||
|      * @param Role[] $roles | ||||
|      * @param Role[]   $roles | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     protected function createManyJointPermissions(array $entities, array $roles) | ||||
| @@ -363,7 +377,6 @@ class PermissionService | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Get the actions related to an entity. | ||||
|      */ | ||||
| @@ -376,6 +389,7 @@ class PermissionService | ||||
|         if ($entity instanceof Book) { | ||||
|             $baseActions[] = 'chapter-create'; | ||||
|         } | ||||
|  | ||||
|         return $baseActions; | ||||
|     } | ||||
|  | ||||
| @@ -397,6 +411,7 @@ class PermissionService | ||||
|  | ||||
|         if ($entity->restricted) { | ||||
|             $hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction); | ||||
|  | ||||
|             return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess); | ||||
|         } | ||||
|  | ||||
| @@ -433,6 +448,7 @@ class PermissionService | ||||
|     protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool | ||||
|     { | ||||
|         $key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action; | ||||
|  | ||||
|         return $entityMap[$key] ?? false; | ||||
|     } | ||||
|  | ||||
| @@ -443,18 +459,19 @@ class PermissionService | ||||
|     protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array | ||||
|     { | ||||
|         return [ | ||||
|             'role_id' => $role->getRawAttribute('id'), | ||||
|             'entity_id' => $entity->getRawAttribute('id'), | ||||
|             'entity_type' => $entity->getMorphClass(), | ||||
|             'action' => $action, | ||||
|             'has_permission' => $permissionAll, | ||||
|             'role_id'            => $role->getRawAttribute('id'), | ||||
|             'entity_id'          => $entity->getRawAttribute('id'), | ||||
|             'entity_type'        => $entity->getMorphClass(), | ||||
|             'action'             => $action, | ||||
|             'has_permission'     => $permissionAll, | ||||
|             'has_permission_own' => $permissionOwn, | ||||
|             'owned_by' => $entity->getRawAttribute('owned_by'), | ||||
|             'owned_by'           => $entity->getRawAttribute('owned_by'), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks if an entity has a restriction set upon it. | ||||
|      * | ||||
|      * @param HasCreatorAndUpdater|HasOwner $ownable | ||||
|      */ | ||||
|     public function checkOwnableUserAccess(Model $ownable, string $permission): bool | ||||
| @@ -473,7 +490,8 @@ class PermissionService | ||||
|             $ownPermission = $user && $user->can($permission . '-own'); | ||||
|             $ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by'; | ||||
|             $isOwner = $user && $user->id === $ownable->$ownerField; | ||||
|             return ($allPermission || ($isOwner && $ownPermission)); | ||||
|  | ||||
|             return $allPermission || ($isOwner && $ownPermission); | ||||
|         } | ||||
|  | ||||
|         // Handle abnormal create jointPermissions | ||||
| @@ -483,6 +501,7 @@ class PermissionService | ||||
|  | ||||
|         $hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0; | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $hasAccess; | ||||
|     } | ||||
|  | ||||
| @@ -509,6 +528,7 @@ class PermissionService | ||||
|  | ||||
|         $hasPermission = $permissionQuery->count() > 0; | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $hasPermission; | ||||
|     } | ||||
|  | ||||
| @@ -529,6 +549,7 @@ class PermissionService | ||||
|         }); | ||||
|  | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $q; | ||||
|     } | ||||
|  | ||||
| @@ -539,6 +560,7 @@ class PermissionService | ||||
|     public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder | ||||
|     { | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $query->where(function (Builder $parentQuery) use ($ability) { | ||||
|             $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) { | ||||
|                 $permissionQuery->whereIn('role_id', $this->getCurrentUserRoles()) | ||||
| @@ -580,6 +602,7 @@ class PermissionService | ||||
|  | ||||
|     /** | ||||
|      * Filter items that have entities set as a polymorphic relation. | ||||
|      * | ||||
|      * @param Builder|\Illuminate\Database\Query\Builder $query | ||||
|      */ | ||||
|     public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view') | ||||
| @@ -600,6 +623,7 @@ class PermissionService | ||||
|         }); | ||||
|  | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $q; | ||||
|     } | ||||
|  | ||||
| @@ -628,12 +652,14 @@ class PermissionService | ||||
|         }); | ||||
|  | ||||
|         $this->clean(); | ||||
|  | ||||
|         return $q; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add the query for checking the given user id has permission | ||||
|      * within the join_permissions table. | ||||
|      * | ||||
|      * @param QueryBuilder|Builder $query | ||||
|      */ | ||||
|     protected function addJointHasPermissionCheck($query, int $userIdToCheck) | ||||
| @@ -645,7 +671,7 @@ class PermissionService | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the current user | ||||
|      * Get the current user. | ||||
|      */ | ||||
|     private function currentUser(): User | ||||
|     { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Permissions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Permissions; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Auth\Role; | ||||
| @@ -9,7 +11,6 @@ use Illuminate\Database\Eloquent\Collection; | ||||
|  | ||||
| class PermissionsRepo | ||||
| { | ||||
|  | ||||
|     protected $permission; | ||||
|     protected $role; | ||||
|     protected $permissionService; | ||||
| @@ -62,6 +63,7 @@ class PermissionsRepo | ||||
|         $this->assignRolePermissions($role, $permissions); | ||||
|         $this->permissionService->buildJointPermissionForRole($role); | ||||
|         Activity::add(ActivityType::ROLE_CREATE, $role); | ||||
|  | ||||
|         return $role; | ||||
|     } | ||||
|  | ||||
| @@ -116,6 +118,7 @@ class PermissionsRepo | ||||
|      * Check it's not an admin role or set as default before deleting. | ||||
|      * If an migration Role ID is specified the users assign to the current role | ||||
|      * will be added to the role of the specified id. | ||||
|      * | ||||
|      * @throws PermissionsException | ||||
|      * @throws Exception | ||||
|      */ | ||||
| @@ -127,7 +130,7 @@ class PermissionsRepo | ||||
|         // Prevent deleting admin role or default registration role. | ||||
|         if ($role->system_name && in_array($role->system_name, $this->systemRoles)) { | ||||
|             throw new PermissionsException(trans('errors.role_system_cannot_be_deleted')); | ||||
|         } else if ($role->id === intval(setting('registration-role'))) { | ||||
|         } elseif ($role->id === intval(setting('registration-role'))) { | ||||
|             throw new PermissionsException(trans('errors.role_registration_default_cannot_delete')); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth\Permissions; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth\Permissions; | ||||
|  | ||||
| use BookStack\Auth\Role; | ||||
| use BookStack\Model; | ||||
| @@ -18,7 +20,9 @@ class RolePermission extends Model | ||||
|  | ||||
|     /** | ||||
|      * Get the permission object by name. | ||||
|      * | ||||
|      * @param $name | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public static function getByName($name) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth; | ||||
|  | ||||
| use BookStack\Auth\Permissions\JointPermission; | ||||
| use BookStack\Auth\Permissions\RolePermission; | ||||
| @@ -9,8 +11,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; | ||||
| use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
|  | ||||
| /** | ||||
|  * Class Role | ||||
|  * @property int $id | ||||
|  * Class Role. | ||||
|  * | ||||
|  * @property int    $id | ||||
|  * @property string $display_name | ||||
|  * @property string $description | ||||
|  * @property string $external_auth_id | ||||
| @@ -18,7 +21,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
|  */ | ||||
| class Role extends Model implements Loggable | ||||
| { | ||||
|  | ||||
|     protected $fillable = ['display_name', 'description', 'external_auth_id']; | ||||
|  | ||||
|     /** | ||||
| @@ -56,6 +58,7 @@ class Role extends Model implements Loggable | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,16 +1,18 @@ | ||||
| <?php namespace BookStack\Auth; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth; | ||||
|  | ||||
| use BookStack\Interfaces\Loggable; | ||||
| use BookStack\Model; | ||||
|  | ||||
| /** | ||||
|  * Class SocialAccount | ||||
|  * Class SocialAccount. | ||||
|  * | ||||
|  * @property string $driver | ||||
|  * @property User $user | ||||
|  * @property User   $user | ||||
|  */ | ||||
| class SocialAccount extends Model implements Loggable | ||||
| { | ||||
|  | ||||
|     protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps']; | ||||
|  | ||||
|     public function user() | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth; | ||||
|  | ||||
| use BookStack\Actions\Favourite; | ||||
| use BookStack\Api\ApiToken; | ||||
| @@ -22,32 +24,37 @@ use Illuminate\Notifications\Notifiable; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| /** | ||||
|  * Class User | ||||
|  * @property string $id | ||||
|  * @property string $name | ||||
|  * @property string $slug | ||||
|  * @property string $email | ||||
|  * @property string $password | ||||
|  * @property Carbon $created_at | ||||
|  * @property Carbon $updated_at | ||||
|  * @property bool $email_confirmed | ||||
|  * @property int $image_id | ||||
|  * @property string $external_auth_id | ||||
|  * @property string $system_name | ||||
|  * Class User. | ||||
|  * | ||||
|  * @property string     $id | ||||
|  * @property string     $name | ||||
|  * @property string     $slug | ||||
|  * @property string     $email | ||||
|  * @property string     $password | ||||
|  * @property Carbon     $created_at | ||||
|  * @property Carbon     $updated_at | ||||
|  * @property bool       $email_confirmed | ||||
|  * @property int        $image_id | ||||
|  * @property string     $external_auth_id | ||||
|  * @property string     $system_name | ||||
|  * @property Collection $roles | ||||
|  */ | ||||
| class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable | ||||
| { | ||||
|     use Authenticatable, CanResetPassword, Notifiable; | ||||
|     use Authenticatable; | ||||
|     use CanResetPassword; | ||||
|     use Notifiable; | ||||
|  | ||||
|     /** | ||||
|      * The database table used by the model. | ||||
|      * | ||||
|      * @var string | ||||
|      */ | ||||
|     protected $table = 'users'; | ||||
|  | ||||
|     /** | ||||
|      * The attributes that are mass assignable. | ||||
|      * | ||||
|      * @var array | ||||
|      */ | ||||
|     protected $fillable = ['name', 'email']; | ||||
| @@ -56,6 +63,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|  | ||||
|     /** | ||||
|      * The attributes excluded from the model's JSON form. | ||||
|      * | ||||
|      * @var array | ||||
|      */ | ||||
|     protected $hidden = [ | ||||
| @@ -65,12 +73,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|  | ||||
|     /** | ||||
|      * This holds the user's permissions when loaded. | ||||
|      * | ||||
|      * @var ?Collection | ||||
|      */ | ||||
|     protected $permissions; | ||||
|  | ||||
|     /** | ||||
|      * This holds the default user when loaded. | ||||
|      * | ||||
|      * @var null|User | ||||
|      */ | ||||
|     protected static $defaultUser = null; | ||||
| @@ -83,8 +93,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|         if (!is_null(static::$defaultUser)) { | ||||
|             return static::$defaultUser; | ||||
|         } | ||||
|          | ||||
|  | ||||
|         static::$defaultUser = static::query()->where('system_name', '=', 'public')->first(); | ||||
|  | ||||
|         return static::$defaultUser; | ||||
|     } | ||||
|  | ||||
| @@ -98,13 +109,15 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|  | ||||
|     /** | ||||
|      * The roles that belong to the user. | ||||
|      * | ||||
|      * @return BelongsToMany | ||||
|      */ | ||||
|     public function roles() | ||||
|     { | ||||
|         if ($this->id === 0) { | ||||
|             return ; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         return $this->belongsToMany(Role::class); | ||||
|     } | ||||
|  | ||||
| @@ -194,7 +207,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|     /** | ||||
|      * Check if the user has a social account, | ||||
|      * If a driver is passed it checks for that single account type. | ||||
|      * | ||||
|      * @param bool|string $socialDriver | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function hasSocialAccount($socialDriver = false) | ||||
| @@ -207,7 +222,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a URL to the user's avatar | ||||
|      * Returns a URL to the user's avatar. | ||||
|      */ | ||||
|     public function getAvatar(int $size = 50): string | ||||
|     { | ||||
| @@ -222,6 +237,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|         } catch (Exception $err) { | ||||
|             $avatar = $default; | ||||
|         } | ||||
|  | ||||
|         return $avatar; | ||||
|     } | ||||
|  | ||||
| @@ -268,6 +284,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|     public function getEditUrl(string $path = ''): string | ||||
|     { | ||||
|         $uri = '/settings/users/' . $this->id . '/' . trim($path, '/'); | ||||
|  | ||||
|         return url(rtrim($uri, '/')); | ||||
|     } | ||||
|  | ||||
| @@ -298,7 +315,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|  | ||||
|     /** | ||||
|      * Send the password reset notification. | ||||
|      * @param  string  $token | ||||
|      * | ||||
|      * @param string $token | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function sendPasswordResetNotification($token) | ||||
| @@ -320,6 +339,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon | ||||
|     public function refreshSlug(): string | ||||
|     { | ||||
|         $this->slug = app(SlugGenerator::class)->generate($this); | ||||
|  | ||||
|         return $this->slug; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Auth; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Auth; | ||||
|  | ||||
| use Activity; | ||||
| use BookStack\Entities\EntityProvider; | ||||
| @@ -82,7 +84,7 @@ class UserRepo | ||||
|         return $query->paginate($count); | ||||
|     } | ||||
|  | ||||
|      /** | ||||
|     /** | ||||
|      * Creates a new user and attaches a role to them. | ||||
|      */ | ||||
|     public function registerNew(array $data, bool $emailConfirmed = false): User | ||||
| @@ -96,6 +98,7 @@ class UserRepo | ||||
|  | ||||
|     /** | ||||
|      * Assign a user to a system-level role. | ||||
|      * | ||||
|      * @throws NotFoundException | ||||
|      */ | ||||
|     public function attachSystemRole(User $user, string $systemRoleName) | ||||
| @@ -126,6 +129,7 @@ class UserRepo | ||||
|  | ||||
|     /** | ||||
|      * Set the assigned user roles via an array of role IDs. | ||||
|      * | ||||
|      * @throws UserUpdateException | ||||
|      */ | ||||
|     public function setUserRoles(User $user, array $roles) | ||||
| @@ -141,7 +145,7 @@ class UserRepo | ||||
|      * Check if the given user is the last admin and their new roles no longer | ||||
|      * contains the admin role. | ||||
|      */ | ||||
|     protected function demotingLastAdmin(User $user, array $newRoles) : bool | ||||
|     protected function demotingLastAdmin(User $user, array $newRoles): bool | ||||
|     { | ||||
|         if ($this->isOnlyAdmin($user)) { | ||||
|             $adminRole = Role::getSystemRole('admin'); | ||||
| @@ -159,10 +163,10 @@ class UserRepo | ||||
|     public function create(array $data, bool $emailConfirmed = false): User | ||||
|     { | ||||
|         $details = [ | ||||
|             'name'     => $data['name'], | ||||
|             'email'    => $data['email'], | ||||
|             'password' => bcrypt($data['password']), | ||||
|             'email_confirmed' => $emailConfirmed, | ||||
|             'name'             => $data['name'], | ||||
|             'email'            => $data['email'], | ||||
|             'password'         => bcrypt($data['password']), | ||||
|             'email_confirmed'  => $emailConfirmed, | ||||
|             'external_auth_id' => $data['external_auth_id'] ?? '', | ||||
|         ]; | ||||
|  | ||||
| @@ -176,6 +180,7 @@ class UserRepo | ||||
|  | ||||
|     /** | ||||
|      * Remove the given user from storage, Delete all related content. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroy(User $user, ?int $newOwnerId = null) | ||||
| @@ -184,7 +189,7 @@ class UserRepo | ||||
|         $user->apiTokens()->delete(); | ||||
|         $user->favourites()->delete(); | ||||
|         $user->delete(); | ||||
|          | ||||
|  | ||||
|         // Delete user profile images | ||||
|         $this->userAvatar->destroyAllForUser($user); | ||||
|  | ||||
| @@ -201,7 +206,7 @@ class UserRepo | ||||
|      */ | ||||
|     protected function migrateOwnership(User $fromUser, User $toUser) | ||||
|     { | ||||
|         $entities = (new EntityProvider)->all(); | ||||
|         $entities = (new EntityProvider())->all(); | ||||
|         foreach ($entities as $instance) { | ||||
|             $instance->newQuery()->where('owned_by', '=', $fromUser->id) | ||||
|                 ->update(['owned_by' => $toUser->id]); | ||||
| @@ -242,11 +247,12 @@ class UserRepo | ||||
|     public function getAssetCounts(User $user): array | ||||
|     { | ||||
|         $createdBy = ['created_by' => $user->id]; | ||||
|  | ||||
|         return [ | ||||
|             'pages'    =>  Page::visible()->where($createdBy)->count(), | ||||
|             'chapters'    =>  Chapter::visible()->where($createdBy)->count(), | ||||
|             'books'    =>  Book::visible()->where($createdBy)->count(), | ||||
|             'shelves'    =>  Bookshelf::visible()->where($createdBy)->count(), | ||||
|             'pages'       => Page::visible()->where($createdBy)->count(), | ||||
|             'chapters'    => Chapter::visible()->where($createdBy)->count(), | ||||
|             'books'       => Book::visible()->where($createdBy)->count(), | ||||
|             'shelves'     => Bookshelf::visible()->where($createdBy)->count(), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,6 @@ return [ | ||||
|     'max_item_count' => env('API_MAX_ITEM_COUNT', 500), | ||||
|  | ||||
|     // The number of API requests that can be made per minute by a single user. | ||||
|     'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180) | ||||
|     'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180), | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -56,7 +56,7 @@ return [ | ||||
|     'locale' => env('APP_LANG', 'en'), | ||||
|  | ||||
|     // Locales available | ||||
|     'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',], | ||||
|     'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'], | ||||
|  | ||||
|     //  Application Fallback Locale | ||||
|     'fallback_locale' => 'en', | ||||
| @@ -140,52 +140,52 @@ return [ | ||||
|     'aliases' => [ | ||||
|  | ||||
|         // Laravel | ||||
|         'App'       => Illuminate\Support\Facades\App::class, | ||||
|         'Arr'       => Illuminate\Support\Arr::class, | ||||
|         'Artisan'   => Illuminate\Support\Facades\Artisan::class, | ||||
|         'Auth'      => Illuminate\Support\Facades\Auth::class, | ||||
|         'Blade'     => Illuminate\Support\Facades\Blade::class, | ||||
|         'Bus'       => Illuminate\Support\Facades\Bus::class, | ||||
|         'Cache'     => Illuminate\Support\Facades\Cache::class, | ||||
|         'Config'    => Illuminate\Support\Facades\Config::class, | ||||
|         'Cookie'    => Illuminate\Support\Facades\Cookie::class, | ||||
|         'Crypt'     => Illuminate\Support\Facades\Crypt::class, | ||||
|         'DB'        => Illuminate\Support\Facades\DB::class, | ||||
|         'Eloquent'  => Illuminate\Database\Eloquent\Model::class, | ||||
|         'Event'     => Illuminate\Support\Facades\Event::class, | ||||
|         'File'      => Illuminate\Support\Facades\File::class, | ||||
|         'Hash'      => Illuminate\Support\Facades\Hash::class, | ||||
|         'Input'     => Illuminate\Support\Facades\Input::class, | ||||
|         'Inspiring' => Illuminate\Foundation\Inspiring::class, | ||||
|         'Lang'      => Illuminate\Support\Facades\Lang::class, | ||||
|         'Log'       => Illuminate\Support\Facades\Log::class, | ||||
|         'Mail'      => Illuminate\Support\Facades\Mail::class, | ||||
|         'App'          => Illuminate\Support\Facades\App::class, | ||||
|         'Arr'          => Illuminate\Support\Arr::class, | ||||
|         'Artisan'      => Illuminate\Support\Facades\Artisan::class, | ||||
|         'Auth'         => Illuminate\Support\Facades\Auth::class, | ||||
|         'Blade'        => Illuminate\Support\Facades\Blade::class, | ||||
|         'Bus'          => Illuminate\Support\Facades\Bus::class, | ||||
|         'Cache'        => Illuminate\Support\Facades\Cache::class, | ||||
|         'Config'       => Illuminate\Support\Facades\Config::class, | ||||
|         'Cookie'       => Illuminate\Support\Facades\Cookie::class, | ||||
|         'Crypt'        => Illuminate\Support\Facades\Crypt::class, | ||||
|         'DB'           => Illuminate\Support\Facades\DB::class, | ||||
|         'Eloquent'     => Illuminate\Database\Eloquent\Model::class, | ||||
|         'Event'        => Illuminate\Support\Facades\Event::class, | ||||
|         'File'         => Illuminate\Support\Facades\File::class, | ||||
|         'Hash'         => Illuminate\Support\Facades\Hash::class, | ||||
|         'Input'        => Illuminate\Support\Facades\Input::class, | ||||
|         'Inspiring'    => Illuminate\Foundation\Inspiring::class, | ||||
|         'Lang'         => Illuminate\Support\Facades\Lang::class, | ||||
|         'Log'          => Illuminate\Support\Facades\Log::class, | ||||
|         'Mail'         => Illuminate\Support\Facades\Mail::class, | ||||
|         'Notification' => Illuminate\Support\Facades\Notification::class, | ||||
|         'Password'  => Illuminate\Support\Facades\Password::class, | ||||
|         'Queue'     => Illuminate\Support\Facades\Queue::class, | ||||
|         'Redirect'  => Illuminate\Support\Facades\Redirect::class, | ||||
|         'Redis'     => Illuminate\Support\Facades\Redis::class, | ||||
|         'Request'   => Illuminate\Support\Facades\Request::class, | ||||
|         'Response'  => Illuminate\Support\Facades\Response::class, | ||||
|         'Route'     => Illuminate\Support\Facades\Route::class, | ||||
|         'Schema'    => Illuminate\Support\Facades\Schema::class, | ||||
|         'Session'   => Illuminate\Support\Facades\Session::class, | ||||
|         'Storage'   => Illuminate\Support\Facades\Storage::class, | ||||
|         'Str'       => Illuminate\Support\Str::class, | ||||
|         'URL'       => Illuminate\Support\Facades\URL::class, | ||||
|         'Validator' => Illuminate\Support\Facades\Validator::class, | ||||
|         'View'      => Illuminate\Support\Facades\View::class, | ||||
|         'Socialite' => Laravel\Socialite\Facades\Socialite::class, | ||||
|         'Password'     => Illuminate\Support\Facades\Password::class, | ||||
|         'Queue'        => Illuminate\Support\Facades\Queue::class, | ||||
|         'Redirect'     => Illuminate\Support\Facades\Redirect::class, | ||||
|         'Redis'        => Illuminate\Support\Facades\Redis::class, | ||||
|         'Request'      => Illuminate\Support\Facades\Request::class, | ||||
|         'Response'     => Illuminate\Support\Facades\Response::class, | ||||
|         'Route'        => Illuminate\Support\Facades\Route::class, | ||||
|         'Schema'       => Illuminate\Support\Facades\Schema::class, | ||||
|         'Session'      => Illuminate\Support\Facades\Session::class, | ||||
|         'Storage'      => Illuminate\Support\Facades\Storage::class, | ||||
|         'Str'          => Illuminate\Support\Str::class, | ||||
|         'URL'          => Illuminate\Support\Facades\URL::class, | ||||
|         'Validator'    => Illuminate\Support\Facades\Validator::class, | ||||
|         'View'         => Illuminate\Support\Facades\View::class, | ||||
|         'Socialite'    => Laravel\Socialite\Facades\Socialite::class, | ||||
|  | ||||
|         // Third Party | ||||
|         'ImageTool' => Intervention\Image\Facades\Image::class, | ||||
|         'DomPDF' => Barryvdh\DomPDF\Facade::class, | ||||
|         'DomPDF'    => Barryvdh\DomPDF\Facade::class, | ||||
|         'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class, | ||||
|  | ||||
|         // Custom BookStack | ||||
|         'Activity' => BookStack\Facades\Activity::class, | ||||
|         'Activity'    => BookStack\Facades\Activity::class, | ||||
|         'Permissions' => BookStack\Facades\Permissions::class, | ||||
|         'Theme'    => BookStack\Facades\Theme::class, | ||||
|         'Theme'       => BookStack\Facades\Theme::class, | ||||
|     ], | ||||
|  | ||||
|     // Proxy configuration | ||||
|   | ||||
| @@ -18,7 +18,7 @@ return [ | ||||
|     // This option controls the default authentication "guard" and password | ||||
|     // reset options for your application. | ||||
|     'defaults' => [ | ||||
|         'guard' => env('AUTH_METHOD', 'standard'), | ||||
|         'guard'     => env('AUTH_METHOD', 'standard'), | ||||
|         'passwords' => 'users', | ||||
|     ], | ||||
|  | ||||
| @@ -29,15 +29,15 @@ return [ | ||||
|     // Supported drivers: "session", "api-token", "ldap-session" | ||||
|     'guards' => [ | ||||
|         'standard' => [ | ||||
|             'driver' => 'session', | ||||
|             'driver'   => 'session', | ||||
|             'provider' => 'users', | ||||
|         ], | ||||
|         'ldap' => [ | ||||
|             'driver' => 'ldap-session', | ||||
|             'driver'   => 'ldap-session', | ||||
|             'provider' => 'external', | ||||
|         ], | ||||
|         'saml2' => [ | ||||
|             'driver' => 'saml2-session', | ||||
|             'driver'   => 'saml2-session', | ||||
|             'provider' => 'external', | ||||
|         ], | ||||
|         'api' => [ | ||||
| @@ -52,11 +52,11 @@ return [ | ||||
|     'providers' => [ | ||||
|         'users' => [ | ||||
|             'driver' => 'eloquent', | ||||
|             'model' => \BookStack\Auth\User::class, | ||||
|             'model'  => \BookStack\Auth\User::class, | ||||
|         ], | ||||
|         'external' => [ | ||||
|             'driver' => 'external-users', | ||||
|             'model' => \BookStack\Auth\User::class, | ||||
|             'model'  => \BookStack\Auth\User::class, | ||||
|         ], | ||||
|     ], | ||||
|  | ||||
| @@ -67,9 +67,9 @@ return [ | ||||
|     'passwords' => [ | ||||
|         'users' => [ | ||||
|             'provider' => 'users', | ||||
|             'email' => 'emails.password', | ||||
|             'table' => 'password_resets', | ||||
|             'expire' => 60, | ||||
|             'email'    => 'emails.password', | ||||
|             'table'    => 'password_resets', | ||||
|             'expire'   => 60, | ||||
|         ], | ||||
|     ], | ||||
|  | ||||
|   | ||||
| @@ -23,18 +23,18 @@ return [ | ||||
|     'connections' => [ | ||||
|  | ||||
|         'pusher' => [ | ||||
|             'driver' => 'pusher', | ||||
|             'key' => env('PUSHER_APP_KEY'), | ||||
|             'secret' => env('PUSHER_APP_SECRET'), | ||||
|             'app_id' => env('PUSHER_APP_ID'), | ||||
|             'driver'  => 'pusher', | ||||
|             'key'     => env('PUSHER_APP_KEY'), | ||||
|             'secret'  => env('PUSHER_APP_SECRET'), | ||||
|             'app_id'  => env('PUSHER_APP_ID'), | ||||
|             'options' => [ | ||||
|                 'cluster' => env('PUSHER_APP_CLUSTER'), | ||||
|                 'useTLS' => true, | ||||
|                 'useTLS'  => true, | ||||
|             ], | ||||
|         ], | ||||
|  | ||||
|         'redis' => [ | ||||
|             'driver' => 'redis', | ||||
|             'driver'     => 'redis', | ||||
|             'connection' => 'default', | ||||
|         ], | ||||
|  | ||||
| @@ -46,7 +46,6 @@ return [ | ||||
|             'driver' => 'null', | ||||
|         ], | ||||
|  | ||||
|  | ||||
|     ], | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -42,8 +42,8 @@ return [ | ||||
|         ], | ||||
|  | ||||
|         'database' => [ | ||||
|             'driver' => 'database', | ||||
|             'table'  => 'cache', | ||||
|             'driver'     => 'database', | ||||
|             'table'      => 'cache', | ||||
|             'connection' => null, | ||||
|         ], | ||||
|  | ||||
| @@ -58,7 +58,7 @@ return [ | ||||
|         ], | ||||
|  | ||||
|         'redis' => [ | ||||
|             'driver' => 'redis', | ||||
|             'driver'     => 'redis', | ||||
|             'connection' => 'default', | ||||
|         ], | ||||
|  | ||||
|   | ||||
| @@ -59,38 +59,38 @@ return [ | ||||
|     'connections' => [ | ||||
|  | ||||
|         'mysql' => [ | ||||
|             'driver'    => 'mysql', | ||||
|             'url' => env('DATABASE_URL'), | ||||
|             'host'      => $mysql_host, | ||||
|             'database'  => env('DB_DATABASE', 'forge'), | ||||
|             'username'  => env('DB_USERNAME', 'forge'), | ||||
|             'password'  => env('DB_PASSWORD', ''), | ||||
|             'unix_socket' => env('DB_SOCKET', ''), | ||||
|             'port'      => $mysql_port, | ||||
|             'charset'   => 'utf8mb4', | ||||
|             'collation' => 'utf8mb4_unicode_ci', | ||||
|             'prefix'    => '', | ||||
|             'driver'         => 'mysql', | ||||
|             'url'            => env('DATABASE_URL'), | ||||
|             'host'           => $mysql_host, | ||||
|             'database'       => env('DB_DATABASE', 'forge'), | ||||
|             'username'       => env('DB_USERNAME', 'forge'), | ||||
|             'password'       => env('DB_PASSWORD', ''), | ||||
|             'unix_socket'    => env('DB_SOCKET', ''), | ||||
|             'port'           => $mysql_port, | ||||
|             'charset'        => 'utf8mb4', | ||||
|             'collation'      => 'utf8mb4_unicode_ci', | ||||
|             'prefix'         => '', | ||||
|             'prefix_indexes' => true, | ||||
|             'strict'    => false, | ||||
|             'engine' => null, | ||||
|             'options' => extension_loaded('pdo_mysql') ? array_filter([ | ||||
|             'strict'         => false, | ||||
|             'engine'         => null, | ||||
|             'options'        => extension_loaded('pdo_mysql') ? array_filter([ | ||||
|                 PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), | ||||
|             ]) : [], | ||||
|         ], | ||||
|  | ||||
|         'mysql_testing' => [ | ||||
|             'driver'    => 'mysql', | ||||
|             'url' => env('TEST_DATABASE_URL'), | ||||
|             'host'      => '127.0.0.1', | ||||
|             'database'  => 'bookstack-test', | ||||
|             'username'  => env('MYSQL_USER', 'bookstack-test'), | ||||
|             'password'  => env('MYSQL_PASSWORD', 'bookstack-test'), | ||||
|             'port'      => $mysql_port, | ||||
|             'charset'   => 'utf8mb4', | ||||
|             'collation' => 'utf8mb4_unicode_ci', | ||||
|             'prefix'    => '', | ||||
|             'driver'         => 'mysql', | ||||
|             'url'            => env('TEST_DATABASE_URL'), | ||||
|             'host'           => '127.0.0.1', | ||||
|             'database'       => 'bookstack-test', | ||||
|             'username'       => env('MYSQL_USER', 'bookstack-test'), | ||||
|             'password'       => env('MYSQL_PASSWORD', 'bookstack-test'), | ||||
|             'port'           => $mysql_port, | ||||
|             'charset'        => 'utf8mb4', | ||||
|             'collation'      => 'utf8mb4_unicode_ci', | ||||
|             'prefix'         => '', | ||||
|             'prefix_indexes' => true, | ||||
|             'strict'    => false, | ||||
|             'strict'         => false, | ||||
|         ], | ||||
|  | ||||
|     ], | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <?php | ||||
|  | ||||
| /** | ||||
|  * Debugbar Configuration Options | ||||
|  * Debugbar Configuration Options. | ||||
|  * | ||||
|  * Changes to these config files are not supported by BookStack and may break upon updates. | ||||
|  * Configuration should be altered via the `.env` file or environment variables. | ||||
| @@ -10,53 +10,52 @@ | ||||
|  | ||||
| return [ | ||||
|  | ||||
|      // Debugbar is enabled by default, when debug is set to true in app.php. | ||||
|      // You can override the value by setting enable to true or false instead of null. | ||||
|      // | ||||
|      // You can provide an array of URI's that must be ignored (eg. 'api/*') | ||||
|     // Debugbar is enabled by default, when debug is set to true in app.php. | ||||
|     // You can override the value by setting enable to true or false instead of null. | ||||
|     // | ||||
|     // You can provide an array of URI's that must be ignored (eg. 'api/*') | ||||
|     'enabled' => env('DEBUGBAR_ENABLED', false), | ||||
|     'except' => [ | ||||
|         'telescope*' | ||||
|     'except'  => [ | ||||
|         'telescope*', | ||||
|     ], | ||||
|  | ||||
|  | ||||
|      // DebugBar stores data for session/ajax requests. | ||||
|      // You can disable this, so the debugbar stores data in headers/session, | ||||
|      // but this can cause problems with large data collectors. | ||||
|      // By default, file storage (in the storage folder) is used. Redis and PDO | ||||
|      // can also be used. For PDO, run the package migrations first. | ||||
|     // DebugBar stores data for session/ajax requests. | ||||
|     // You can disable this, so the debugbar stores data in headers/session, | ||||
|     // but this can cause problems with large data collectors. | ||||
|     // By default, file storage (in the storage folder) is used. Redis and PDO | ||||
|     // can also be used. For PDO, run the package migrations first. | ||||
|     'storage' => [ | ||||
|         'enabled'    => true, | ||||
|         'driver'     => 'file', // redis, file, pdo, custom | ||||
|         'path'       => storage_path('debugbar'), // For file driver | ||||
|         'connection' => null,   // Leave null for default connection (Redis/PDO) | ||||
|         'provider'   => '' // Instance of StorageInterface for custom driver | ||||
|         'provider'   => '', // Instance of StorageInterface for custom driver | ||||
|     ], | ||||
|  | ||||
|      // Vendor files are included by default, but can be set to false. | ||||
|      // This can also be set to 'js' or 'css', to only include javascript or css vendor files. | ||||
|      // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) | ||||
|      // and for js: jquery and and highlight.js | ||||
|      // So if you want syntax highlighting, set it to true. | ||||
|      // jQuery is set to not conflict with existing jQuery scripts. | ||||
|     // Vendor files are included by default, but can be set to false. | ||||
|     // This can also be set to 'js' or 'css', to only include javascript or css vendor files. | ||||
|     // Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) | ||||
|     // and for js: jquery and and highlight.js | ||||
|     // So if you want syntax highlighting, set it to true. | ||||
|     // jQuery is set to not conflict with existing jQuery scripts. | ||||
|     'include_vendors' => true, | ||||
|  | ||||
|      // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), | ||||
|      // you can use this option to disable sending the data through the headers. | ||||
|      // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. | ||||
|     // The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), | ||||
|     // you can use this option to disable sending the data through the headers. | ||||
|     // Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. | ||||
|  | ||||
|     'capture_ajax' => true, | ||||
|     'capture_ajax'    => true, | ||||
|     'add_ajax_timing' => false, | ||||
|  | ||||
|      // When enabled, the Debugbar shows deprecated warnings for Symfony components | ||||
|      // in the Messages tab. | ||||
|     // When enabled, the Debugbar shows deprecated warnings for Symfony components | ||||
|     // in the Messages tab. | ||||
|     'error_handler' => false, | ||||
|  | ||||
|      // The Debugbar can emulate the Clockwork headers, so you can use the Chrome | ||||
|      // Extension, without the server-side code. It uses Debugbar collectors instead. | ||||
|     // The Debugbar can emulate the Clockwork headers, so you can use the Chrome | ||||
|     // Extension, without the server-side code. It uses Debugbar collectors instead. | ||||
|     'clockwork' => false, | ||||
|  | ||||
|      // Enable/disable DataCollectors | ||||
|     // Enable/disable DataCollectors | ||||
|     'collectors' => [ | ||||
|         'phpinfo'         => true,  // Php version | ||||
|         'messages'        => true,  // Messages | ||||
| @@ -82,7 +81,7 @@ return [ | ||||
|         'models'          => true, // Display models | ||||
|     ], | ||||
|  | ||||
|      // Configure some DataCollectors | ||||
|     // Configure some DataCollectors | ||||
|     'options' => [ | ||||
|         'auth' => [ | ||||
|             'show_name' => true,   // Also show the users name/email in the debugbar | ||||
| @@ -91,43 +90,43 @@ return [ | ||||
|             'with_params'       => true,   // Render SQL with the parameters substituted | ||||
|             'backtrace'         => true,   // Use a backtrace to find the origin of the query in your files. | ||||
|             'timeline'          => false,  // Add the queries to the timeline | ||||
|             'explain' => [                 // Show EXPLAIN output on queries | ||||
|             'explain'           => [                 // Show EXPLAIN output on queries | ||||
|                 'enabled' => false, | ||||
|                 'types' => ['SELECT'],     // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ | ||||
|                 'types'   => ['SELECT'],     // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+ | ||||
|             ], | ||||
|             'hints'             => true,    // Show hints for common mistakes | ||||
|         ], | ||||
|         'mail' => [ | ||||
|             'full_log' => false | ||||
|             'full_log' => false, | ||||
|         ], | ||||
|         'views' => [ | ||||
|             'data' => false,    //Note: Can slow down the application, because the data can be quite large.. | ||||
|         ], | ||||
|         'route' => [ | ||||
|             'label' => true  // show complete route on bar | ||||
|             'label' => true,  // show complete route on bar | ||||
|         ], | ||||
|         'logs' => [ | ||||
|             'file' => null | ||||
|             'file' => null, | ||||
|         ], | ||||
|         'cache' => [ | ||||
|             'values' => true // collect cache values | ||||
|             'values' => true, // collect cache values | ||||
|         ], | ||||
|     ], | ||||
|  | ||||
|      // Inject Debugbar into the response | ||||
|      // Usually, the debugbar is added just before </body>, by listening to the | ||||
|      // Response after the App is done. If you disable this, you have to add them | ||||
|      // in your template yourself. See http://phpdebugbar.com/docs/rendering.html | ||||
|     // Inject Debugbar into the response | ||||
|     // Usually, the debugbar is added just before </body>, by listening to the | ||||
|     // Response after the App is done. If you disable this, you have to add them | ||||
|     // in your template yourself. See http://phpdebugbar.com/docs/rendering.html | ||||
|     'inject' => true, | ||||
|  | ||||
|      // DebugBar route prefix | ||||
|      // Sometimes you want to set route prefix to be used by DebugBar to load | ||||
|      // its resources from. Usually the need comes from misconfigured web server or | ||||
|      // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 | ||||
|     // DebugBar route prefix | ||||
|     // Sometimes you want to set route prefix to be used by DebugBar to load | ||||
|     // its resources from. Usually the need comes from misconfigured web server or | ||||
|     // from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 | ||||
|     'route_prefix' => '_debugbar', | ||||
|  | ||||
|      // DebugBar route domain | ||||
|      // By default DebugBar route served from the same domain that request served. | ||||
|      // To override default domain, specify it as a non-empty value. | ||||
|     // DebugBar route domain | ||||
|     // By default DebugBar route served from the same domain that request served. | ||||
|     // To override default domain, specify it as a non-empty value. | ||||
|     'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''), | ||||
| ]; | ||||
|   | ||||
| @@ -10,12 +10,11 @@ | ||||
|  | ||||
| return [ | ||||
|  | ||||
|  | ||||
|     'show_warnings' => false,   // Throw an Exception on warnings from dompdf | ||||
|     'orientation' => 'portrait', | ||||
|     'defines' => [ | ||||
|     'orientation'   => 'portrait', | ||||
|     'defines'       => [ | ||||
|         /** | ||||
|          * The location of the DOMPDF font directory | ||||
|          * The location of the DOMPDF font directory. | ||||
|          * | ||||
|          * The location of the directory where DOMPDF will store fonts and font metrics | ||||
|          * Note: This directory must exist and be writable by the webserver process. | ||||
| @@ -38,17 +37,17 @@ return [ | ||||
|          * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, | ||||
|          * Symbol, ZapfDingbats. | ||||
|          */ | ||||
|         "DOMPDF_FONT_DIR" => storage_path('fonts/'),  // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) | ||||
|         'DOMPDF_FONT_DIR' => storage_path('fonts/'),  // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) | ||||
|  | ||||
|         /** | ||||
|          * The location of the DOMPDF font cache directory | ||||
|          * The location of the DOMPDF font cache directory. | ||||
|          * | ||||
|          * This directory contains the cached font metrics for the fonts used by DOMPDF. | ||||
|          * This directory can be the same as DOMPDF_FONT_DIR | ||||
|          * | ||||
|          * Note: This directory must exist and be writable by the webserver process. | ||||
|          */ | ||||
|         "DOMPDF_FONT_CACHE" => storage_path('fonts/'), | ||||
|         'DOMPDF_FONT_CACHE' => storage_path('fonts/'), | ||||
|  | ||||
|         /** | ||||
|          * The location of a temporary directory. | ||||
| @@ -57,10 +56,10 @@ return [ | ||||
|          * The temporary directory is required to download remote images and when | ||||
|          * using the PFDLib back end. | ||||
|          */ | ||||
|         "DOMPDF_TEMP_DIR" => sys_get_temp_dir(), | ||||
|         'DOMPDF_TEMP_DIR' => sys_get_temp_dir(), | ||||
|  | ||||
|         /** | ||||
|          * ==== IMPORTANT ==== | ||||
|          * ==== IMPORTANT ====. | ||||
|          * | ||||
|          * dompdf's "chroot": Prevents dompdf from accessing system files or other | ||||
|          * files on the webserver.  All local files opened by dompdf must be in a | ||||
| @@ -71,7 +70,7 @@ return [ | ||||
|          * direct class use like: | ||||
|          * $dompdf = new DOMPDF();  $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output(); | ||||
|          */ | ||||
|         "DOMPDF_CHROOT" => realpath(base_path()), | ||||
|         'DOMPDF_CHROOT' => realpath(base_path()), | ||||
|  | ||||
|         /** | ||||
|          * Whether to use Unicode fonts or not. | ||||
| @@ -82,20 +81,19 @@ return [ | ||||
|          * When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a | ||||
|          * document must be present in your fonts, however. | ||||
|          */ | ||||
|         "DOMPDF_UNICODE_ENABLED" => true, | ||||
|         'DOMPDF_UNICODE_ENABLED' => true, | ||||
|  | ||||
|         /** | ||||
|          * Whether to enable font subsetting or not. | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_FONTSUBSETTING" => false, | ||||
|         'DOMPDF_ENABLE_FONTSUBSETTING' => false, | ||||
|  | ||||
|         /** | ||||
|          * The PDF rendering backend to use | ||||
|          * The PDF rendering backend to use. | ||||
|          * | ||||
|          * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and | ||||
|          * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will | ||||
|          * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link | ||||
|          * Canvas_Factory} ultimately determines which rendering class to instantiate | ||||
|          * fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate | ||||
|          * based on this setting. | ||||
|          * | ||||
|          * Both PDFLib & CPDF rendering backends provide sufficient rendering | ||||
| @@ -117,10 +115,10 @@ return [ | ||||
|          * @link http://www.ros.co.nz/pdf | ||||
|          * @link http://www.php.net/image | ||||
|          */ | ||||
|         "DOMPDF_PDF_BACKEND" => "CPDF", | ||||
|         'DOMPDF_PDF_BACKEND' => 'CPDF', | ||||
|  | ||||
|         /** | ||||
|          * PDFlib license key | ||||
|          * PDFlib license key. | ||||
|          * | ||||
|          * If you are using a licensed, commercial version of PDFlib, specify | ||||
|          * your license key here.  If you are using PDFlib-Lite or are evaluating | ||||
| @@ -143,7 +141,7 @@ return [ | ||||
|          * the desired content might be different (e.g. screen or projection view of html file). | ||||
|          * Therefore allow specification of content here. | ||||
|          */ | ||||
|         "DOMPDF_DEFAULT_MEDIA_TYPE" => "print", | ||||
|         'DOMPDF_DEFAULT_MEDIA_TYPE' => 'print', | ||||
|  | ||||
|         /** | ||||
|          * The default paper size. | ||||
| @@ -152,18 +150,19 @@ return [ | ||||
|          * | ||||
|          * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.) | ||||
|          */ | ||||
|         "DOMPDF_DEFAULT_PAPER_SIZE" => "a4", | ||||
|         'DOMPDF_DEFAULT_PAPER_SIZE' => 'a4', | ||||
|  | ||||
|         /** | ||||
|          * The default font family | ||||
|          * The default font family. | ||||
|          * | ||||
|          * Used if no suitable fonts can be found. This must exist in the font folder. | ||||
|          * | ||||
|          * @var string | ||||
|          */ | ||||
|         "DOMPDF_DEFAULT_FONT" => "dejavu sans", | ||||
|         'DOMPDF_DEFAULT_FONT' => 'dejavu sans', | ||||
|  | ||||
|         /** | ||||
|          * Image DPI setting | ||||
|          * Image DPI setting. | ||||
|          * | ||||
|          * This setting determines the default DPI setting for images and fonts.  The | ||||
|          * DPI may be overridden for inline images by explictly setting the | ||||
| @@ -195,10 +194,10 @@ return [ | ||||
|          * | ||||
|          * @var int | ||||
|          */ | ||||
|         "DOMPDF_DPI" => 96, | ||||
|         'DOMPDF_DPI' => 96, | ||||
|  | ||||
|         /** | ||||
|          * Enable inline PHP | ||||
|          * Enable inline PHP. | ||||
|          * | ||||
|          * If this setting is set to true then DOMPDF will automatically evaluate | ||||
|          * inline PHP contained within <script type="text/php"> ... </script> tags. | ||||
| @@ -209,20 +208,20 @@ return [ | ||||
|          * | ||||
|          * @var bool | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_PHP" => false, | ||||
|         'DOMPDF_ENABLE_PHP' => false, | ||||
|  | ||||
|         /** | ||||
|          * Enable inline Javascript | ||||
|          * Enable inline Javascript. | ||||
|          * | ||||
|          * If this setting is set to true then DOMPDF will automatically insert | ||||
|          * JavaScript code contained within <script type="text/javascript"> ... </script> tags. | ||||
|          * | ||||
|          * @var bool | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_JAVASCRIPT" => false, | ||||
|         'DOMPDF_ENABLE_JAVASCRIPT' => false, | ||||
|  | ||||
|         /** | ||||
|          * Enable remote file access | ||||
|          * Enable remote file access. | ||||
|          * | ||||
|          * If this setting is set to true, DOMPDF will access remote sites for | ||||
|          * images and CSS files as required. | ||||
| @@ -238,29 +237,27 @@ return [ | ||||
|          * | ||||
|          * @var bool | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_REMOTE" => true, | ||||
|         'DOMPDF_ENABLE_REMOTE' => true, | ||||
|  | ||||
|         /** | ||||
|          * A ratio applied to the fonts height to be more like browsers' line height | ||||
|          * A ratio applied to the fonts height to be more like browsers' line height. | ||||
|          */ | ||||
|         "DOMPDF_FONT_HEIGHT_RATIO" => 1.1, | ||||
|         'DOMPDF_FONT_HEIGHT_RATIO' => 1.1, | ||||
|  | ||||
|         /** | ||||
|          * Enable CSS float | ||||
|          * Enable CSS float. | ||||
|          * | ||||
|          * Allows people to disabled CSS float support | ||||
|          * | ||||
|          * @var bool | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_CSS_FLOAT" => true, | ||||
|  | ||||
|         'DOMPDF_ENABLE_CSS_FLOAT' => true, | ||||
|  | ||||
|         /** | ||||
|          * Use the more-than-experimental HTML5 Lib parser | ||||
|          * Use the more-than-experimental HTML5 Lib parser. | ||||
|          */ | ||||
|         "DOMPDF_ENABLE_HTML5PARSER" => true, | ||||
|  | ||||
|         'DOMPDF_ENABLE_HTML5PARSER' => true, | ||||
|  | ||||
|     ], | ||||
|  | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -34,7 +34,7 @@ return [ | ||||
|  | ||||
|         'local' => [ | ||||
|             'driver' => 'local', | ||||
|             'root' => public_path(), | ||||
|             'root'   => public_path(), | ||||
|         ], | ||||
|  | ||||
|         'local_secure' => [ | ||||
| @@ -43,12 +43,12 @@ return [ | ||||
|         ], | ||||
|  | ||||
|         's3' => [ | ||||
|             'driver' => 's3', | ||||
|             'key'    => env('STORAGE_S3_KEY', 'your-key'), | ||||
|             'secret' => env('STORAGE_S3_SECRET', 'your-secret'), | ||||
|             'region' => env('STORAGE_S3_REGION', 'your-region'), | ||||
|             'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'), | ||||
|             'endpoint' => env('STORAGE_S3_ENDPOINT', null), | ||||
|             'driver'                  => 's3', | ||||
|             'key'                     => env('STORAGE_S3_KEY', 'your-key'), | ||||
|             'secret'                  => env('STORAGE_S3_SECRET', 'your-secret'), | ||||
|             'region'                  => env('STORAGE_S3_REGION', 'your-region'), | ||||
|             'bucket'                  => env('STORAGE_S3_BUCKET', 'your-bucket'), | ||||
|             'endpoint'                => env('STORAGE_S3_ENDPOINT', null), | ||||
|             'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null, | ||||
|         ], | ||||
|  | ||||
|   | ||||
| @@ -29,9 +29,9 @@ return [ | ||||
|     // passwords are hashed using the Argon algorithm. These will allow you | ||||
|     // to control the amount of time it takes to hash the given password. | ||||
|     'argon' => [ | ||||
|         'memory' => 1024, | ||||
|         'memory'  => 1024, | ||||
|         'threads' => 2, | ||||
|         'time' => 2, | ||||
|         'time'    => 2, | ||||
|     ], | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -30,66 +30,66 @@ return [ | ||||
|     //                    "custom", "stack" | ||||
|     'channels' => [ | ||||
|         'stack' => [ | ||||
|             'driver' => 'stack', | ||||
|             'channels' => ['daily'], | ||||
|             'driver'            => 'stack', | ||||
|             'channels'          => ['daily'], | ||||
|             'ignore_exceptions' => false, | ||||
|         ], | ||||
|  | ||||
|         'single' => [ | ||||
|             'driver' => 'single', | ||||
|             'path' => storage_path('logs/laravel.log'), | ||||
|             'level' => 'debug', | ||||
|             'days' => 14, | ||||
|             'path'   => storage_path('logs/laravel.log'), | ||||
|             'level'  => 'debug', | ||||
|             'days'   => 14, | ||||
|         ], | ||||
|  | ||||
|         'daily' => [ | ||||
|             'driver' => 'daily', | ||||
|             'path' => storage_path('logs/laravel.log'), | ||||
|             'level' => 'debug', | ||||
|             'days' => 7, | ||||
|             'path'   => storage_path('logs/laravel.log'), | ||||
|             'level'  => 'debug', | ||||
|             'days'   => 7, | ||||
|         ], | ||||
|  | ||||
|         'slack' => [ | ||||
|             'driver' => 'slack', | ||||
|             'url' => env('LOG_SLACK_WEBHOOK_URL'), | ||||
|             'driver'   => 'slack', | ||||
|             'url'      => env('LOG_SLACK_WEBHOOK_URL'), | ||||
|             'username' => 'Laravel Log', | ||||
|             'emoji' => ':boom:', | ||||
|             'level' => 'critical', | ||||
|             'emoji'    => ':boom:', | ||||
|             'level'    => 'critical', | ||||
|         ], | ||||
|  | ||||
|         'stderr' => [ | ||||
|             'driver' => 'monolog', | ||||
|             'driver'  => 'monolog', | ||||
|             'handler' => StreamHandler::class, | ||||
|             'with' => [ | ||||
|             'with'    => [ | ||||
|                 'stream' => 'php://stderr', | ||||
|             ], | ||||
|         ], | ||||
|  | ||||
|         'syslog' => [ | ||||
|             'driver' => 'syslog', | ||||
|             'level' => 'debug', | ||||
|             'level'  => 'debug', | ||||
|         ], | ||||
|  | ||||
|         'errorlog' => [ | ||||
|             'driver' => 'errorlog', | ||||
|             'level' => 'debug', | ||||
|             'level'  => 'debug', | ||||
|         ], | ||||
|  | ||||
|         // Custom errorlog implementation that logs out a plain, | ||||
|         // non-formatted message intended for the webserver log. | ||||
|         'errorlog_plain_webserver' => [ | ||||
|             'driver' => 'monolog', | ||||
|             'level' => 'debug', | ||||
|             'handler' => ErrorLogHandler::class, | ||||
|             'handler_with' => [4], | ||||
|             'formatter' => LineFormatter::class, | ||||
|             'driver'         => 'monolog', | ||||
|             'level'          => 'debug', | ||||
|             'handler'        => ErrorLogHandler::class, | ||||
|             'handler_with'   => [4], | ||||
|             'formatter'      => LineFormatter::class, | ||||
|             'formatter_with' => [ | ||||
|                 'format' => "%message%", | ||||
|                 'format' => '%message%', | ||||
|             ], | ||||
|         ], | ||||
|  | ||||
|         'null' => [ | ||||
|             'driver' => 'monolog', | ||||
|             'driver'  => 'monolog', | ||||
|             'handler' => NullHandler::class, | ||||
|         ], | ||||
|  | ||||
| @@ -101,7 +101,6 @@ return [ | ||||
|         ], | ||||
|     ], | ||||
|  | ||||
|  | ||||
|     // Failed Login Message | ||||
|     // Allows a configurable message to be logged when a login request fails. | ||||
|     'failed_login' => [ | ||||
|   | ||||
| @@ -23,7 +23,7 @@ return [ | ||||
|     // Global "From" address & name | ||||
|     'from' => [ | ||||
|         'address' => env('MAIL_FROM', 'mail@bookstackapp.com'), | ||||
|         'name' => env('MAIL_FROM_NAME', 'BookStack') | ||||
|         'name'    => env('MAIL_FROM_NAME', 'BookStack'), | ||||
|     ], | ||||
|  | ||||
|     // Email encryption protocol | ||||
|   | ||||
| @@ -17,24 +17,23 @@ return [ | ||||
|     // Queue connection configuration | ||||
|     'connections' => [ | ||||
|  | ||||
|  | ||||
|         'sync' => [ | ||||
|             'driver' => 'sync', | ||||
|         ], | ||||
|  | ||||
|         'database' => [ | ||||
|             'driver' => 'database', | ||||
|             'table' => 'jobs', | ||||
|             'queue' => 'default', | ||||
|             'driver'      => 'database', | ||||
|             'table'       => 'jobs', | ||||
|             'queue'       => 'default', | ||||
|             'retry_after' => 90, | ||||
|         ], | ||||
|  | ||||
|         'redis' => [ | ||||
|             'driver' => 'redis', | ||||
|             'connection' => 'default', | ||||
|             'queue' => env('REDIS_QUEUE', 'default'), | ||||
|             'driver'      => 'redis', | ||||
|             'connection'  => 'default', | ||||
|             'queue'       => env('REDIS_QUEUE', 'default'), | ||||
|             'retry_after' => 90, | ||||
|             'block_for' => null, | ||||
|             'block_for'   => null, | ||||
|         ], | ||||
|  | ||||
|     ], | ||||
|   | ||||
| @@ -31,7 +31,6 @@ return [ | ||||
|     // Overrides, in JSON format, to the configuration passed to underlying onelogin library. | ||||
|     'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null), | ||||
|  | ||||
|  | ||||
|     'onelogin' => [ | ||||
|         // If 'strict' is True, then the PHP Toolkit will reject unsigned | ||||
|         // or unencrypted messages if it expects them signed or encrypted | ||||
| @@ -81,7 +80,7 @@ return [ | ||||
|             'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', | ||||
|             // Usually x509cert and privateKey of the SP are provided by files placed at | ||||
|             // the certs folder. But we can also provide them with the following parameters | ||||
|             'x509cert' => '', | ||||
|             'x509cert'   => '', | ||||
|             'privateKey' => '', | ||||
|         ], | ||||
|         // Identity Provider Data that we want connect with our SP | ||||
|   | ||||
| @@ -28,16 +28,16 @@ return [ | ||||
|         'redirect'      => env('APP_URL') . '/login/service/github/callback', | ||||
|         'name'          => 'GitHub', | ||||
|         'auto_register' => env('GITHUB_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('GITHUB_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'google'   => [ | ||||
|         'client_id'     => env('GOOGLE_APP_ID', false), | ||||
|         'client_secret' => env('GOOGLE_APP_SECRET', false), | ||||
|         'redirect'      => env('APP_URL') . '/login/service/google/callback', | ||||
|         'name'          => 'Google', | ||||
|         'auto_register' => env('GOOGLE_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false), | ||||
|         'client_id'      => env('GOOGLE_APP_ID', false), | ||||
|         'client_secret'  => env('GOOGLE_APP_SECRET', false), | ||||
|         'redirect'       => env('APP_URL') . '/login/service/google/callback', | ||||
|         'name'           => 'Google', | ||||
|         'auto_register'  => env('GOOGLE_AUTO_REGISTER', false), | ||||
|         'auto_confirm'   => env('GOOGLE_AUTO_CONFIRM_EMAIL', false), | ||||
|         'select_account' => env('GOOGLE_SELECT_ACCOUNT', false), | ||||
|     ], | ||||
|  | ||||
| @@ -47,7 +47,7 @@ return [ | ||||
|         'redirect'      => env('APP_URL') . '/login/service/slack/callback', | ||||
|         'name'          => 'Slack', | ||||
|         'auto_register' => env('SLACK_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('SLACK_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'facebook'   => [ | ||||
| @@ -56,7 +56,7 @@ return [ | ||||
|         'redirect'      => env('APP_URL') . '/login/service/facebook/callback', | ||||
|         'name'          => 'Facebook', | ||||
|         'auto_register' => env('FACEBOOK_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'twitter'   => [ | ||||
| @@ -65,27 +65,27 @@ return [ | ||||
|         'redirect'      => env('APP_URL') . '/login/service/twitter/callback', | ||||
|         'name'          => 'Twitter', | ||||
|         'auto_register' => env('TWITTER_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('TWITTER_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'azure'   => [ | ||||
|         'client_id'     => env('AZURE_APP_ID', false), | ||||
|         'client_secret' => env('AZURE_APP_SECRET', false), | ||||
|         'tenant'       => env('AZURE_TENANT', false), | ||||
|         'tenant'        => env('AZURE_TENANT', false), | ||||
|         'redirect'      => env('APP_URL') . '/login/service/azure/callback', | ||||
|         'name'          => 'Microsoft Azure', | ||||
|         'auto_register' => env('AZURE_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('AZURE_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'okta' => [ | ||||
|         'client_id' => env('OKTA_APP_ID'), | ||||
|         'client_id'     => env('OKTA_APP_ID'), | ||||
|         'client_secret' => env('OKTA_APP_SECRET'), | ||||
|         'redirect' => env('APP_URL') . '/login/service/okta/callback', | ||||
|         'base_url' => env('OKTA_BASE_URL'), | ||||
|         'redirect'      => env('APP_URL') . '/login/service/okta/callback', | ||||
|         'base_url'      => env('OKTA_BASE_URL'), | ||||
|         'name'          => 'Okta', | ||||
|         'auto_register' => env('OKTA_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('OKTA_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'gitlab' => [ | ||||
| @@ -95,45 +95,45 @@ return [ | ||||
|         'instance_uri'  => env('GITLAB_BASE_URI'), // Needed only for self hosted instances | ||||
|         'name'          => 'GitLab', | ||||
|         'auto_register' => env('GITLAB_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('GITLAB_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'twitch' => [ | ||||
|         'client_id' => env('TWITCH_APP_ID'), | ||||
|         'client_id'     => env('TWITCH_APP_ID'), | ||||
|         'client_secret' => env('TWITCH_APP_SECRET'), | ||||
|         'redirect' => env('APP_URL') . '/login/service/twitch/callback', | ||||
|         'redirect'      => env('APP_URL') . '/login/service/twitch/callback', | ||||
|         'name'          => 'Twitch', | ||||
|         'auto_register' => env('TWITCH_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('TWITCH_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'discord' => [ | ||||
|         'client_id' => env('DISCORD_APP_ID'), | ||||
|         'client_id'     => env('DISCORD_APP_ID'), | ||||
|         'client_secret' => env('DISCORD_APP_SECRET'), | ||||
|         'redirect' => env('APP_URL') . '/login/service/discord/callback', | ||||
|         'name' => 'Discord', | ||||
|         'redirect'      => env('APP_URL') . '/login/service/discord/callback', | ||||
|         'name'          => 'Discord', | ||||
|         'auto_register' => env('DISCORD_AUTO_REGISTER', false), | ||||
|         'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false), | ||||
|         'auto_confirm'  => env('DISCORD_AUTO_CONFIRM_EMAIL', false), | ||||
|     ], | ||||
|  | ||||
|     'ldap' => [ | ||||
|         'server' => env('LDAP_SERVER', false), | ||||
|         'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false), | ||||
|         'dn' => env('LDAP_DN', false), | ||||
|         'pass' => env('LDAP_PASS', false), | ||||
|         'base_dn' => env('LDAP_BASE_DN', false), | ||||
|         'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'), | ||||
|         'version' => env('LDAP_VERSION', false), | ||||
|         'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'), | ||||
|         'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'), | ||||
|         'server'                 => env('LDAP_SERVER', false), | ||||
|         'dump_user_details'      => env('LDAP_DUMP_USER_DETAILS', false), | ||||
|         'dn'                     => env('LDAP_DN', false), | ||||
|         'pass'                   => env('LDAP_PASS', false), | ||||
|         'base_dn'                => env('LDAP_BASE_DN', false), | ||||
|         'user_filter'            => env('LDAP_USER_FILTER', '(&(uid=${user}))'), | ||||
|         'version'                => env('LDAP_VERSION', false), | ||||
|         'id_attribute'           => env('LDAP_ID_ATTRIBUTE', 'uid'), | ||||
|         'email_attribute'        => env('LDAP_EMAIL_ATTRIBUTE', 'mail'), | ||||
|         'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'), | ||||
|         'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false), | ||||
|         'user_to_groups' => env('LDAP_USER_TO_GROUPS', false), | ||||
|         'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), | ||||
|         'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false), | ||||
|         'tls_insecure' => env('LDAP_TLS_INSECURE', false), | ||||
|         'start_tls' => env('LDAP_START_TLS', false), | ||||
|         'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null), | ||||
|         'follow_referrals'       => env('LDAP_FOLLOW_REFERRALS', false), | ||||
|         'user_to_groups'         => env('LDAP_USER_TO_GROUPS', false), | ||||
|         'group_attribute'        => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), | ||||
|         'remove_from_groups'     => env('LDAP_REMOVE_FROM_GROUPS', false), | ||||
|         'tls_insecure'           => env('LDAP_TLS_INSECURE', false), | ||||
|         'start_tls'              => env('LDAP_START_TLS', false), | ||||
|         'thumbnail_attribute'    => env('LDAP_THUMBNAIL_ATTRIBUTE', null), | ||||
|     ], | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <?php | ||||
|  | ||||
| use \Illuminate\Support\Str; | ||||
| use Illuminate\Support\Str; | ||||
|  | ||||
| /** | ||||
|  * Session configuration options. | ||||
|   | ||||
| @@ -26,10 +26,10 @@ return [ | ||||
|  | ||||
|     // User-level default settings | ||||
|     'user' => [ | ||||
|         'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false), | ||||
|         'dark-mode-enabled'     => env('APP_DEFAULT_DARK_MODE', false), | ||||
|         'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'), | ||||
|         'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'), | ||||
|         'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'), | ||||
|         'bookshelf_view_type'   => env('APP_VIEWS_BOOKSHELF', 'grid'), | ||||
|         'books_view_type'       => env('APP_VIEWS_BOOKS', 'grid'), | ||||
|     ], | ||||
|  | ||||
| ]; | ||||
|   | ||||
| @@ -14,7 +14,7 @@ return [ | ||||
|         'binary'  => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false), | ||||
|         'timeout' => false, | ||||
|         'options' => [ | ||||
|             'outline' => true | ||||
|             'outline' => true, | ||||
|         ], | ||||
|         'env'     => [], | ||||
|     ], | ||||
|   | ||||
| @@ -25,11 +25,11 @@ class CleanupImages extends Command | ||||
|      */ | ||||
|     protected $description = 'Cleanup images and drawings'; | ||||
|  | ||||
|  | ||||
|     protected $imageService; | ||||
|  | ||||
|     /** | ||||
|      * Create a new command instance. | ||||
|      * | ||||
|      * @param \BookStack\Uploads\ImageService $imageService | ||||
|      */ | ||||
|     public function __construct(ImageService $imageService) | ||||
| @@ -63,6 +63,7 @@ class CleanupImages extends Command | ||||
|             $this->comment($deleteCount . ' images found that would have been deleted'); | ||||
|             $this->showDeletedImages($deleted); | ||||
|             $this->comment('Run with -f or --force to perform deletions'); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,6 @@ class ClearViews extends Command | ||||
|  | ||||
|     /** | ||||
|      * Create a new command instance. | ||||
|      * | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
|   | ||||
| @@ -54,13 +54,14 @@ class CopyShelfPermissions extends Command | ||||
|  | ||||
|         if (!$cascadeAll && !$shelfSlug) { | ||||
|             $this->error('Either a --slug or --all option must be provided.'); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($cascadeAll) { | ||||
|             $continue = $this->confirm( | ||||
|                 'Permission settings for all shelves will be cascaded. '. | ||||
|                         'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. '. | ||||
|                 'Permission settings for all shelves will be cascaded. ' . | ||||
|                         'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' . | ||||
|                         'Are you sure you want to proceed?' | ||||
|             ); | ||||
|  | ||||
|   | ||||
| @@ -38,8 +38,9 @@ class CreateAdmin extends Command | ||||
|     /** | ||||
|      * Execute the console command. | ||||
|      * | ||||
|      * @return mixed | ||||
|      * @throws \BookStack\Exceptions\NotFoundException | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function handle() | ||||
|     { | ||||
| @@ -71,7 +72,6 @@ class CreateAdmin extends Command | ||||
|             return $this->error('Invalid password provided, Must be at least 5 characters'); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         $user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]); | ||||
|         $this->userRepo->attachSystemRole($user, 'admin'); | ||||
|         $this->userRepo->downloadAndAssignUserAvatar($user); | ||||
|   | ||||
| @@ -8,7 +8,6 @@ use Illuminate\Console\Command; | ||||
|  | ||||
| class DeleteUsers extends Command | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * The name and signature of the console command. | ||||
|      * | ||||
| @@ -47,7 +46,7 @@ class DeleteUsers extends Command | ||||
|                     continue; | ||||
|                 } | ||||
|                 $this->userRepo->destroy($user); | ||||
|                 ++$numDeleted; | ||||
|                 $numDeleted++; | ||||
|             } | ||||
|             $this->info("Deleted $numDeleted of $totalUsers total users."); | ||||
|         } else { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ namespace BookStack\Console\Commands; | ||||
|  | ||||
| use Illuminate\Console\Command; | ||||
| use Illuminate\Database\Connection; | ||||
| use Illuminate\Support\Facades\DB; | ||||
|  | ||||
| class UpdateUrl extends Command | ||||
| { | ||||
| @@ -49,7 +48,8 @@ class UpdateUrl extends Command | ||||
|  | ||||
|         $urlPattern = '/https?:\/\/(.+)/'; | ||||
|         if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) { | ||||
|             $this->error("The given urls are expected to be full urls starting with http:// or https://"); | ||||
|             $this->error('The given urls are expected to be full urls starting with http:// or https://'); | ||||
|  | ||||
|             return 1; | ||||
|         } | ||||
|  | ||||
| @@ -58,11 +58,11 @@ class UpdateUrl extends Command | ||||
|         } | ||||
|  | ||||
|         $columnsToUpdateByTable = [ | ||||
|             "attachments" => ["path"], | ||||
|             "pages" => ["html", "text", "markdown"], | ||||
|             "images" => ["url"], | ||||
|             "settings" => ["value"], | ||||
|             "comments" => ["html", "text"], | ||||
|             'attachments' => ['path'], | ||||
|             'pages'       => ['html', 'text', 'markdown'], | ||||
|             'images'      => ['url'], | ||||
|             'settings'    => ['value'], | ||||
|             'comments'    => ['html', 'text'], | ||||
|         ]; | ||||
|  | ||||
|         foreach ($columnsToUpdateByTable as $table => $columns) { | ||||
| @@ -73,7 +73,7 @@ class UpdateUrl extends Command | ||||
|         } | ||||
|  | ||||
|         $jsonColumnsToUpdateByTable = [ | ||||
|             "settings" => ["value"], | ||||
|             'settings' => ['value'], | ||||
|         ]; | ||||
|  | ||||
|         foreach ($jsonColumnsToUpdateByTable as $table => $columns) { | ||||
| @@ -85,10 +85,11 @@ class UpdateUrl extends Command | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         $this->info("URL update procedure complete."); | ||||
|         $this->info('URL update procedure complete.'); | ||||
|         $this->info('============================================================================'); | ||||
|         $this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.'); | ||||
|         $this->info('============================================================================'); | ||||
|  | ||||
|         return 0; | ||||
|     } | ||||
|  | ||||
| @@ -100,8 +101,9 @@ class UpdateUrl extends Command | ||||
|     { | ||||
|         $oldQuoted = $this->db->getPdo()->quote($oldUrl); | ||||
|         $newQuoted = $this->db->getPdo()->quote($newUrl); | ||||
|  | ||||
|         return $this->db->table($table)->update([ | ||||
|             $column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})") | ||||
|             $column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"), | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
| @@ -112,8 +114,8 @@ class UpdateUrl extends Command | ||||
|     protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool | ||||
|     { | ||||
|         $dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with  \"{$newUrl}\".\n"; | ||||
|         $dangerWarning .= "Are you sure you want to proceed?"; | ||||
|         $backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?"; | ||||
|         $dangerWarning .= 'Are you sure you want to proceed?'; | ||||
|         $backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?'; | ||||
|  | ||||
|         return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation); | ||||
|     } | ||||
|   | ||||
| @@ -23,7 +23,6 @@ class UpgradeDatabaseEncoding extends Command | ||||
|  | ||||
|     /** | ||||
|      * Create a new command instance. | ||||
|      * | ||||
|      */ | ||||
|     public function __construct() | ||||
|     { | ||||
| @@ -44,12 +43,12 @@ class UpgradeDatabaseEncoding extends Command | ||||
|  | ||||
|         $database = DB::getDatabaseName(); | ||||
|         $tables = DB::select('SHOW TABLES'); | ||||
|         $this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); | ||||
|         $this->line('USE `'.$database.'`;'); | ||||
|         $this->line('ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); | ||||
|         $this->line('USE `' . $database . '`;'); | ||||
|         $key = 'Tables_in_' . $database; | ||||
|         foreach ($tables as $table) { | ||||
|             $tableName = $table->$key; | ||||
|             $this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); | ||||
|             $this->line('ALTER TABLE `' . $tableName . '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); | ||||
|         } | ||||
|  | ||||
|         DB::setDefaultConnection($connection); | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Console; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Console; | ||||
|  | ||||
| use Illuminate\Console\Scheduling\Schedule; | ||||
| use Illuminate\Foundation\Console\Kernel as ConsoleKernel; | ||||
| @@ -17,7 +19,8 @@ class Kernel extends ConsoleKernel | ||||
|     /** | ||||
|      * Define the application's command schedule. | ||||
|      * | ||||
|      * @param  \Illuminate\Console\Scheduling\Schedule  $schedule | ||||
|      * @param \Illuminate\Console\Scheduling\Schedule $schedule | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     protected function schedule(Schedule $schedule) | ||||
| @@ -32,6 +35,6 @@ class Kernel extends ConsoleKernel | ||||
|      */ | ||||
|     protected function commands() | ||||
|     { | ||||
|         $this->load(__DIR__.'/Commands'); | ||||
|         $this->load(__DIR__ . '/Commands'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities; | ||||
|  | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Tools\ShelfContext; | ||||
| @@ -6,11 +8,11 @@ use Illuminate\View\View; | ||||
|  | ||||
| class BreadcrumbsViewComposer | ||||
| { | ||||
|  | ||||
|     protected $entityContextManager; | ||||
|  | ||||
|     /** | ||||
|      * BreadcrumbsViewComposer constructor. | ||||
|      * | ||||
|      * @param ShelfContext $entityContextManager | ||||
|      */ | ||||
|     public function __construct(ShelfContext $entityContextManager) | ||||
| @@ -20,6 +22,7 @@ class BreadcrumbsViewComposer | ||||
|  | ||||
|     /** | ||||
|      * Modify data when the view is composed. | ||||
|      * | ||||
|      * @param View $view | ||||
|      */ | ||||
|     public function compose(View $view) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities; | ||||
|  | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\Bookshelf; | ||||
| @@ -8,7 +10,7 @@ use BookStack\Entities\Models\Page; | ||||
| use BookStack\Entities\Models\PageRevision; | ||||
|  | ||||
| /** | ||||
|  * Class EntityProvider | ||||
|  * Class EntityProvider. | ||||
|  * | ||||
|  * Provides access to the core entity models. | ||||
|  * Wrapped up in this provider since they are often used together | ||||
| @@ -16,7 +18,6 @@ use BookStack\Entities\Models\PageRevision; | ||||
|  */ | ||||
| class EntityProvider | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * @var Bookshelf | ||||
|      */ | ||||
| @@ -42,7 +43,6 @@ class EntityProvider | ||||
|      */ | ||||
|     public $pageRevision; | ||||
|  | ||||
|  | ||||
|     public function __construct() | ||||
|     { | ||||
|         $this->bookshelf = new Bookshelf(); | ||||
| @@ -55,15 +55,16 @@ class EntityProvider | ||||
|     /** | ||||
|      * Fetch all core entity types as an associated array | ||||
|      * with their basic names as the keys. | ||||
|      * | ||||
|      * @return array<Entity> | ||||
|      */ | ||||
|     public function all(): array | ||||
|     { | ||||
|         return [ | ||||
|             'bookshelf' => $this->bookshelf, | ||||
|             'book' => $this->book, | ||||
|             'chapter' => $this->chapter, | ||||
|             'page' => $this->page, | ||||
|             'book'      => $this->book, | ||||
|             'chapter'   => $this->chapter, | ||||
|             'page'      => $this->page, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
| @@ -73,6 +74,7 @@ class EntityProvider | ||||
|     public function get(string $type): Entity | ||||
|     { | ||||
|         $type = strtolower($type); | ||||
|  | ||||
|         return $this->all()[$type]; | ||||
|     } | ||||
|  | ||||
| @@ -86,6 +88,7 @@ class EntityProvider | ||||
|             $model = $this->get($type); | ||||
|             $morphClasses[] = $model->getMorphClass(); | ||||
|         } | ||||
|  | ||||
|         return $morphClasses; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Uploads\Image; | ||||
| use Exception; | ||||
| @@ -8,9 +10,10 @@ use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| /** | ||||
|  * Class Book | ||||
|  * @property string $description | ||||
|  * @property int $image_id | ||||
|  * Class Book. | ||||
|  * | ||||
|  * @property string     $description | ||||
|  * @property int        $image_id | ||||
|  * @property Image|null $cover | ||||
|  */ | ||||
| class Book extends Entity implements HasCoverImage | ||||
| @@ -30,8 +33,10 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Returns book cover image, if book cover not exists return default cover image. | ||||
|      * @param int $width - Width of the image | ||||
|      * | ||||
|      * @param int $width  - Width of the image | ||||
|      * @param int $height - Height of the image | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getBookCover($width = 440, $height = 250) | ||||
| @@ -46,11 +51,12 @@ class Book extends Entity implements HasCoverImage | ||||
|         } catch (Exception $err) { | ||||
|             $cover = $default; | ||||
|         } | ||||
|  | ||||
|         return $cover; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the cover image of the book | ||||
|      * Get the cover image of the book. | ||||
|      */ | ||||
|     public function cover(): BelongsTo | ||||
|     { | ||||
| @@ -67,6 +73,7 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Get all pages within this book. | ||||
|      * | ||||
|      * @return HasMany | ||||
|      */ | ||||
|     public function pages() | ||||
| @@ -76,6 +83,7 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Get the direct child pages of this book. | ||||
|      * | ||||
|      * @return HasMany | ||||
|      */ | ||||
|     public function directPages() | ||||
| @@ -85,6 +93,7 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Get all chapters within this book. | ||||
|      * | ||||
|      * @return HasMany | ||||
|      */ | ||||
|     public function chapters() | ||||
| @@ -94,6 +103,7 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Get the shelves this book is contained within. | ||||
|      * | ||||
|      * @return BelongsToMany | ||||
|      */ | ||||
|     public function shelves() | ||||
| @@ -103,12 +113,14 @@ class Book extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Get the direct child items within this book. | ||||
|      * | ||||
|      * @return Collection | ||||
|      */ | ||||
|     public function getDirectChildren(): Collection | ||||
|     { | ||||
|         $pages = $this->directPages()->visible()->get(); | ||||
|         $chapters = $this->chapters()->visible()->get(); | ||||
|  | ||||
|         return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,21 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
|  | ||||
| /** | ||||
|  * Class BookChild | ||||
|  * @property int $book_id | ||||
|  * @property int $priority | ||||
|  * Class BookChild. | ||||
|  * | ||||
|  * @property int  $book_id | ||||
|  * @property int  $priority | ||||
|  * @property Book $book | ||||
|  * | ||||
|  * @method Builder whereSlugs(string $bookSlug, string $childSlug) | ||||
|  */ | ||||
| abstract class BookChild extends Entity | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Scope a query to find items where the the child has the given childSlug | ||||
|      * where its parent has the bookSlug. | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Uploads\Image; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| @@ -17,6 +19,7 @@ class Bookshelf extends Entity implements HasCoverImage | ||||
|     /** | ||||
|      * Get the books in this shelf. | ||||
|      * Should not be used directly since does not take into account permissions. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany | ||||
|      */ | ||||
|     public function books() | ||||
| @@ -44,8 +47,10 @@ class Bookshelf extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Returns BookShelf cover image, if cover does not exists return default cover image. | ||||
|      * @param int $width - Width of the image | ||||
|      * | ||||
|      * @param int $width  - Width of the image | ||||
|      * @param int $height - Height of the image | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getBookCover($width = 440, $height = 250) | ||||
| @@ -61,11 +66,12 @@ class Bookshelf extends Entity implements HasCoverImage | ||||
|         } catch (\Exception $err) { | ||||
|             $cover = $default; | ||||
|         } | ||||
|  | ||||
|         return $cover; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the cover image of the shelf | ||||
|      * Get the cover image of the shelf. | ||||
|      */ | ||||
|     public function cover(): BelongsTo | ||||
|     { | ||||
| @@ -82,7 +88,9 @@ class Bookshelf extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Check if this shelf contains the given book. | ||||
|      * | ||||
|      * @param Book $book | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function contains(Book $book): bool | ||||
| @@ -92,6 +100,7 @@ class Bookshelf extends Entity implements HasCoverImage | ||||
|  | ||||
|     /** | ||||
|      * Add a book to the end of this shelf. | ||||
|      * | ||||
|      * @param Book $book | ||||
|      */ | ||||
|     public function appendBook(Book $book) | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| /** | ||||
|  * Class Chapter | ||||
|  * Class Chapter. | ||||
|  * | ||||
|  * @property Collection<Page> $pages | ||||
|  * @property mixed description | ||||
|  */ | ||||
| @@ -16,7 +19,9 @@ class Chapter extends BookChild | ||||
|  | ||||
|     /** | ||||
|      * Get the pages that this chapter contains. | ||||
|      * | ||||
|      * @param string $dir | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function pages($dir = 'ASC') | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| use BookStack\Interfaces\Loggable; | ||||
| use Illuminate\Database\Eloquent\Model; | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
| @@ -12,7 +13,6 @@ use Illuminate\Database\Eloquent\Relations\MorphTo; | ||||
|  */ | ||||
| class Deletion extends Model implements Loggable | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Get the related deletable record. | ||||
|      */ | ||||
| @@ -35,17 +35,19 @@ class Deletion extends Model implements Loggable | ||||
|     public static function createForEntity(Entity $entity): Deletion | ||||
|     { | ||||
|         $record = (new self())->forceFill([ | ||||
|             'deleted_by' => user()->id, | ||||
|             'deleted_by'     => user()->id, | ||||
|             'deletable_type' => $entity->getMorphClass(), | ||||
|             'deletable_id' => $entity->id, | ||||
|             'deletable_id'   => $entity->id, | ||||
|         ]); | ||||
|         $record->save(); | ||||
|  | ||||
|         return $record; | ||||
|     } | ||||
|  | ||||
|     public function logDescriptor(): string | ||||
|     { | ||||
|         $deletable = $this->deletable()->first(); | ||||
|  | ||||
|         return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}"; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Actions\Activity; | ||||
| use BookStack\Actions\Comment; | ||||
| @@ -27,15 +29,16 @@ use Illuminate\Database\Eloquent\SoftDeletes; | ||||
|  * The base class for book-like items such as pages, chapters & books. | ||||
|  * This is not a database model in itself but extended. | ||||
|  * | ||||
|  * @property int $id | ||||
|  * @property string $name | ||||
|  * @property string $slug | ||||
|  * @property Carbon $created_at | ||||
|  * @property Carbon $updated_at | ||||
|  * @property int $created_by | ||||
|  * @property int $updated_by | ||||
|  * @property boolean $restricted | ||||
|  * @property int        $id | ||||
|  * @property string     $name | ||||
|  * @property string     $slug | ||||
|  * @property Carbon     $created_at | ||||
|  * @property Carbon     $updated_at | ||||
|  * @property int        $created_by | ||||
|  * @property int        $updated_by | ||||
|  * @property bool       $restricted | ||||
|  * @property Collection $tags | ||||
|  * | ||||
|  * @method static Entity|Builder visible() | ||||
|  * @method static Entity|Builder hasPermission(string $permission) | ||||
|  * @method static Builder withLastView() | ||||
| @@ -154,11 +157,12 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the comments for an entity | ||||
|      * Get the comments for an entity. | ||||
|      */ | ||||
|     public function comments(bool $orderByCreated = true): MorphMany | ||||
|     { | ||||
|         $query = $this->morphMany(Comment::class, 'entity'); | ||||
|  | ||||
|         return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query; | ||||
|     } | ||||
|  | ||||
| @@ -205,7 +209,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|  | ||||
|     /** | ||||
|      * Check if this instance or class is a certain type of entity. | ||||
|      * Examples of $type are 'page', 'book', 'chapter' | ||||
|      * Examples of $type are 'page', 'book', 'chapter'. | ||||
|      */ | ||||
|     public static function isA(string $type): bool | ||||
|     { | ||||
| @@ -218,6 +222,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|     public static function getType(): string | ||||
|     { | ||||
|         $className = array_slice(explode('\\', static::class), -1, 1)[0]; | ||||
|  | ||||
|         return strtolower($className); | ||||
|     } | ||||
|  | ||||
| @@ -229,6 +234,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|         if (mb_strlen($this->name) <= $length) { | ||||
|             return $this->name; | ||||
|         } | ||||
|  | ||||
|         return mb_substr($this->name, 0, $length - 3) . '...'; | ||||
|     } | ||||
|  | ||||
| @@ -248,14 +254,14 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|         $text = $this->getText(); | ||||
|  | ||||
|         if (mb_strlen($text) > $length) { | ||||
|             $text = mb_substr($text, 0, $length-3) . '...'; | ||||
|             $text = mb_substr($text, 0, $length - 3) . '...'; | ||||
|         } | ||||
|  | ||||
|         return trim($text); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the url of this entity | ||||
|      * Get the url of this entity. | ||||
|      */ | ||||
|     abstract public function getUrl(string $path = '/'): string; | ||||
|  | ||||
| @@ -272,6 +278,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|         if ($this instanceof Chapter) { | ||||
|             return $this->book()->withTrashed()->first(); | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
| @@ -285,7 +292,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Index the current entity for search | ||||
|      * Index the current entity for search. | ||||
|      */ | ||||
|     public function indexForSearch() | ||||
|     { | ||||
| @@ -298,6 +305,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable | ||||
|     public function refreshSlug(): string | ||||
|     { | ||||
|         $this->slug = app(SlugGenerator::class)->generate($this); | ||||
|  | ||||
|         return $this->slug; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,11 @@ | ||||
| <?php | ||||
|  | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use Illuminate\Database\Eloquent\Relations\BelongsTo; | ||||
|  | ||||
| interface HasCoverImage | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Get the cover image for this item. | ||||
|      */ | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Entities\Tools\PageContent; | ||||
| use BookStack\Uploads\Attachment; | ||||
| @@ -9,15 +11,16 @@ use Illuminate\Database\Eloquent\Relations\HasMany; | ||||
| use Permissions; | ||||
|  | ||||
| /** | ||||
|  * Class Page | ||||
|  * @property int $chapter_id | ||||
|  * @property string $html | ||||
|  * @property string $markdown | ||||
|  * @property string $text | ||||
|  * @property bool $template | ||||
|  * @property bool $draft | ||||
|  * @property int $revision_count | ||||
|  * @property Chapter $chapter | ||||
|  * Class Page. | ||||
|  * | ||||
|  * @property int        $chapter_id | ||||
|  * @property string     $html | ||||
|  * @property string     $markdown | ||||
|  * @property string     $text | ||||
|  * @property bool       $template | ||||
|  * @property bool       $draft | ||||
|  * @property int        $revision_count | ||||
|  * @property Chapter    $chapter | ||||
|  * @property Collection $attachments | ||||
|  */ | ||||
| class Page extends BookChild | ||||
| @@ -31,7 +34,7 @@ class Page extends BookChild | ||||
|     protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at']; | ||||
|  | ||||
|     protected $casts = [ | ||||
|         'draft' => 'boolean', | ||||
|         'draft'    => 'boolean', | ||||
|         'template' => 'boolean', | ||||
|     ]; | ||||
|  | ||||
| @@ -41,22 +44,26 @@ class Page extends BookChild | ||||
|     public function scopeVisible(Builder $query): Builder | ||||
|     { | ||||
|         $query = Permissions::enforceDraftVisibilityOnQuery($query); | ||||
|  | ||||
|         return parent::scopeVisible($query); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts this page into a simplified array. | ||||
|      * | ||||
|      * @return mixed | ||||
|      */ | ||||
|     public function toSimpleArray() | ||||
|     { | ||||
|         $array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes)); | ||||
|         $array['url'] = $this->getUrl(); | ||||
|  | ||||
|         return $array; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the chapter that this page is in, If applicable. | ||||
|      * | ||||
|      * @return BelongsTo | ||||
|      */ | ||||
|     public function chapter() | ||||
| @@ -66,6 +73,7 @@ class Page extends BookChild | ||||
|  | ||||
|     /** | ||||
|      * Check if this page has a chapter. | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function hasChapter() | ||||
| @@ -96,6 +104,7 @@ class Page extends BookChild | ||||
|  | ||||
|     /** | ||||
|      * Get the attachments assigned to this page. | ||||
|      * | ||||
|      * @return HasMany | ||||
|      */ | ||||
|     public function attachments() | ||||
| @@ -120,7 +129,8 @@ class Page extends BookChild | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the current revision for the page if existing | ||||
|      * Get the current revision for the page if existing. | ||||
|      * | ||||
|      * @return PageRevision|null | ||||
|      */ | ||||
|     public function getCurrentRevision() | ||||
| @@ -136,6 +146,7 @@ class Page extends BookChild | ||||
|         $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']); | ||||
|         $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown'])); | ||||
|         $refreshed->html = (new PageContent($refreshed))->render(); | ||||
|  | ||||
|         return $refreshed; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,29 +1,32 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Model; | ||||
| use Carbon\Carbon; | ||||
|  | ||||
| /** | ||||
|  * Class PageRevision | ||||
|  * @property int $page_id | ||||
|  * Class PageRevision. | ||||
|  * | ||||
|  * @property int    $page_id | ||||
|  * @property string $slug | ||||
|  * @property string $book_slug | ||||
|  * @property int $created_by | ||||
|  * @property int    $created_by | ||||
|  * @property Carbon $created_at | ||||
|  * @property string $type | ||||
|  * @property string $summary | ||||
|  * @property string $markdown | ||||
|  * @property string $html | ||||
|  * @property int $revision_number | ||||
|  * @property int    $revision_number | ||||
|  */ | ||||
| class PageRevision extends Model | ||||
| { | ||||
|     protected $fillable = ['name', 'html', 'text', 'markdown', 'summary']; | ||||
|  | ||||
|     /** | ||||
|      * Get the user that created the page revision | ||||
|      * Get the user that created the page revision. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||||
|      */ | ||||
|     public function createdBy() | ||||
| @@ -33,6 +36,7 @@ class PageRevision extends Model | ||||
|  | ||||
|     /** | ||||
|      * Get the page this revision originates from. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||||
|      */ | ||||
|     public function page() | ||||
| @@ -42,7 +46,9 @@ class PageRevision extends Model | ||||
|  | ||||
|     /** | ||||
|      * Get the url for this revision. | ||||
|      * | ||||
|      * @param null|string $path | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getUrl($path = null) | ||||
| @@ -51,11 +57,13 @@ class PageRevision extends Model | ||||
|         if ($path) { | ||||
|             return $url . '/' . trim($path, '/'); | ||||
|         } | ||||
|  | ||||
|         return $url; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the previous revision for the same page if existing | ||||
|      * Get the previous revision for the same page if existing. | ||||
|      * | ||||
|      * @return \BookStack\Entities\PageRevision|null | ||||
|      */ | ||||
|     public function getPrevious() | ||||
| @@ -74,8 +82,10 @@ class PageRevision extends Model | ||||
|     /** | ||||
|      * Allows checking of the exact class, Used to check entity type. | ||||
|      * Included here to align with entities in similar use cases. | ||||
|      * (Yup, Bit of an awkward hack) | ||||
|      * (Yup, Bit of an awkward hack). | ||||
|      * | ||||
|      * @param $type | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public static function isA($type) | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| <?php namespace BookStack\Entities\Models; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Models; | ||||
|  | ||||
| use BookStack\Model; | ||||
|  | ||||
| class SearchTerm extends Model | ||||
| { | ||||
|  | ||||
|     protected $fillable = ['term', 'entity_id', 'entity_type', 'score']; | ||||
|     public $timestamps = false; | ||||
|  | ||||
|     /** | ||||
|      * Get the entity that this term belongs to | ||||
|      * Get the entity that this term belongs to. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Relations\MorphTo | ||||
|      */ | ||||
|     public function entity() | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Queries; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Queries; | ||||
|  | ||||
| use BookStack\Auth\Permissions\PermissionService; | ||||
| use BookStack\Entities\EntityProvider; | ||||
| @@ -14,4 +16,4 @@ abstract class EntityQuery | ||||
|     { | ||||
|         return app()->make(EntityProvider::class); | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Queries; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Queries; | ||||
|  | ||||
| use BookStack\Actions\View; | ||||
| use Illuminate\Support\Facades\DB; | ||||
| @@ -25,5 +26,4 @@ class Popular extends EntityQuery | ||||
|             ->pluck('viewable') | ||||
|             ->filter(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Queries; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Queries; | ||||
|  | ||||
| use BookStack\Actions\View; | ||||
| use Illuminate\Support\Collection; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Queries; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Queries; | ||||
|  | ||||
| use BookStack\Actions\Favourite; | ||||
| use Illuminate\Database\Query\JoinClause; | ||||
|   | ||||
| @@ -2,24 +2,18 @@ | ||||
|  | ||||
| namespace BookStack\Entities\Repos; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Actions\TagRepo; | ||||
| use BookStack\Auth\User; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| use BookStack\Entities\Models\HasCoverImage; | ||||
| use BookStack\Exceptions\ImageUploadException; | ||||
| use BookStack\Facades\Activity; | ||||
| use BookStack\Uploads\ImageRepo; | ||||
| use Illuminate\Http\UploadedFile; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| class BaseRepo | ||||
| { | ||||
|  | ||||
|     protected $tagRepo; | ||||
|     protected $imageRepo; | ||||
|  | ||||
|  | ||||
|     public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo) | ||||
|     { | ||||
|         $this->tagRepo = $tagRepo; | ||||
| @@ -27,7 +21,7 @@ class BaseRepo | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a new entity in the system | ||||
|      * Create a new entity in the system. | ||||
|      */ | ||||
|     public function create(Entity $entity, array $input) | ||||
|     { | ||||
| @@ -35,7 +29,7 @@ class BaseRepo | ||||
|         $entity->forceFill([ | ||||
|             'created_by' => user()->id, | ||||
|             'updated_by' => user()->id, | ||||
|             'owned_by' => user()->id, | ||||
|             'owned_by'   => user()->id, | ||||
|         ]); | ||||
|         $entity->refreshSlug(); | ||||
|         $entity->save(); | ||||
| @@ -72,6 +66,7 @@ class BaseRepo | ||||
|  | ||||
|     /** | ||||
|      * Update the given items' cover image, or clear it. | ||||
|      * | ||||
|      * @throws ImageUploadException | ||||
|      * @throws \Exception | ||||
|      */ | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Repos; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Repos; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Actions\TagRepo; | ||||
| @@ -15,7 +17,6 @@ use Illuminate\Support\Collection; | ||||
|  | ||||
| class BookRepo | ||||
| { | ||||
|  | ||||
|     protected $baseRepo; | ||||
|     protected $tagRepo; | ||||
|     protected $imageRepo; | ||||
| @@ -84,13 +85,14 @@ class BookRepo | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a new book in the system | ||||
|      * Create a new book in the system. | ||||
|      */ | ||||
|     public function create(array $input): Book | ||||
|     { | ||||
|         $book = new Book(); | ||||
|         $this->baseRepo->create($book, $input); | ||||
|         Activity::addForEntity($book, ActivityType::BOOK_CREATE); | ||||
|  | ||||
|         return $book; | ||||
|     } | ||||
|  | ||||
| @@ -101,11 +103,13 @@ class BookRepo | ||||
|     { | ||||
|         $this->baseRepo->update($book, $input); | ||||
|         Activity::addForEntity($book, ActivityType::BOOK_UPDATE); | ||||
|  | ||||
|         return $book; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update the given book's cover image, or clear it. | ||||
|      * | ||||
|      * @throws ImageUploadException | ||||
|      * @throws Exception | ||||
|      */ | ||||
| @@ -116,6 +120,7 @@ class BookRepo | ||||
|  | ||||
|     /** | ||||
|      * Remove a book from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroy(Book $book) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Repos; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Repos; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Entities\Models\Book; | ||||
| @@ -89,6 +91,7 @@ class BookshelfRepo | ||||
|         $this->baseRepo->create($shelf, $input); | ||||
|         $this->updateBooks($shelf, $bookIds); | ||||
|         Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE); | ||||
|  | ||||
|         return $shelf; | ||||
|     } | ||||
|  | ||||
| @@ -104,6 +107,7 @@ class BookshelfRepo | ||||
|         } | ||||
|  | ||||
|         Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE); | ||||
|  | ||||
|         return $shelf; | ||||
|     } | ||||
|  | ||||
| @@ -129,6 +133,7 @@ class BookshelfRepo | ||||
|  | ||||
|     /** | ||||
|      * Update the given shelf cover image, or clear it. | ||||
|      * | ||||
|      * @throws ImageUploadException | ||||
|      * @throws Exception | ||||
|      */ | ||||
| @@ -164,6 +169,7 @@ class BookshelfRepo | ||||
|  | ||||
|     /** | ||||
|      * Remove a bookshelf from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroy(Bookshelf $shelf) | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Repos; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Repos; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Entities\Models\Book; | ||||
| @@ -9,11 +11,9 @@ use BookStack\Exceptions\MoveOperationException; | ||||
| use BookStack\Exceptions\NotFoundException; | ||||
| use BookStack\Facades\Activity; | ||||
| use Exception; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| class ChapterRepo | ||||
| { | ||||
|  | ||||
|     protected $baseRepo; | ||||
|  | ||||
|     /** | ||||
| @@ -26,6 +26,7 @@ class ChapterRepo | ||||
|  | ||||
|     /** | ||||
|      * Get a chapter via the slug. | ||||
|      * | ||||
|      * @throws NotFoundException | ||||
|      */ | ||||
|     public function getBySlug(string $bookSlug, string $chapterSlug): Chapter | ||||
| @@ -49,6 +50,7 @@ class ChapterRepo | ||||
|         $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1; | ||||
|         $this->baseRepo->create($chapter, $input); | ||||
|         Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE); | ||||
|  | ||||
|         return $chapter; | ||||
|     } | ||||
|  | ||||
| @@ -59,11 +61,13 @@ class ChapterRepo | ||||
|     { | ||||
|         $this->baseRepo->update($chapter, $input); | ||||
|         Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE); | ||||
|  | ||||
|         return $chapter; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a chapter from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroy(Chapter $chapter) | ||||
| @@ -77,7 +81,8 @@ class ChapterRepo | ||||
|     /** | ||||
|      * Move the given chapter into a new parent book. | ||||
|      * The $parentIdentifier must be a string of the following format: | ||||
|      * 'book:<id>' (book:5) | ||||
|      * 'book:<id>' (book:5). | ||||
|      * | ||||
|      * @throws MoveOperationException | ||||
|      */ | ||||
|     public function move(Chapter $chapter, string $parentIdentifier): Book | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| <?php namespace BookStack\Entities\Repos; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Repos; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\Chapter; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Entities\Models\PageRevision; | ||||
| use BookStack\Entities\Tools\BookContents; | ||||
| use BookStack\Entities\Tools\PageContent; | ||||
| use BookStack\Entities\Tools\TrashCan; | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Entities\Models\PageRevision; | ||||
| use BookStack\Exceptions\MoveOperationException; | ||||
| use BookStack\Exceptions\NotFoundException; | ||||
| use BookStack\Exceptions\PermissionsException; | ||||
| @@ -16,11 +18,9 @@ use BookStack\Facades\Activity; | ||||
| use Exception; | ||||
| use Illuminate\Database\Eloquent\Builder; | ||||
| use Illuminate\Pagination\LengthAwarePaginator; | ||||
| use Illuminate\Support\Collection; | ||||
|  | ||||
| class PageRepo | ||||
| { | ||||
|  | ||||
|     protected $baseRepo; | ||||
|  | ||||
|     /** | ||||
| @@ -33,6 +33,7 @@ class PageRepo | ||||
|  | ||||
|     /** | ||||
|      * Get a page by ID. | ||||
|      * | ||||
|      * @throws NotFoundException | ||||
|      */ | ||||
|     public function getById(int $id, array $relations = ['book']): Page | ||||
| @@ -48,6 +49,7 @@ class PageRepo | ||||
|  | ||||
|     /** | ||||
|      * Get a page its book and own slug. | ||||
|      * | ||||
|      * @throws NotFoundException | ||||
|      */ | ||||
|     public function getBySlug(string $bookSlug, string $pageSlug): Page | ||||
| @@ -77,6 +79,7 @@ class PageRepo | ||||
|             ->orderBy('created_at', 'desc') | ||||
|             ->with('page') | ||||
|             ->first(); | ||||
|  | ||||
|         return $revision ? $revision->page : null; | ||||
|     } | ||||
|  | ||||
| @@ -119,6 +122,7 @@ class PageRepo | ||||
|     public function getUserDraft(Page $page): ?PageRevision | ||||
|     { | ||||
|         $revision = $this->getUserDraftQuery($page)->first(); | ||||
|  | ||||
|         return $revision; | ||||
|     } | ||||
|  | ||||
| @@ -128,11 +132,11 @@ class PageRepo | ||||
|     public function getNewDraftPage(Entity $parent) | ||||
|     { | ||||
|         $page = (new Page())->forceFill([ | ||||
|             'name' => trans('entities.pages_initial_name'), | ||||
|             'name'       => trans('entities.pages_initial_name'), | ||||
|             'created_by' => user()->id, | ||||
|             'owned_by' => user()->id, | ||||
|             'owned_by'   => user()->id, | ||||
|             'updated_by' => user()->id, | ||||
|             'draft' => true, | ||||
|             'draft'      => true, | ||||
|         ]); | ||||
|  | ||||
|         if ($parent instanceof Chapter) { | ||||
| @@ -144,6 +148,7 @@ class PageRepo | ||||
|  | ||||
|         $page->save(); | ||||
|         $page->refresh()->rebuildPermissions(); | ||||
|  | ||||
|         return $page; | ||||
|     } | ||||
|  | ||||
| @@ -166,6 +171,7 @@ class PageRepo | ||||
|         $draft->refresh(); | ||||
|  | ||||
|         Activity::addForEntity($draft, ActivityType::PAGE_CREATE); | ||||
|  | ||||
|         return $draft; | ||||
|     } | ||||
|  | ||||
| @@ -190,7 +196,7 @@ class PageRepo | ||||
|         $this->getUserDraftQuery($page)->delete(); | ||||
|  | ||||
|         // Save a revision after updating | ||||
|         $summary = trim($input['summary'] ?? ""); | ||||
|         $summary = trim($input['summary'] ?? ''); | ||||
|         $htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml; | ||||
|         $nameChanged = isset($input['name']) && $input['name'] !== $oldName; | ||||
|         $markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown; | ||||
| @@ -199,6 +205,7 @@ class PageRepo | ||||
|         } | ||||
|  | ||||
|         Activity::addForEntity($page, ActivityType::PAGE_UPDATE); | ||||
|  | ||||
|         return $page; | ||||
|     } | ||||
|  | ||||
| @@ -234,6 +241,7 @@ class PageRepo | ||||
|         $revision->save(); | ||||
|  | ||||
|         $this->deleteOldRevisions($page); | ||||
|  | ||||
|         return $revision; | ||||
|     } | ||||
|  | ||||
| @@ -249,6 +257,7 @@ class PageRepo | ||||
|             } | ||||
|             $page->fill($input); | ||||
|             $page->save(); | ||||
|  | ||||
|             return $page; | ||||
|         } | ||||
|  | ||||
| @@ -260,11 +269,13 @@ class PageRepo | ||||
|         } | ||||
|  | ||||
|         $draft->save(); | ||||
|  | ||||
|         return $draft; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Destroy a page from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroy(Page $page) | ||||
| @@ -291,7 +302,7 @@ class PageRepo | ||||
|         } else { | ||||
|             $content->setNewHTML($revision->html); | ||||
|         } | ||||
|          | ||||
|  | ||||
|         $page->updated_by = user()->id; | ||||
|         $page->refreshSlug(); | ||||
|         $page->save(); | ||||
| @@ -301,13 +312,15 @@ class PageRepo | ||||
|         $this->savePageRevision($page, $summary); | ||||
|  | ||||
|         Activity::addForEntity($page, ActivityType::PAGE_RESTORE); | ||||
|  | ||||
|         return $page; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Move the given page into a new parent book or chapter. | ||||
|      * The $parentIdentifier must be a string of the following format: | ||||
|      * 'book:<id>' (book:5) | ||||
|      * 'book:<id>' (book:5). | ||||
|      * | ||||
|      * @throws MoveOperationException | ||||
|      * @throws PermissionsException | ||||
|      */ | ||||
| @@ -327,12 +340,14 @@ class PageRepo | ||||
|         $page->rebuildPermissions(); | ||||
|  | ||||
|         Activity::addForEntity($page, ActivityType::PAGE_MOVE); | ||||
|  | ||||
|         return $parent; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Copy an existing page in the system. | ||||
|      * Optionally providing a new parent via string identifier and a new name. | ||||
|      * | ||||
|      * @throws MoveOperationException | ||||
|      * @throws PermissionsException | ||||
|      */ | ||||
| @@ -369,7 +384,8 @@ class PageRepo | ||||
|     /** | ||||
|      * Find a page parent entity via a identifier string in the format: | ||||
|      * {type}:{id} | ||||
|      * Example: (book:5) | ||||
|      * Example: (book:5). | ||||
|      * | ||||
|      * @throws MoveOperationException | ||||
|      */ | ||||
|     protected function findParentByIdentifier(string $identifier): ?Entity | ||||
| @@ -383,6 +399,7 @@ class PageRepo | ||||
|         } | ||||
|  | ||||
|         $parentClass = $entityType === 'book' ? Book::class : Chapter::class; | ||||
|  | ||||
|         return $parentClass::visible()->where('id', '=', $entityId)->first(); | ||||
|     } | ||||
|  | ||||
| @@ -420,6 +437,7 @@ class PageRepo | ||||
|         $draft->book_slug = $page->book->slug; | ||||
|         $draft->created_by = user()->id; | ||||
|         $draft->type = 'update_draft'; | ||||
|  | ||||
|         return $draft; | ||||
|     } | ||||
|  | ||||
| @@ -445,13 +463,14 @@ class PageRepo | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a new priority for a page | ||||
|      * Get a new priority for a page. | ||||
|      */ | ||||
|     protected function getNewPriority(Page $page): int | ||||
|     { | ||||
|         $parent = $page->getParent(); | ||||
|         if ($parent instanceof Chapter) { | ||||
|             $lastPage = $parent->pages('desc')->first(); | ||||
|  | ||||
|             return $lastPage ? $lastPage->priority + 1 : 0; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\BookChild; | ||||
| @@ -10,7 +12,6 @@ use Illuminate\Support\Collection; | ||||
|  | ||||
| class BookContents | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * @var Book | ||||
|      */ | ||||
| @@ -35,6 +36,7 @@ class BookContents | ||||
|             ->where('chapter_id', '=', 0)->max('priority'); | ||||
|         $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id) | ||||
|             ->max('priority'); | ||||
|  | ||||
|         return max($maxChapter, $maxPage, 1); | ||||
|     } | ||||
|  | ||||
| @@ -83,6 +85,7 @@ class BookContents | ||||
|             if (isset($entity['draft']) && $entity['draft']) { | ||||
|                 return -100; | ||||
|             } | ||||
|  | ||||
|             return $entity['priority'] ?? 0; | ||||
|         }; | ||||
|     } | ||||
| @@ -110,9 +113,10 @@ class BookContents | ||||
|      *     +"parentChapter": false (ID of parent chapter, as string, or false) | ||||
|      *     +"type": "page" (Entity type of item) | ||||
|      *     +"book": "1" (Id of book to place item in) | ||||
|      *   } | ||||
|      *   }. | ||||
|      * | ||||
|      * Returns a list of books that were involved in the operation. | ||||
|      * | ||||
|      * @throws SortOperationException | ||||
|      */ | ||||
|     public function sortUsingMap(Collection $sortMap): Collection | ||||
| @@ -190,6 +194,7 @@ class BookContents | ||||
|     /** | ||||
|      * Get the books involved in a sort. | ||||
|      * The given sort map should have its models loaded first. | ||||
|      * | ||||
|      * @throws SortOperationException | ||||
|      */ | ||||
|     protected function getBooksInvolvedInSort(Collection $sortMap): Collection | ||||
| @@ -202,7 +207,7 @@ class BookContents | ||||
|         $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get(); | ||||
|  | ||||
|         if (count($books) !== count($bookIdsInvolved)) { | ||||
|             throw new SortOperationException("Could not find all books requested in sort operation"); | ||||
|             throw new SortOperationException('Could not find all books requested in sort operation'); | ||||
|         } | ||||
|  | ||||
|         return $books; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\Chapter; | ||||
| @@ -12,7 +14,6 @@ use Throwable; | ||||
|  | ||||
| class ExportFormatter | ||||
| { | ||||
|  | ||||
|     protected $imageService; | ||||
|  | ||||
|     /** | ||||
| @@ -26,20 +27,23 @@ class ExportFormatter | ||||
|     /** | ||||
|      * Convert a page to a self-contained HTML file. | ||||
|      * Includes required CSS & image content. Images are base64 encoded into the HTML. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function pageToContainedHtml(Page $page) | ||||
|     { | ||||
|         $page->html = (new PageContent($page))->render(); | ||||
|         $pageHtml = view('pages.export', [ | ||||
|             'page' => $page, | ||||
|             'page'   => $page, | ||||
|             'format' => 'html', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->containHtml($pageHtml); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert a chapter to a self-contained HTML file. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function chapterToContainedHtml(Chapter $chapter) | ||||
| @@ -50,43 +54,49 @@ class ExportFormatter | ||||
|         }); | ||||
|         $html = view('chapters.export', [ | ||||
|             'chapter' => $chapter, | ||||
|             'pages' => $pages, | ||||
|             'format' => 'html', | ||||
|             'pages'   => $pages, | ||||
|             'format'  => 'html', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->containHtml($html); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert a book to a self-contained HTML file. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function bookToContainedHtml(Book $book) | ||||
|     { | ||||
|         $bookTree = (new BookContents($book))->getTree(false, true); | ||||
|         $html = view('books.export', [ | ||||
|             'book' => $book, | ||||
|             'book'         => $book, | ||||
|             'bookChildren' => $bookTree, | ||||
|             'format' => 'html', | ||||
|             'format'       => 'html', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->containHtml($html); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert a page to a PDF file. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function pageToPdf(Page $page) | ||||
|     { | ||||
|         $page->html = (new PageContent($page))->render(); | ||||
|         $html = view('pages.export', [ | ||||
|             'page' => $page, | ||||
|             'page'   => $page, | ||||
|             'format' => 'pdf', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->htmlToPdf($html); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert a chapter to a PDF file. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function chapterToPdf(Chapter $chapter) | ||||
| @@ -98,8 +108,8 @@ class ExportFormatter | ||||
|  | ||||
|         $html = view('chapters.export', [ | ||||
|             'chapter' => $chapter, | ||||
|             'pages' => $pages, | ||||
|             'format' => 'pdf', | ||||
|             'pages'   => $pages, | ||||
|             'format'  => 'pdf', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->htmlToPdf($html); | ||||
| @@ -107,21 +117,24 @@ class ExportFormatter | ||||
|  | ||||
|     /** | ||||
|      * Convert a book to a PDF file. | ||||
|      * | ||||
|      * @throws Throwable | ||||
|      */ | ||||
|     public function bookToPdf(Book $book) | ||||
|     { | ||||
|         $bookTree = (new BookContents($book))->getTree(false, true); | ||||
|         $html = view('books.export', [ | ||||
|             'book' => $book, | ||||
|             'book'         => $book, | ||||
|             'bookChildren' => $bookTree, | ||||
|             'format' => 'pdf', | ||||
|             'format'       => 'pdf', | ||||
|         ])->render(); | ||||
|  | ||||
|         return $this->htmlToPdf($html); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert normal web-page HTML to a PDF. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function htmlToPdf(string $html): string | ||||
| @@ -134,11 +147,13 @@ class ExportFormatter | ||||
|         } else { | ||||
|             $pdf = DomPDF::loadHTML($containedHtml); | ||||
|         } | ||||
|  | ||||
|         return $pdf->output(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Bundle of the contents of a html file to be self-contained. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function containHtml(string $htmlContent): string | ||||
| @@ -195,6 +210,7 @@ class ExportFormatter | ||||
|         $text = html_entity_decode($text); | ||||
|         // Add title | ||||
|         $text = $page->name . "\n\n" . $text; | ||||
|  | ||||
|         return $text; | ||||
|     } | ||||
|  | ||||
| @@ -208,6 +224,7 @@ class ExportFormatter | ||||
|         foreach ($chapter->getVisiblePages() as $page) { | ||||
|             $text .= $this->pageToPlainText($page); | ||||
|         } | ||||
|  | ||||
|         return $text; | ||||
|     } | ||||
|  | ||||
| @@ -225,6 +242,7 @@ class ExportFormatter | ||||
|                 $text .= $this->pageToPlainText($bookChild); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $text; | ||||
|     } | ||||
|  | ||||
| @@ -234,10 +252,10 @@ class ExportFormatter | ||||
|     public function pageToMarkdown(Page $page): string | ||||
|     { | ||||
|         if ($page->markdown) { | ||||
|             return "# " . $page->name . "\n\n" . $page->markdown; | ||||
|             return '# ' . $page->name . "\n\n" . $page->markdown; | ||||
|         } | ||||
|  | ||||
|         return "# " . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert(); | ||||
|         return '# ' . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -245,11 +263,12 @@ class ExportFormatter | ||||
|      */ | ||||
|     public function chapterToMarkdown(Chapter $chapter): string | ||||
|     { | ||||
|         $text = "# " . $chapter->name . "\n\n"; | ||||
|         $text = '# ' . $chapter->name . "\n\n"; | ||||
|         $text .= $chapter->description . "\n\n"; | ||||
|         foreach ($chapter->pages as $page) { | ||||
|             $text .= $this->pageToMarkdown($page) . "\n\n"; | ||||
|         } | ||||
|  | ||||
|         return $text; | ||||
|     } | ||||
|  | ||||
| @@ -259,7 +278,7 @@ class ExportFormatter | ||||
|     public function bookToMarkdown(Book $book): string | ||||
|     { | ||||
|         $bookTree = (new BookContents($book))->getTree(false, true); | ||||
|         $text = "# " . $book->name . "\n\n"; | ||||
|         $text = '# ' . $book->name . "\n\n"; | ||||
|         foreach ($bookTree as $bookChild) { | ||||
|             if ($bookChild instanceof Chapter) { | ||||
|                 $text .= $this->chapterToMarkdown($bookChild); | ||||
| @@ -267,6 +286,7 @@ class ExportFormatter | ||||
|                 $text .= $this->pageToMarkdown($bookChild); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return $text; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools\Markdown; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools\Markdown; | ||||
|  | ||||
| use League\HTMLToMarkdown\Converter\ParagraphConverter; | ||||
| use League\HTMLToMarkdown\ElementInterface; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools\Markdown; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools\Markdown; | ||||
|  | ||||
| use League\CommonMark\ConfigurableEnvironmentInterface; | ||||
| use League\CommonMark\Extension\ExtensionInterface; | ||||
| @@ -7,7 +9,6 @@ use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor; | ||||
|  | ||||
| class CustomStrikeThroughExtension implements ExtensionInterface | ||||
| { | ||||
|  | ||||
|     public function register(ConfigurableEnvironmentInterface $environment) | ||||
|     { | ||||
|         $environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor()); | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools\Markdown; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools\Markdown; | ||||
|  | ||||
| use League\CommonMark\ElementRendererInterface; | ||||
| use League\CommonMark\Extension\Strikethrough\Strikethrough; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools\Markdown; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools\Markdown; | ||||
|  | ||||
| use League\HTMLToMarkdown\Converter\BlockquoteConverter; | ||||
| use League\HTMLToMarkdown\Converter\CodeConverter; | ||||
| @@ -27,12 +29,13 @@ class HtmlToMarkdown | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Run the conversion | ||||
|      * Run the conversion. | ||||
|      */ | ||||
|     public function convert(): string | ||||
|     { | ||||
|         $converter = new HtmlConverter($this->getConverterEnvironment()); | ||||
|         $html = $this->prepareHtml($this->html); | ||||
|  | ||||
|         return $converter->convert($html); | ||||
|     } | ||||
|  | ||||
| @@ -54,19 +57,19 @@ class HtmlToMarkdown | ||||
|     protected function getConverterEnvironment(): Environment | ||||
|     { | ||||
|         $environment = new Environment([ | ||||
|             'header_style' => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2 | ||||
|             'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML | ||||
|             'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output. | ||||
|             'header_style'            => 'atx', // Set to 'atx' to output H1 and H2 headers as # Header1 and ## Header2 | ||||
|             'suppress_errors'         => true, // Set to false to show warnings when loading malformed HTML | ||||
|             'strip_tags'              => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output. | ||||
|             'strip_placeholder_links' => false, // Set to true to remove <a> that doesn't have href. | ||||
|             'bold_style' => '**', // DEPRECATED: Set to '__' if you prefer the underlined style | ||||
|             'italic_style' => '*', // DEPRECATED: Set to '_' if you prefer the underlined style | ||||
|             'remove_nodes' => '', // space-separated list of dom nodes that should be removed. example: 'meta style script' | ||||
|             'hard_break' => false, // Set to true to turn <br> into `\n` instead of `  \n` | ||||
|             'list_item_style' => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+' | ||||
|             'preserve_comments' => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments | ||||
|             'use_autolinks' => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false | ||||
|             'table_pipe_escape' => '\|', // Replacement string for pipe characters inside markdown table cells | ||||
|             'table_caption_side' => 'top', // Set to 'top' or 'bottom' to show <caption> content before or after table, null to suppress | ||||
|             'bold_style'              => '**', // DEPRECATED: Set to '__' if you prefer the underlined style | ||||
|             'italic_style'            => '*', // DEPRECATED: Set to '_' if you prefer the underlined style | ||||
|             'remove_nodes'            => '', // space-separated list of dom nodes that should be removed. example: 'meta style script' | ||||
|             'hard_break'              => false, // Set to true to turn <br> into `\n` instead of `  \n` | ||||
|             'list_item_style'         => '-', // Set the default character for each <li> in a <ul>. Can be '-', '*', or '+' | ||||
|             'preserve_comments'       => false, // Set to true to preserve comments, or set to an array of strings to preserve specific comments | ||||
|             'use_autolinks'           => false, // Set to true to use simple link syntax if possible. Will always use []() if set to false | ||||
|             'table_pipe_escape'       => '\|', // Replacement string for pipe characters inside markdown table cells | ||||
|             'table_caption_side'      => 'top', // Set to 'top' or 'bottom' to show <caption> content before or after table, null to suppress | ||||
|         ]); | ||||
|  | ||||
|         $environment->addConverter(new BlockquoteConverter()); | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\BookChild; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| @@ -48,6 +50,7 @@ class NextPreviousContentLocator | ||||
|             return get_class($entity) === get_class($this->relativeBookItem) | ||||
|                 && $entity->id === $this->relativeBookItem->id; | ||||
|         }); | ||||
|  | ||||
|         return $index === false ? null : $index; | ||||
|     } | ||||
|  | ||||
| @@ -64,6 +67,7 @@ class NextPreviousContentLocator | ||||
|             $childPages = $item->visible_pages ?? []; | ||||
|             $flatOrdered = $flatOrdered->concat($childPages); | ||||
|         } | ||||
|  | ||||
|         return $flatOrdered; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension; | ||||
| use BookStack\Exceptions\ImageUploadException; | ||||
| use BookStack\Facades\Theme; | ||||
| use BookStack\Theming\ThemeEvents; | ||||
| use BookStack\Util\HtmlContentFilter; | ||||
| use BookStack\Uploads\ImageRepo; | ||||
| use BookStack\Util\HtmlContentFilter; | ||||
| use DOMDocument; | ||||
| use DOMNodeList; | ||||
| use DOMXPath; | ||||
| @@ -18,7 +20,6 @@ use League\CommonMark\Extension\TaskList\TaskListExtension; | ||||
|  | ||||
| class PageContent | ||||
| { | ||||
|  | ||||
|     protected $page; | ||||
|  | ||||
|     /** | ||||
| @@ -62,11 +63,12 @@ class PageContent | ||||
|         $environment->addExtension(new CustomStrikeThroughExtension()); | ||||
|         $environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment; | ||||
|         $converter = new CommonMarkConverter([], $environment); | ||||
|  | ||||
|         return $converter->convertToHtml($markdown); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert all base64 image data to saved images | ||||
|      * Convert all base64 image data to saved images. | ||||
|      */ | ||||
|     public function extractBase64Images(Page $page, string $htmlText): string | ||||
|     { | ||||
| @@ -97,6 +99,7 @@ class PageContent | ||||
|  | ||||
|             // Save image from data with a random name | ||||
|             $imageName = 'embedded-image-' . Str::random(8) . '.' . $extension; | ||||
|  | ||||
|             try { | ||||
|                 $image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id); | ||||
|                 $imageNode->setAttribute('src', $image->url); | ||||
| @@ -171,7 +174,7 @@ class PageContent | ||||
|     /** | ||||
|      * Set a unique id on the given DOMElement. | ||||
|      * A map for existing ID's should be passed in to check for current existence. | ||||
|      * Returns a pair of strings in the format [old_id, new_id] | ||||
|      * Returns a pair of strings in the format [old_id, new_id]. | ||||
|      */ | ||||
|     protected function setUniqueId(\DOMNode $element, array &$idMap): array | ||||
|     { | ||||
| @@ -183,6 +186,7 @@ class PageContent | ||||
|         $existingId = $element->getAttribute('id'); | ||||
|         if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) { | ||||
|             $idMap[$existingId] = true; | ||||
|  | ||||
|             return [$existingId, $existingId]; | ||||
|         } | ||||
|  | ||||
| @@ -200,6 +204,7 @@ class PageContent | ||||
|  | ||||
|         $element->setAttribute('id', $newId); | ||||
|         $idMap[$newId] = true; | ||||
|  | ||||
|         return [$existingId, $newId]; | ||||
|     } | ||||
|  | ||||
| @@ -209,11 +214,12 @@ class PageContent | ||||
|     protected function toPlainText(): string | ||||
|     { | ||||
|         $html = $this->render(true); | ||||
|  | ||||
|         return html_entity_decode(strip_tags($html)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Render the page for viewing | ||||
|      * Render the page for viewing. | ||||
|      */ | ||||
|     public function render(bool $blankIncludes = false): string | ||||
|     { | ||||
| @@ -233,7 +239,7 @@ class PageContent | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Parse the headers on the page to get a navigation menu | ||||
|      * Parse the headers on the page to get a navigation menu. | ||||
|      */ | ||||
|     public function getNavigation(string $htmlContent): array | ||||
|     { | ||||
| @@ -243,7 +249,7 @@ class PageContent | ||||
|  | ||||
|         $doc = $this->loadDocumentFromHtml($htmlContent); | ||||
|         $xPath = new DOMXPath($doc); | ||||
|         $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6"); | ||||
|         $headers = $xPath->query('//h1|//h2|//h3|//h4|//h5|//h6'); | ||||
|  | ||||
|         return $headers ? $this->headerNodesToLevelList($headers) : []; | ||||
|     } | ||||
| @@ -260,9 +266,9 @@ class PageContent | ||||
|  | ||||
|             return [ | ||||
|                 'nodeName' => strtolower($header->nodeName), | ||||
|                 'level' => intval(str_replace('h', '', $header->nodeName)), | ||||
|                 'link' => '#' . $header->getAttribute('id'), | ||||
|                 'text' => $text, | ||||
|                 'level'    => intval(str_replace('h', '', $header->nodeName)), | ||||
|                 'link'     => '#' . $header->getAttribute('id'), | ||||
|                 'text'     => $text, | ||||
|             ]; | ||||
|         })->filter(function ($header) { | ||||
|             return mb_strlen($header['text']) > 0; | ||||
| @@ -272,6 +278,7 @@ class PageContent | ||||
|         $levelChange = ($tree->pluck('level')->min() - 1); | ||||
|         $tree = $tree->map(function ($header) use ($levelChange) { | ||||
|             $header['level'] -= ($levelChange); | ||||
|  | ||||
|             return $header; | ||||
|         }); | ||||
|  | ||||
| @@ -325,7 +332,6 @@ class PageContent | ||||
|         return $html; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Fetch the content from a specific section of the given page. | ||||
|      */ | ||||
| @@ -365,6 +371,7 @@ class PageContent | ||||
|         $doc = new DOMDocument(); | ||||
|         $html = '<body>' . $html . '</body>'; | ||||
|         $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); | ||||
|  | ||||
|         return $doc; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Entities\Models\PageRevision; | ||||
| @@ -7,7 +9,6 @@ use Illuminate\Database\Eloquent\Builder; | ||||
|  | ||||
| class PageEditActivity | ||||
| { | ||||
|  | ||||
|     protected $page; | ||||
|  | ||||
|     /** | ||||
| @@ -20,6 +21,7 @@ class PageEditActivity | ||||
|  | ||||
|     /** | ||||
|      * Check if there's active editing being performed on this page. | ||||
|      * | ||||
|      * @return bool | ||||
|      */ | ||||
|     public function hasActiveEditing(): bool | ||||
| @@ -35,14 +37,17 @@ class PageEditActivity | ||||
|         $pageDraftEdits = $this->activePageEditingQuery(60)->get(); | ||||
|         $count = $pageDraftEdits->count(); | ||||
|  | ||||
|         $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); | ||||
|         $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]) : trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]); | ||||
|         $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]); | ||||
|  | ||||
|         return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the message to show when the user will be editing one of their drafts. | ||||
|      * | ||||
|      * @param PageRevision $draft | ||||
|      * | ||||
|      * @return string | ||||
|      */ | ||||
|     public function getEditingActiveDraftMessage(PageRevision $draft): string | ||||
| @@ -51,6 +56,7 @@ class PageEditActivity | ||||
|         if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) { | ||||
|             return $message; | ||||
|         } | ||||
|  | ||||
|         return $message . "\n" . trans('entities.pages_draft_edited_notification'); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Actions\ActivityType; | ||||
| use BookStack\Auth\User; | ||||
| @@ -9,7 +11,6 @@ use Illuminate\Support\Collection; | ||||
|  | ||||
| class PermissionsUpdater | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Update an entities permissions from a permission form submit request. | ||||
|      */ | ||||
| @@ -60,8 +61,8 @@ class PermissionsUpdater | ||||
|             return collect($restrictions)->keys()->map(function ($action) use ($roleId) { | ||||
|                 return [ | ||||
|                     'role_id' => $roleId, | ||||
|                     'action' => strtolower($action), | ||||
|                 ] ; | ||||
|                     'action'  => strtolower($action), | ||||
|                 ]; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\EntityProvider; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| @@ -17,14 +19,12 @@ class SearchIndex | ||||
|      */ | ||||
|     protected $entityProvider; | ||||
|  | ||||
|  | ||||
|     public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider) | ||||
|     { | ||||
|         $this->searchTerm = $searchTerm; | ||||
|         $this->entityProvider = $entityProvider; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Index the given entity. | ||||
|      */ | ||||
| @@ -42,7 +42,8 @@ class SearchIndex | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Index multiple Entities at once | ||||
|      * Index multiple Entities at once. | ||||
|      * | ||||
|      * @param Entity[] $entities | ||||
|      */ | ||||
|     protected function indexEntities(array $entities) | ||||
| @@ -110,8 +111,8 @@ class SearchIndex | ||||
|         $terms = []; | ||||
|         foreach ($tokenMap as $token => $count) { | ||||
|             $terms[] = [ | ||||
|                 'term' => $token, | ||||
|                 'score' => $count * $scoreAdjustment | ||||
|                 'term'  => $token, | ||||
|                 'score' => $count * $scoreAdjustment, | ||||
|             ]; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use Illuminate\Http\Request; | ||||
|  | ||||
| class SearchOptions | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * @var array | ||||
|      */ | ||||
| @@ -35,6 +36,7 @@ class SearchOptions | ||||
|         foreach ($decoded as $type => $value) { | ||||
|             $instance->$type = $value; | ||||
|         } | ||||
|  | ||||
|         return $instance; | ||||
|     } | ||||
|  | ||||
| @@ -67,6 +69,7 @@ class SearchOptions | ||||
|         if (isset($inputs['types']) && count($inputs['types']) < 4) { | ||||
|             $instance->filters['type'] = implode('|', $inputs['types']); | ||||
|         } | ||||
|  | ||||
|         return $instance; | ||||
|     } | ||||
|  | ||||
| @@ -77,15 +80,15 @@ class SearchOptions | ||||
|     { | ||||
|         $terms = [ | ||||
|             'searches' => [], | ||||
|             'exacts' => [], | ||||
|             'tags' => [], | ||||
|             'filters' => [] | ||||
|             'exacts'   => [], | ||||
|             'tags'     => [], | ||||
|             'filters'  => [], | ||||
|         ]; | ||||
|  | ||||
|         $patterns = [ | ||||
|             'exacts' => '/"(.*?)"/', | ||||
|             'tags' => '/\[(.*?)\]/', | ||||
|             'filters' => '/\{(.*?)\}/' | ||||
|             'exacts'  => '/"(.*?)"/', | ||||
|             'tags'    => '/\[(.*?)\]/', | ||||
|             'filters' => '/\{(.*?)\}/', | ||||
|         ]; | ||||
|  | ||||
|         // Parse special terms | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Auth\Permissions\PermissionService; | ||||
| use BookStack\Auth\User; | ||||
| @@ -13,7 +15,6 @@ use Illuminate\Support\Str; | ||||
|  | ||||
| class SearchRunner | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * @var EntityProvider | ||||
|      */ | ||||
| @@ -29,14 +30,13 @@ class SearchRunner | ||||
|      */ | ||||
|     protected $permissionService; | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Acceptable operators to be used in a query | ||||
|      * Acceptable operators to be used in a query. | ||||
|      * | ||||
|      * @var array | ||||
|      */ | ||||
|     protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!=']; | ||||
|  | ||||
|  | ||||
|     public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService) | ||||
|     { | ||||
|         $this->entityProvider = $entityProvider; | ||||
| @@ -56,7 +56,7 @@ class SearchRunner | ||||
|  | ||||
|         if ($entityType !== 'all') { | ||||
|             $entityTypesToSearch = $entityType; | ||||
|         } else if (isset($searchOpts->filters['type'])) { | ||||
|         } elseif (isset($searchOpts->filters['type'])) { | ||||
|             $entityTypesToSearch = explode('|', $searchOpts->filters['type']); | ||||
|         } | ||||
|  | ||||
| @@ -78,16 +78,15 @@ class SearchRunner | ||||
|         } | ||||
|  | ||||
|         return [ | ||||
|             'total' => $total, | ||||
|             'count' => count($results), | ||||
|             'total'    => $total, | ||||
|             'count'    => count($results), | ||||
|             'has_more' => $hasMore, | ||||
|             'results' => $results->sortByDesc('score')->values(), | ||||
|             'results'  => $results->sortByDesc('score')->values(), | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Search a book for entities | ||||
|      * Search a book for entities. | ||||
|      */ | ||||
|     public function searchBook(int $bookId, string $searchString): Collection | ||||
|     { | ||||
| @@ -108,12 +107,13 @@ class SearchRunner | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Search a chapter for entities | ||||
|      * Search a chapter for entities. | ||||
|      */ | ||||
|     public function searchChapter(int $chapterId, string $searchString): Collection | ||||
|     { | ||||
|         $opts = SearchOptions::fromString($searchString); | ||||
|         $pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get(); | ||||
|  | ||||
|         return $pages->sortByDesc('score'); | ||||
|     } | ||||
|  | ||||
| @@ -121,6 +121,7 @@ class SearchRunner | ||||
|      * Search across a particular entity type. | ||||
|      * Setting getCount = true will return the total | ||||
|      * matching instead of the items themselves. | ||||
|      * | ||||
|      * @return \Illuminate\Database\Eloquent\Collection|int|static[] | ||||
|      */ | ||||
|     protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false) | ||||
| @@ -130,12 +131,13 @@ class SearchRunner | ||||
|             return $query->count(); | ||||
|         } | ||||
|  | ||||
|         $query = $query->skip(($page-1) * $count)->take($count); | ||||
|         $query = $query->skip(($page - 1) * $count)->take($count); | ||||
|  | ||||
|         return $query->get(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a search query for an entity | ||||
|      * Create a search query for an entity. | ||||
|      */ | ||||
|     protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder | ||||
|     { | ||||
| @@ -149,20 +151,20 @@ class SearchRunner | ||||
|             $subQuery->where('entity_type', '=', $entity->getMorphClass()); | ||||
|             $subQuery->where(function (Builder $query) use ($searchOpts) { | ||||
|                 foreach ($searchOpts->searches as $inputTerm) { | ||||
|                     $query->orWhere('term', 'like', $inputTerm .'%'); | ||||
|                     $query->orWhere('term', 'like', $inputTerm . '%'); | ||||
|                 } | ||||
|             })->groupBy('entity_type', 'entity_id'); | ||||
|             $entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) { | ||||
|                 $join->on('id', '=', 'entity_id'); | ||||
|             })->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc'); | ||||
|             })->selectRaw($entity->getTable() . '.*, s.score')->orderBy('score', 'desc'); | ||||
|             $entitySelect->mergeBindings($subQuery); | ||||
|         } | ||||
|  | ||||
|         // Handle exact term matching | ||||
|         foreach ($searchOpts->exacts as $inputTerm) { | ||||
|             $entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) { | ||||
|                 $query->where('name', 'like', '%'.$inputTerm .'%') | ||||
|                     ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%'); | ||||
|                 $query->where('name', 'like', '%' . $inputTerm . '%') | ||||
|                     ->orWhere($entity->textField, 'like', '%' . $inputTerm . '%'); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
| @@ -191,6 +193,7 @@ class SearchRunner | ||||
|         foreach ($this->queryOperators as $operator) { | ||||
|             $escapedOperators[] = preg_quote($operator); | ||||
|         } | ||||
|  | ||||
|         return join('|', $escapedOperators); | ||||
|     } | ||||
|  | ||||
| @@ -199,7 +202,7 @@ class SearchRunner | ||||
|      */ | ||||
|     protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder | ||||
|     { | ||||
|         preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit); | ||||
|         preg_match('/^(.*?)((' . $this->getRegexEscapedOperators() . ')(.*?))?$/', $tagTerm, $tagSplit); | ||||
|         $query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) { | ||||
|             $tagName = $tagSplit[1]; | ||||
|             $tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : ''; | ||||
| @@ -222,13 +225,13 @@ class SearchRunner | ||||
|                 $query->where('name', '=', $tagName); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return $query; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Custom entity search filters | ||||
|      * Custom entity search filters. | ||||
|      */ | ||||
|  | ||||
|     protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input) | ||||
|     { | ||||
|         try { | ||||
| @@ -298,7 +301,7 @@ class SearchRunner | ||||
|  | ||||
|     protected function filterInName(EloquentBuilder $query, Entity $model, $input) | ||||
|     { | ||||
|         $query->where('name', 'like', '%' .$input. '%'); | ||||
|         $query->where('name', 'like', '%' . $input . '%'); | ||||
|     } | ||||
|  | ||||
|     protected function filterInTitle(EloquentBuilder $query, Entity $model, $input) | ||||
| @@ -308,7 +311,7 @@ class SearchRunner | ||||
|  | ||||
|     protected function filterInBody(EloquentBuilder $query, Entity $model, $input) | ||||
|     { | ||||
|         $query->where($model->textField, 'like', '%' .$input. '%'); | ||||
|         $query->where($model->textField, 'like', '%' . $input . '%'); | ||||
|     } | ||||
|  | ||||
|     protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input) | ||||
| @@ -338,16 +341,14 @@ class SearchRunner | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Sorting filter options | ||||
|      * Sorting filter options. | ||||
|      */ | ||||
|  | ||||
|     protected function sortByLastCommented(EloquentBuilder $query, Entity $model) | ||||
|     { | ||||
|         $commentsTable = $this->db->getTablePrefix() . 'comments'; | ||||
|         $morphClass = str_replace('\\', '\\\\', $model->getMorphClass()); | ||||
|         $commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments'); | ||||
|         $commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM ' . $commentsTable . ' c1 LEFT JOIN ' . $commentsTable . ' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \'' . $morphClass . '\' AND c2.created_at IS NULL) as comments'); | ||||
|  | ||||
|         $query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc'); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\Bookshelf; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\EntityProvider; | ||||
| use BookStack\Entities\Models\Book; | ||||
| @@ -7,13 +9,12 @@ use Illuminate\Support\Collection; | ||||
|  | ||||
| class SiblingFetcher | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Search among the siblings of the entity of given type and id. | ||||
|      */ | ||||
|     public function fetch(string $entityType, int $entityId): Collection | ||||
|     { | ||||
|         $entity = (new EntityProvider)->get($entityType)->visible()->findOrFail($entityId); | ||||
|         $entity = (new EntityProvider())->get($entityType)->visible()->findOrFail($entityId); | ||||
|         $entities = []; | ||||
|  | ||||
|         // Page in chapter | ||||
| @@ -29,7 +30,7 @@ class SiblingFetcher | ||||
|         // Book | ||||
|         // Gets just the books in a shelf if shelf is in context | ||||
|         if ($entity->isA('book')) { | ||||
|             $contextShelf = (new ShelfContext)->getContextualShelfForBook($entity); | ||||
|             $contextShelf = (new ShelfContext())->getContextualShelfForBook($entity); | ||||
|             if ($contextShelf) { | ||||
|                 $entities = $contextShelf->visibleBooks()->get(); | ||||
|             } else { | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\Models\BookChild; | ||||
| use BookStack\Interfaces\Sluggable; | ||||
| @@ -6,7 +8,6 @@ use Illuminate\Support\Str; | ||||
|  | ||||
| class SlugGenerator | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Generate a fresh slug for the given entity. | ||||
|      * The slug will generated so it does not conflict within the same parent item. | ||||
| @@ -17,6 +18,7 @@ class SlugGenerator | ||||
|         while ($this->slugInUse($slug, $model)) { | ||||
|             $slug .= '-' . Str::random(3); | ||||
|         } | ||||
|  | ||||
|         return $slug; | ||||
|     } | ||||
|  | ||||
| @@ -26,9 +28,10 @@ class SlugGenerator | ||||
|     protected function formatNameAsSlug(string $name): string | ||||
|     { | ||||
|         $slug = Str::slug($name); | ||||
|         if ($slug === "") { | ||||
|         if ($slug === '') { | ||||
|             $slug = substr(md5(rand(1, 500)), 0, 5); | ||||
|         } | ||||
|  | ||||
|         return $slug; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| <?php namespace BookStack\Entities\Tools; | ||||
| <?php | ||||
|  | ||||
| namespace BookStack\Entities\Tools; | ||||
|  | ||||
| use BookStack\Entities\EntityProvider; | ||||
| use BookStack\Entities\Models\Book; | ||||
| use BookStack\Entities\Models\Bookshelf; | ||||
| use BookStack\Entities\Models\Chapter; | ||||
| use BookStack\Entities\Models\Deletion; | ||||
| use BookStack\Entities\Models\Entity; | ||||
| use BookStack\Entities\EntityProvider; | ||||
| use BookStack\Entities\Models\HasCoverImage; | ||||
| use BookStack\Entities\Models\Page; | ||||
| use BookStack\Exceptions\NotifyException; | ||||
| @@ -17,7 +19,6 @@ use Illuminate\Support\Carbon; | ||||
|  | ||||
| class TrashCan | ||||
| { | ||||
|  | ||||
|     /** | ||||
|      * Send a shelf to the recycle bin. | ||||
|      */ | ||||
| @@ -29,6 +30,7 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Send a book to the recycle bin. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function softDestroyBook(Book $book) | ||||
| @@ -48,6 +50,7 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Send a chapter to the recycle bin. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true) | ||||
| @@ -67,6 +70,7 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Send a page to the recycle bin. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function softDestroyPage(Page $page, bool $recordDelete = true) | ||||
| @@ -89,18 +93,21 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Remove a bookshelf from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function destroyShelf(Bookshelf $shelf): int | ||||
|     { | ||||
|         $this->destroyCommonRelations($shelf); | ||||
|         $shelf->forceDelete(); | ||||
|  | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a book from the system. | ||||
|      * Destroys any child chapters and pages. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function destroyBook(Book $book): int | ||||
| @@ -120,12 +127,14 @@ class TrashCan | ||||
|  | ||||
|         $this->destroyCommonRelations($book); | ||||
|         $book->forceDelete(); | ||||
|  | ||||
|         return $count + 1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a chapter from the system. | ||||
|      * Destroys all pages within. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function destroyChapter(Chapter $chapter): int | ||||
| @@ -141,11 +150,13 @@ class TrashCan | ||||
|  | ||||
|         $this->destroyCommonRelations($chapter); | ||||
|         $chapter->forceDelete(); | ||||
|  | ||||
|         return $count + 1; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a page from the system. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function destroyPage(Page $page): int | ||||
| @@ -160,6 +171,7 @@ class TrashCan | ||||
|         } | ||||
|  | ||||
|         $page->forceDelete(); | ||||
|  | ||||
|         return 1; | ||||
|     } | ||||
|  | ||||
| @@ -172,7 +184,7 @@ class TrashCan | ||||
|         $counts = []; | ||||
|  | ||||
|         /** @var Entity $instance */ | ||||
|         foreach ((new EntityProvider)->all() as $key => $instance) { | ||||
|         foreach ((new EntityProvider())->all() as $key => $instance) { | ||||
|             $counts[$key] = $instance->newQuery()->onlyTrashed()->count(); | ||||
|         } | ||||
|  | ||||
| @@ -181,6 +193,7 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Destroy all items that have pending deletions. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function empty(): int | ||||
| @@ -190,11 +203,13 @@ class TrashCan | ||||
|         foreach ($deletions as $deletion) { | ||||
|             $deleteCount += $this->destroyFromDeletion($deletion); | ||||
|         } | ||||
|  | ||||
|         return $deleteCount; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Destroy an element from the given deletion model. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function destroyFromDeletion(Deletion $deletion): int | ||||
| @@ -207,11 +222,13 @@ class TrashCan | ||||
|             $count = $this->destroyEntity($deletion->deletable); | ||||
|         } | ||||
|         $deletion->delete(); | ||||
|  | ||||
|         return $count; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restore the content within the given deletion. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function restoreFromDeletion(Deletion $deletion): int | ||||
| @@ -229,6 +246,7 @@ class TrashCan | ||||
|         } | ||||
|  | ||||
|         $deletion->delete(); | ||||
|  | ||||
|         return $restoreCount; | ||||
|     } | ||||
|  | ||||
| @@ -236,6 +254,7 @@ class TrashCan | ||||
|      * Automatically clear old content from the recycle bin | ||||
|      * depending on the configured lifetime. | ||||
|      * Returns the total number of deleted elements. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     public function autoClearOld(): int | ||||
| @@ -287,6 +306,7 @@ class TrashCan | ||||
|  | ||||
|     /** | ||||
|      * Destroy the given entity. | ||||
|      * | ||||
|      * @throws Exception | ||||
|      */ | ||||
|     protected function destroyEntity(Entity $entity): int | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user