/**
* @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.
*/
if ($this->entity_type === '') {
$this->entity_type = null;
}
+
return $this->morphTo('entity');
}
public function isForEntity(): bool
{
return Str::startsWith($this->type, [
- 'page_', 'chapter_', 'book_', 'bookshelf_'
+ 'page_', 'chapter_', 'book_', 'bookshelf_',
]);
}
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
/**
* Add a generic activity event to the database.
+ *
* @param string|Loggable $detail
*/
public function add(string $type, $detail = '')
{
return $this->activity->newInstance()->forceFill([
'type' => strtolower($type),
- 'user_id' => user()->id,
+ 'user_id' => user()->id,
]);
}
{
$entity->activity()->update([
'detail' => $entity->name,
- 'entity_id' => null,
- 'entity_type' => null,
+ 'entity_id' => null,
+ 'entity_type' => null,
]);
}
$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();
/**
* Filters out similar activity.
+ *
* @param Activity[] $activities
+ *
* @return array
*/
protected function filterSimilar(iterable $activities): array
return;
}
- $message = str_replace("%u", $username, $message);
+ $message = str_replace('%u', $username, $message);
$channel = config('logging.failed_login.channel');
Log::channel($channel)->warning($message);
}
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
class ActivityType
{
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
protected $appends = ['created', 'updated'];
/**
- * Get the entity that this comment belongs to
+ * Get the entity that this comment belongs to.
*/
public function entity(): MorphTo
{
/**
* Get created date as a relative diff.
+ *
* @return mixed
*/
public function getCreatedAttribute()
/**
* Get updated date as a relative diff.
+ *
* @return mixed
*/
public function getUpdatedAttribute()
-<?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;
$entity->comments()->save($comment);
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
+
return $comment;
}
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
$comment->save();
+
return $comment;
}
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,
]);
protected function getNextLocalId(Entity $entity): int
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
+
return ($comments->local_id ?? 0) + 1;
}
}
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
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
{
*/
public function nameUrl(): string
{
- return url('/search?term=%5B' . urlencode($this->name) .'%5D');
+ return url('/search?term=%5B' . urlencode($this->name) . '%5D');
}
/**
*/
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');
}
}
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Entity;
class TagRepo
{
-
protected $tag;
protected $permissionService;
}
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
+
return $query->get(['name'])->pluck('name');
}
}
$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
{
{
$name = trim($input['name']);
$value = isset($input['value']) ? trim($input['value']) : '';
+
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
}
}
-<?php namespace BookStack\Actions;
+<?php
+
+namespace BookStack\Actions;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
*/
class View extends Model
{
-
protected $fillable = ['user_id', 'views'];
/**
-<?php namespace BookStack\Api;
+<?php
+
+namespace BookStack\Api;
use BookStack\Http\Controllers\Api\ApiController;
use Illuminate\Contracts\Container\BindingResolutionException;
class ApiDocsGenerator
{
-
protected $reflectionClasses = [];
protected $controllerClasses = [];
$docs = (new static())->generate();
Cache::put($cacheKey, $docs, 60 * 24);
}
+
return $docs;
}
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
$apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
$apiRoutes = $apiRoutes->groupBy('base_model');
+
return $apiRoutes;
}
$exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
$route["example_{$exampleType}"] = $exampleContent;
}
+
return $route;
});
}
$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
foreach ($rules as $param => $ruleString) {
$rules[$param] = explode('|', $ruleString);
}
+
return count($rules) > 0 ? $rules : null;
}
{
$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
[$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,
];
});
}
-<?php namespace BookStack\Api;
+<?php
+
+namespace BookStack\Api;
use BookStack\Auth\User;
use BookStack\Interfaces\Loggable;
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',
];
/**
class ApiTokenGuard implements Guard
{
-
use GuardHelpers;
/**
*/
protected $request;
-
/**
* The last auth exception thrown in this request.
+ *
* @var ApiAuthException
*/
protected $lastAuthException;
{
$this->request = $request;
}
-
+
/**
* @inheritDoc
*/
}
$user = null;
+
try {
$user = $this->getAuthorisedUserFromRequest();
} catch (ApiAuthException $exception) {
}
$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;
}
/**
* Check the API token in the request and fetch a valid authorised user.
+ *
* @throws ApiAuthException
*/
protected function getAuthorisedUserFromRequest(): Authenticatable
/**
* Validate the format of the token header value string.
+ *
* @throws ApiAuthException
*/
protected function validateTokenHeaderValue(string $authToken): void
/**
* 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
-<?php namespace BookStack\Api;
+<?php
+
+namespace BookStack\Api;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
class ListingResponseBuilder
{
-
protected $query;
protected $request;
protected $fields;
'lt' => '<',
'gte' => '>=',
'lte' => '<=',
- 'like' => 'like'
+ 'like' => 'like',
];
/**
$data = $this->fetchData($filteredQuery);
return response()->json([
- 'data' => $data,
+ 'data' => $data,
'total' => $total,
]);
}
{
$query = $this->countAndOffsetQuery($query);
$query = $this->sortQuery($query);
+
return $query->get($this->fields);
}
}
$queryOperator = $this->filterOperators[$filterOperator];
+
return [$field, $queryOperator, $value];
}
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 = '')
. 'app'
. DIRECTORY_SEPARATOR
. 'Config'
- . ($path ? DIRECTORY_SEPARATOR.$path : $path);
+ . ($path ? DIRECTORY_SEPARATOR . $path : $path);
}
}
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\ConfirmationEmailException;
/**
* Create new confirmation for a user,
* Also removes any existing old ones.
+ *
* @param User $user
+ *
* @throws ConfirmationEmailException
*/
public function sendConfirmation(User $user)
/**
* 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');
-<?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
{
}
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
+
return in_array($roleName, $groupNames);
}
}
/**
- * 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
{
class ExternalBaseUserProvider implements UserProvider
{
-
/**
* The user model.
*
/**
* LdapUserProvider constructor.
- * @param $model
+ *
+ * @param $model
*/
public function __construct(string $model)
{
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)
/**
* 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)
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)
/**
* 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();
/**
* 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)
// 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;
}
// 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);
}
/**
* Log a user into the application without sessions or cookies.
*
- * @param array $credentials
+ * @param array $credentials
+ *
* @return bool
*/
public function once(array $credentials = [])
/**
* 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;
/**
* Validate a user's credentials.
*
- * @param array $credentials
+ * @param array $credentials
+ *
* @return bool
*/
public function validate(array $credentials = [])
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)
/**
* 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;
/**
* 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)
/**
* Update the session with the given ID.
*
- * @param string $id
+ * @param string $id
+ *
* @return void
*/
protected function updateSession($id)
*/
public function getName()
{
- return 'login_'.$this->name.'_'.sha1(static::class);
+ return 'login_' . $this->name . '_' . sha1(static::class);
}
/**
/**
* Set the current user.
*
- * @param \Illuminate\Contracts\Auth\Authenticatable $user
+ * @param \Illuminate\Contracts\Auth\Authenticatable $user
+ *
* @return $this
*/
public function setUser(AuthenticatableContract $user)
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;
class LdapSessionGuard extends ExternalBaseSessionGuard
{
-
protected $ldapService;
/**
* Validate a user's credentials.
*
* @param array $credentials
- * @return bool
+ *
* @throws LdapException
+ *
+ * @return bool
*/
public function validate(array $credentials = [])
{
if (isset($userDetails['uid'])) {
$this->lastAttempted = $this->provider->retrieveByCredentials([
- 'external_auth_id' => $userDetails['uid']
+ 'external_auth_id' => $userDetails['uid'],
]);
}
* 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)
{
$user = null;
if (isset($userDetails['uid'])) {
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
- 'external_auth_id' => $userDetails['uid']
+ 'external_auth_id' => $userDetails['uid'],
]);
}
}
$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
}
$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;
}
}
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
* Validate a user's credentials.
*
* @param array $credentials
+ *
* @return bool
*/
public function validate(array $credentials = [])
* 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)
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
/**
* Class Ldap
*/
class Ldap
{
-
/**
* Connect to a LDAP server.
+ *
* @param string $hostName
* @param int $port
+ *
* @return resource
*/
public function connect($hostName, $port)
/**
* 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)
/**
* Set the version number for the given ldap connection.
+ *
* @param $ldapConnection
* @param $version
+ *
* @return bool
*/
public function setVersion($ldapConnection, $version)
/**
* 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)
/**
* Get entries from an ldap search result.
+ *
* @param resource $ldapConnection
* @param resource $ldapSearchResult
+ *
* @return array
*/
public function getEntries($ldapConnection, $ldapSearchResult)
/**
* 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)
/**
* 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)
/**
* 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);
}
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
*/
class LdapService extends ExternalAuthService
{
-
protected $ldap;
protected $ldapConnection;
protected $userAvatars;
/**
* Check if groups should be synced.
+ *
* @return bool
*/
public function shouldSyncGroups()
/**
* Search for attributes for a specific user on the ldap.
+ *
* @throws LdapException
*/
private function getUserWithAttributes(string $userName, array $attributes): ?array
/**
* 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
$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,
]);
}
/**
* Check if the given credentials are valid for the given user.
+ *
* @throws LdapException
*/
public function validateUserCredentials(?array $ldapUserDetails, string $password): bool
}
$ldapConnection = $this->getConnection();
+
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);
} catch (ErrorException $e) {
/**
* 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)
/**
* 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()
{
}
$this->ldapConnection = $ldapConnection;
+
return $this->ldapConnection;
}
// Otherwise, extract the port out
$hostName = $serverNameParts[0];
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
+
return ['host' => $hostName, 'port' => $ldapPort];
}
$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
$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
/**
* Get the parent groups of a single group.
+ *
* @throws LdapException
*/
private function getGroupGroups(string $groupName): array
$count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
- $count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
+ $count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
}
for ($i = 0; $i < $count; $i++) {
/**
* Sync the LDAP groups to the user roles for the current user.
+ *
* @throws LdapException
*/
public function syncGroups(User $user, string $username)
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
class RegistrationService
{
-
protected $userRepo;
protected $emailConfirmationService;
/**
* Check whether or not registrations are allowed in the app settings.
+ *
* @throws UserRegistrationException
*/
public function ensureRegistrationAllowed()
{
$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
session()->flash('sent-email-confirmation', true);
} catch (Exception $e) {
$message = trans('auth.email_confirm_send_error');
+
throw new UserRegistrationException($message, '/register/confirm');
}
}
/**
* 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
}
$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);
}
}
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
/**
* 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
* 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
if (!empty($errors)) {
throw new Error(
- 'Invalid ACS Response: '.implode(', ', $errors)
+ 'Invalid ACS Response: ' . implode(', ', $errors)
);
}
/**
* Process a response for the single logout service.
+ *
* @throws Error
*/
public function processSlsResponse(?string $requestId): ?string
if (!empty($errors)) {
throw new Error(
- 'Invalid SLS Response: '.implode(', ', $errors)
+ 'Invalid SLS Response: ' . implode(', ', $errors)
);
}
$this->actionLogout();
+
return $redirect;
}
/**
* Get the metadata for this service provider.
+ *
* @throws Error
*/
public function metadata(): string
if (!empty($errors)) {
throw new Error(
- 'Invalid SP metadata: '.implode(', ', $errors),
+ 'Invalid SP metadata: ' . implode(', ', $errors),
Error::METADATA_SP_INVALID
);
}
/**
* Load the underlying Onelogin SAML2 toolkit.
+ *
* @throws Error
* @throws Exception
*/
$spSettings = $this->loadOneloginServiceProviderDetails();
$settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides);
+
return new Auth($settings);
}
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,
];
}
}
/**
- * Calculate the display name
+ * Calculate the display name.
*/
protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string
{
return [
'external_id' => $externalId,
- 'name' => $this->getUserDisplayName($samlAttributes, $externalId),
- 'email' => $email,
- 'saml_id' => $samlID,
+ 'name' => $this->getUserDisplayName($samlAttributes, $externalId),
+ 'email' => $email,
+ 'saml_id' => $samlID,
];
}
$data = $data[0];
break;
}
+
return $data;
}
/**
* Get the user from the database for the specified details.
+ *
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User
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'],
];
/**
* Process the SAML response for a user. Login the user when
* they exist, optionally registering them automatically.
+ *
* @throws SamlException
* @throws JsonDebugException
* @throws UserRegistrationException
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,
]);
}
auth()->login($user);
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
+
return $user;
}
}
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
{
/**
* The core socialite library used.
+ *
* @var Socialite
*/
protected $socialite;
/**
* The default built-in social drivers we support.
+ *
* @var string[]
*/
protected $validSocialDrivers = [
'okta',
'gitlab',
'twitch',
- 'discord'
+ 'discord',
];
/**
* 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 = [];
/**
* 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
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
$email = $socialUser->getEmail();
+
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
}
/**
* 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)
auth()->login($socialAccount->user);
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
+
return redirect()->intended('/');
}
$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());
}
/**
* Ensure the social driver is correct and supported.
+ *
* @throws SocialDriverNotConfigured
*/
protected function validateDriver(string $socialDriver): string
$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);
}
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(),
]);
}
}
/**
- * 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
{
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Notifications\UserInvite;
/**
* Send an invitation to a user to sign into BookStack
* Removes existing invitation tokens.
+ *
* @param User $user
*/
public function sendInvitation(User $user)
-<?php namespace BookStack\Auth\Access;
+<?php
+
+namespace BookStack\Auth\Access;
use BookStack\Auth\User;
use BookStack\Exceptions\UserTokenExpiredException;
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;
/**
* UserTokenService constructor.
+ *
* @param Database $db
*/
public function __construct(Database $db)
/**
* Delete all email confirmations that belong to a user.
+ *
* @param User $user
+ *
* @return mixed
*/
public function deleteByUser(User $user)
/**
* 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);
/**
* 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();
/**
* Get a token entry for the given token.
+ *
* @param string $token
+ *
* @return object|null
*/
protected function getEntryByToken(string $token)
/**
* 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));
-<?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()
-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Entity;
-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
}
/**
- * Set the database connection
+ * Set the database connection.
*/
public function setConnection(Connection $connection)
{
}
/**
- * 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 = [])
}
/**
- * Get a book via ID, Checks local cache
+ * Get a book via ID, Checks local cache.
*/
protected function getBook(int $bookId): ?Book
{
}
/**
- * Get a chapter via ID, Checks local cache
+ * Get a chapter via ID, Checks local cache.
*/
protected function getChapter(int $chapterId): ?Chapter
{
},
'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)
/**
* Build joint permissions for the given book and role combinations.
+ *
* @throws Throwable
*/
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
/**
* Rebuild the entity jointPermissions for a particular entity.
+ *
* @throws Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
+
return;
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
+ *
* @throws Throwable
*/
public function buildJointPermissionsForEntities(array $entities)
/**
* Delete all of the entity jointPermissions for a list of entities.
+ *
* @param Role[] $roles
*/
protected function deleteManyJointPermissionsForRoles($roles)
/**
* Delete the entity jointPermissions for a particular entity.
+ *
* @param Entity $entity
+ *
* @throws Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
/**
* Delete all of the entity jointPermissions for a list of entities.
+ *
* @param Entity[] $entities
+ *
* @throws Throwable
*/
protected function deleteManyJointPermissionsForEntities(array $entities)
}
$this->db->transaction(function () use ($entities) {
-
foreach (array_chunk($entities, 1000) as $entityChunk) {
$query = $this->db->table('joint_permissions');
foreach ($entityChunk as $entity) {
/**
* 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)
});
}
-
/**
* Get the actions related to an entity.
*/
if ($entity instanceof Book) {
$baseActions[] = 'chapter-create';
}
+
return $baseActions;
}
if ($entity->restricted) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
+
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
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;
}
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
$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
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
$this->clean();
+
return $hasAccess;
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
+
return $hasPermission;
}
});
$this->clean();
+
return $q;
}
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())
/**
* 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')
});
$this->clean();
+
return $q;
}
});
$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)
}
/**
- * Get the current user
+ * Get the current user.
*/
private function currentUser(): User
{
-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Role;
class PermissionsRepo
{
-
protected $permission;
protected $role;
protected $permissionService;
$this->assignRolePermissions($role, $permissions);
$this->permissionService->buildJointPermissionForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
+
return $role;
}
* 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
*/
// 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'));
}
-<?php namespace BookStack\Auth\Permissions;
+<?php
+
+namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Model;
/**
* Get the permission object by name.
+ *
* @param $name
+ *
* @return mixed
*/
public static function getByName($name)
-<?php namespace BookStack\Auth;
+<?php
+
+namespace BookStack\Auth;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
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
*/
class Role extends Model implements Loggable
{
-
protected $fillable = ['display_name', 'description', 'external_auth_id'];
/**
return true;
}
}
+
return false;
}
-<?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()
-<?php namespace BookStack\Auth;
+<?php
+
+namespace BookStack\Auth;
use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken;
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'];
/**
* The attributes excluded from the model's JSON form.
+ *
* @var array
*/
protected $hidden = [
/**
* 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;
if (!is_null(static::$defaultUser)) {
return static::$defaultUser;
}
-
+
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
+
return static::$defaultUser;
}
/**
* The roles that belong to the user.
+ *
* @return BelongsToMany
*/
public function roles()
{
if ($this->id === 0) {
- return ;
+ return;
}
+
return $this->belongsToMany(Role::class);
}
/**
* 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)
}
/**
- * Returns a URL to the user's avatar
+ * Returns a URL to the user's avatar.
*/
public function getAvatar(int $size = 50): string
{
} catch (Exception $err) {
$avatar = $default;
}
+
return $avatar;
}
public function getEditUrl(string $path = ''): string
{
$uri = '/settings/users/' . $this->id . '/' . trim($path, '/');
+
return url(rtrim($uri, '/'));
}
/**
* Send the password reset notification.
- * @param string $token
+ *
+ * @param string $token
+ *
* @return void
*/
public function sendPasswordResetNotification($token)
public function refreshSlug(): string
{
$this->slug = app(SlugGenerator::class)->generate($this);
+
return $this->slug;
}
}
-<?php namespace BookStack\Auth;
+<?php
+
+namespace BookStack\Auth;
use Activity;
use BookStack\Entities\EntityProvider;
return $query->paginate($count);
}
- /**
+ /**
* Creates a new user and attaches a role to them.
*/
public function registerNew(array $data, bool $emailConfirmed = false): User
/**
* Assign a user to a system-level role.
+ *
* @throws NotFoundException
*/
public function attachSystemRole(User $user, string $systemRoleName)
/**
* Set the assigned user roles via an array of role IDs.
+ *
* @throws UserUpdateException
*/
public function setUserRoles(User $user, array $roles)
* 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');
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'] ?? '',
];
/**
* Remove the given user from storage, Delete all related content.
+ *
* @throws Exception
*/
public function destroy(User $user, ?int $newOwnerId = null)
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->delete();
-
+
// Delete user profile images
$this->userAvatar->destroyAllForUser($user);
*/
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]);
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(),
];
}
'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),
];
'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',
'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
// 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',
],
// 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' => [
'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,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
- 'email' => 'emails.password',
- 'table' => 'password_resets',
- 'expire' => 60,
+ 'email' => 'emails.password',
+ 'table' => 'password_resets',
+ 'expire' => 60,
],
],
'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',
],
'driver' => 'null',
],
-
],
];
],
'database' => [
- 'driver' => 'database',
- 'table' => 'cache',
+ 'driver' => 'database',
+ 'table' => 'cache',
'connection' => null,
],
],
'redis' => [
- 'driver' => 'redis',
+ 'driver' => 'redis',
'connection' => 'default',
],
'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,
],
],
<?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.
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
'models' => true, // Display models
],
- // Configure some DataCollectors
+ // Configure some DataCollectors
'options' => [
'auth' => [
'show_name' => true, // Also show the users name/email in the debugbar
'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', ''),
];
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.
* 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.
* 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
* 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.
* 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
* @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
* 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.
*
* @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
*
* @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.
*
* @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.
*
* @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,
],
-
];
'local' => [
'driver' => 'local',
- 'root' => public_path(),
+ 'root' => public_path(),
],
'local_secure' => [
],
'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,
],
// 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,
],
];
// "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,
],
],
],
-
// Failed Login Message
// Allows a configurable message to be logged when a login request fails.
'failed_login' => [
// Global "From" address & name
'from' => [
- 'name' => env('MAIL_FROM_NAME', 'BookStack')
+ 'name' => env('MAIL_FROM_NAME', 'BookStack'),
],
// Email encryption protocol
// 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,
],
],
// 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
'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
'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),
],
'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' => [
'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' => [
'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' => [
'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),
],
];
<?php
-use \Illuminate\Support\Str;
+use Illuminate\Support\Str;
/**
* Session configuration options.
// 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'),
],
];
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
'timeout' => false,
'options' => [
- 'outline' => true
+ 'outline' => true,
],
'env' => [],
],
*/
protected $description = 'Cleanup images and drawings';
-
protected $imageService;
/**
* Create a new command instance.
+ *
* @param \BookStack\Uploads\ImageService $imageService
*/
public function __construct(ImageService $imageService)
$this->comment($deleteCount . ' images found that would have been deleted');
$this->showDeletedImages($deleted);
$this->comment('Run with -f or --force to perform deletions');
+
return;
}
/**
* Create a new command instance.
- *
*/
public function __construct()
{
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?'
);
/**
* Execute the console command.
*
- * @return mixed
* @throws \BookStack\Exceptions\NotFoundException
+ *
+ * @return mixed
*/
public function handle()
{
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);
class DeleteUsers extends Command
{
-
/**
* The name and signature of the console command.
*
continue;
}
$this->userRepo->destroy($user);
- ++$numDeleted;
+ $numDeleted++;
}
$this->info("Deleted $numDeleted of $totalUsers total users.");
} else {
use Illuminate\Console\Command;
use Illuminate\Database\Connection;
-use Illuminate\Support\Facades\DB;
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;
}
}
$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) {
}
$jsonColumnsToUpdateByTable = [
- "settings" => ["value"],
+ 'settings' => ['value'],
];
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
}
}
- $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;
}
{
$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})"),
]);
}
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);
}
/**
* Create a new command instance.
- *
*/
public function __construct()
{
$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);
-<?php namespace BookStack\Console;
+<?php
+
+namespace BookStack\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as 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)
*/
protected function commands()
{
- $this->load(__DIR__.'/Commands');
+ $this->load(__DIR__ . '/Commands');
}
}
-<?php namespace BookStack\Entities;
+<?php
+
+namespace BookStack\Entities;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\ShelfContext;
class BreadcrumbsViewComposer
{
-
protected $entityContextManager;
/**
* BreadcrumbsViewComposer constructor.
+ *
* @param ShelfContext $entityContextManager
*/
public function __construct(ShelfContext $entityContextManager)
/**
* Modify data when the view is composed.
+ *
* @param View $view
*/
public function compose(View $view)
-<?php namespace BookStack\Entities;
+<?php
+
+namespace BookStack\Entities;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
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
*/
class EntityProvider
{
-
/**
* @var Bookshelf
*/
*/
public $pageRevision;
-
public function __construct()
{
$this->bookshelf = new Bookshelf();
/**
* 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,
];
}
public function get(string $type): Entity
{
$type = strtolower($type);
+
return $this->all()[$type];
}
$model = $this->get($type);
$morphClasses[] = $model->getMorphClass();
}
+
return $morphClasses;
}
}
-<?php namespace BookStack\Entities\Models;
+<?php
+
+namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Exception;
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
/**
* 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)
} 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
{
/**
* Get all pages within this book.
+ *
* @return HasMany
*/
public function pages()
/**
* Get the direct child pages of this book.
+ *
* @return HasMany
*/
public function directPages()
/**
* Get all chapters within this book.
+ *
* @return HasMany
*/
public function chapters()
/**
* Get the shelves this book is contained within.
+ *
* @return BelongsToMany
*/
public function shelves()
/**
* 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');
}
}
-<?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.
-<?php namespace BookStack\Entities\Models;
+<?php
+
+namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* 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()
/**
* 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)
} 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
{
/**
* Check if this shelf contains the given book.
+ *
* @param Book $book
+ *
* @return bool
*/
public function contains(Book $book): bool
/**
* Add a book to the end of this shelf.
+ *
* @param Book $book
*/
public function appendBook(Book $book)
-<?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
*/
/**
* Get the pages that this chapter contains.
+ *
* @param string $dir
+ *
* @return mixed
*/
public function pages($dir = 'ASC')
-<?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;
*/
class Deletion extends Model implements Loggable
{
-
/**
* Get the related deletable record.
*/
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}";
}
-<?php namespace BookStack\Entities\Models;
+<?php
+
+namespace BookStack\Entities\Models;
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
* 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()
}
/**
- * 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;
}
/**
* 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
{
public static function getType(): string
{
$className = array_slice(explode('\\', static::class), -1, 1)[0];
+
return strtolower($className);
}
if (mb_strlen($this->name) <= $length) {
return $this->name;
}
+
return mb_substr($this->name, 0, $length - 3) . '...';
}
$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;
if ($this instanceof Chapter) {
return $this->book()->withTrashed()->first();
}
+
return null;
}
}
/**
- * Index the current entity for search
+ * Index the current entity for search.
*/
public function indexForSearch()
{
public function refreshSlug(): string
{
$this->slug = app(SlugGenerator::class)->generate($this);
+
return $this->slug;
}
<?php
-
namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
interface HasCoverImage
{
-
/**
* Get the cover image for this item.
*/
-<?php namespace BookStack\Entities\Models;
+<?php
+
+namespace BookStack\Entities\Models;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\Attachment;
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
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
protected $casts = [
- 'draft' => 'boolean',
+ 'draft' => 'boolean',
'template' => 'boolean',
];
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()
/**
* Check if this page has a chapter.
+ *
* @return bool
*/
public function hasChapter()
/**
* Get the attachments assigned to this page.
+ *
* @return HasMany
*/
public function attachments()
}
/**
- * Get the current revision for the page if existing
+ * Get the current revision for the page if existing.
+ *
* @return PageRevision|null
*/
public function getCurrentRevision()
$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;
}
}
-<?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()
/**
* Get the page this revision originates from.
+ *
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
/**
* Get the url for this revision.
+ *
* @param null|string $path
+ *
* @return string
*/
public function getUrl($path = null)
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()
/**
* 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)
-<?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()
-<?php namespace BookStack\Entities\Queries;
+<?php
+
+namespace BookStack\Entities\Queries;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\EntityProvider;
{
return app()->make(EntityProvider::class);
}
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Entities\Queries;
+<?php
+namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Facades\DB;
->pluck('viewable')
->filter();
}
-
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Entities\Queries;
+<?php
+
+namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Collection;
-<?php namespace BookStack\Entities\Queries;
+<?php
+
+namespace BookStack\Entities\Queries;
use BookStack\Actions\Favourite;
use Illuminate\Database\Query\JoinClause;
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;
}
/**
- * Create a new entity in the system
+ * Create a new entity in the system.
*/
public function create(Entity $entity, array $input)
{
$entity->forceFill([
'created_by' => user()->id,
'updated_by' => user()->id,
- 'owned_by' => user()->id,
+ 'owned_by' => user()->id,
]);
$entity->refreshSlug();
$entity->save();
/**
* Update the given items' cover image, or clear it.
+ *
* @throws ImageUploadException
* @throws \Exception
*/
-<?php namespace BookStack\Entities\Repos;
+<?php
+
+namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
class BookRepo
{
-
protected $baseRepo;
protected $tagRepo;
protected $imageRepo;
}
/**
- * 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;
}
{
$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
*/
/**
* Remove a book from the system.
+ *
* @throws Exception
*/
public function destroy(Book $book)
-<?php namespace BookStack\Entities\Repos;
+<?php
+
+namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
$this->baseRepo->create($shelf, $input);
$this->updateBooks($shelf, $bookIds);
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
+
return $shelf;
}
}
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
+
return $shelf;
}
/**
* Update the given shelf cover image, or clear it.
+ *
* @throws ImageUploadException
* @throws Exception
*/
/**
* Remove a bookshelf from the system.
+ *
* @throws Exception
*/
public function destroy(Bookshelf $shelf)
-<?php namespace BookStack\Entities\Repos;
+<?php
+
+namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use Exception;
-use Illuminate\Support\Collection;
class ChapterRepo
{
-
protected $baseRepo;
/**
/**
* Get a chapter via the slug.
+ *
* @throws NotFoundException
*/
public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
+
return $chapter;
}
{
$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)
/**
* 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
-<?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;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
-use Illuminate\Support\Collection;
class PageRepo
{
-
protected $baseRepo;
/**
/**
* Get a page by ID.
+ *
* @throws NotFoundException
*/
public function getById(int $id, array $relations = ['book']): Page
/**
* Get a page its book and own slug.
+ *
* @throws NotFoundException
*/
public function getBySlug(string $bookSlug, string $pageSlug): Page
->orderBy('created_at', 'desc')
->with('page')
->first();
+
return $revision ? $revision->page : null;
}
public function getUserDraft(Page $page): ?PageRevision
{
$revision = $this->getUserDraftQuery($page)->first();
+
return $revision;
}
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) {
$page->save();
$page->refresh()->rebuildPermissions();
+
return $page;
}
$draft->refresh();
Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
+
return $draft;
}
$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;
}
Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
+
return $page;
}
$revision->save();
$this->deleteOldRevisions($page);
+
return $revision;
}
}
$page->fill($input);
$page->save();
+
return $page;
}
}
$draft->save();
+
return $draft;
}
/**
* Destroy a page from the system.
+ *
* @throws Exception
*/
public function destroy(Page $page)
} else {
$content->setNewHTML($revision->html);
}
-
+
$page->updated_by = user()->id;
$page->refreshSlug();
$page->save();
$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
*/
$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
*/
/**
* 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
}
$parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+
return $parentClass::visible()->where('id', '=', $entityId)->first();
}
$draft->book_slug = $page->book->slug;
$draft->created_by = user()->id;
$draft->type = 'update_draft';
+
return $draft;
}
}
/**
- * 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;
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
class BookContents
{
-
/**
* @var Book
*/
->where('chapter_id', '=', 0)->max('priority');
$maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
->max('priority');
+
return max($maxChapter, $maxPage, 1);
}
if (isset($entity['draft']) && $entity['draft']) {
return -100;
}
+
return $entity['priority'] ?? 0;
};
}
* +"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
/**
* 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
$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;
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
class ExportFormatter
{
-
protected $imageService;
/**
/**
* 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)
});
$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)
$html = view('chapters.export', [
'chapter' => $chapter,
- 'pages' => $pages,
- 'format' => 'pdf',
+ 'pages' => $pages,
+ 'format' => 'pdf',
])->render();
return $this->htmlToPdf($html);
/**
* 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
} 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
$text = html_entity_decode($text);
// Add title
$text = $page->name . "\n\n" . $text;
+
return $text;
}
foreach ($chapter->getVisiblePages() as $page) {
$text .= $this->pageToPlainText($page);
}
+
return $text;
}
$text .= $this->pageToPlainText($bookChild);
}
}
+
return $text;
}
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();
}
/**
*/
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;
}
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);
$text .= $this->pageToMarkdown($bookChild);
}
}
+
return $text;
}
}
-<?php namespace BookStack\Entities\Tools\Markdown;
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\ParagraphConverter;
use League\HTMLToMarkdown\ElementInterface;
-<?php namespace BookStack\Entities\Tools\Markdown;
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
class CustomStrikeThroughExtension implements ExtensionInterface
{
-
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
-<?php namespace BookStack\Entities\Tools\Markdown;
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
-<?php namespace BookStack\Entities\Tools\Markdown;
+<?php
+
+namespace BookStack\Entities\Tools\Markdown;
use League\HTMLToMarkdown\Converter\BlockquoteConverter;
use League\HTMLToMarkdown\Converter\CodeConverter;
}
/**
- * Run the conversion
+ * Run the conversion.
*/
public function convert(): string
{
$converter = new HtmlConverter($this->getConverterEnvironment());
$html = $this->prepareHtml($this->html);
+
return $converter->convert($html);
}
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());
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
return get_class($entity) === get_class($this->relativeBookItem)
&& $entity->id === $this->relativeBookItem->id;
});
+
return $index === false ? null : $index;
}
$childPages = $item->visible_pages ?? [];
$flatOrdered = $flatOrdered->concat($childPages);
}
+
return $flatOrdered;
}
}
-<?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;
class PageContent
{
-
protected $page;
/**
$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
{
// 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);
/**
* 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
{
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
+
return [$existingId, $existingId];
}
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
+
return [$existingId, $newId];
}
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
{
}
/**
- * 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
{
$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) : [];
}
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;
$levelChange = ($tree->pluck('level')->min() - 1);
$tree = $tree->map(function ($header) use ($levelChange) {
$header['level'] -= ($levelChange);
+
return $header;
});
return $html;
}
-
/**
* Fetch the content from a specific section of the given page.
*/
$doc = new DOMDocument();
$html = '<body>' . $html . '</body>';
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+
return $doc;
}
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
class PageEditActivity
{
-
protected $page;
/**
/**
* Check if there's active editing being performed on this page.
+ *
* @return bool
*/
public function hasActiveEditing(): bool
$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
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
}
+
return $message . "\n" . trans('entities.pages_draft_edited_notification');
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
class PermissionsUpdater
{
-
/**
* Update an entities permissions from a permission form submit request.
*/
return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
return [
'role_id' => $roleId,
- 'action' => strtolower($action),
- ] ;
+ 'action' => strtolower($action),
+ ];
});
});
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
*/
protected $entityProvider;
-
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider)
{
$this->searchTerm = $searchTerm;
$this->entityProvider = $entityProvider;
}
-
/**
* Index the given entity.
*/
}
/**
- * Index multiple Entities at once
+ * Index multiple Entities at once.
+ *
* @param Entity[] $entities
*/
protected function indexEntities(array $entities)
$terms = [];
foreach ($tokenMap as $token => $count) {
$terms[] = [
- 'term' => $token,
- 'score' => $count * $scoreAdjustment
+ 'term' => $token,
+ 'score' => $count * $scoreAdjustment,
];
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use Illuminate\Http\Request;
class SearchOptions
{
-
/**
* @var array
*/
foreach ($decoded as $type => $value) {
$instance->$type = $value;
}
+
return $instance;
}
if (isset($inputs['types']) && count($inputs['types']) < 4) {
$instance->filters['type'] = implode('|', $inputs['types']);
}
+
return $instance;
}
{
$terms = [
'searches' => [],
- 'exacts' => [],
- 'tags' => [],
- 'filters' => []
+ 'exacts' => [],
+ 'tags' => [],
+ 'filters' => [],
];
$patterns = [
- 'exacts' => '/"(.*?)"/',
- 'tags' => '/\[(.*?)\]/',
- 'filters' => '/\{(.*?)\}/'
+ 'exacts' => '/"(.*?)"/',
+ 'tags' => '/\[(.*?)\]/',
+ 'filters' => '/\{(.*?)\}/',
];
// Parse special terms
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
class SearchRunner
{
-
/**
* @var EntityProvider
*/
*/
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;
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
- } else if (isset($searchOpts->filters['type'])) {
+ } elseif (isset($searchOpts->filters['type'])) {
$entityTypesToSearch = explode('|', $searchOpts->filters['type']);
}
}
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
{
}
/**
- * 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');
}
* 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)
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
{
$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 . '%');
});
}
foreach ($this->queryOperators as $operator) {
$escapedOperators[] = preg_quote($operator);
}
+
return join('|', $escapedOperators);
}
*/
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] : '';
$query->where('name', '=', $tagName);
}
});
+
return $query;
}
/**
- * Custom entity search filters
+ * Custom entity search filters.
*/
-
protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input)
{
try {
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)
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)
}
}
-
/**
- * 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');
}
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Book;
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
// 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 {
-<?php namespace BookStack\Entities\Tools;
+<?php
+
+namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Interfaces\Sluggable;
class SlugGenerator
{
-
/**
* Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item.
while ($this->slugInUse($slug, $model)) {
$slug .= '-' . Str::random(3);
}
+
return $slug;
}
protected function formatNameAsSlug(string $name): string
{
$slug = Str::slug($name);
- if ($slug === "") {
+ if ($slug === '') {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
+
return $slug;
}
-<?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;
class TrashCan
{
-
/**
* Send a shelf to the recycle bin.
*/
/**
* Send a book to the recycle bin.
+ *
* @throws Exception
*/
public function softDestroyBook(Book $book)
/**
* Send a chapter to the recycle bin.
+ *
* @throws Exception
*/
public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
/**
* Send a page to the recycle bin.
+ *
* @throws Exception
*/
public function softDestroyPage(Page $page, bool $recordDelete = true)
/**
* 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
$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
$this->destroyCommonRelations($chapter);
$chapter->forceDelete();
+
return $count + 1;
}
/**
* Remove a page from the system.
+ *
* @throws Exception
*/
protected function destroyPage(Page $page): int
}
$page->forceDelete();
+
return 1;
}
$counts = [];
/** @var Entity $instance */
- foreach ((new EntityProvider)->all() as $key => $instance) {
+ foreach ((new EntityProvider())->all() as $key => $instance) {
$counts[$key] = $instance->newQuery()->onlyTrashed()->count();
}
/**
* Destroy all items that have pending deletions.
+ *
* @throws Exception
*/
public function empty(): int
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
$count = $this->destroyEntity($deletion->deletable);
}
$deletion->delete();
+
return $count;
}
/**
* Restore the content within the given deletion.
+ *
* @throws Exception
*/
public function restoreFromDeletion(Deletion $deletion): int
}
$deletion->delete();
+
return $restoreCount;
}
* 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
/**
* Destroy the given entity.
+ *
* @throws Exception
*/
protected function destroyEntity(Entity $entity): int
class ApiAuthException extends UnauthorizedException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class FileUploadException extends PrettyException
{
-
}
* Report or log an exception.
*
* @param Exception $exception
- * @return void
*
* @throws Exception
+ *
+ * @return void
*/
public function report(Exception $exception)
{
/**
* Render an exception into an HTTP response.
*
- * @param \Illuminate\Http\Request $request
- * @param Exception $e
+ * @param \Illuminate\Http\Request $request
+ * @param Exception $e
+ *
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
$responseData = [
'error' => [
'message' => $e->getMessage(),
- ]
+ ],
];
if ($e instanceof ValidationException) {
}
$responseData['error']['code'] = $code;
+
return new JsonResponse($responseData, $code, $headers);
}
/**
* Convert an authentication exception into an unauthenticated response.
*
- * @param \Illuminate\Http\Request $request
- * @param \Illuminate\Auth\AuthenticationException $exception
+ * @param \Illuminate\Http\Request $request
+ * @param \Illuminate\Auth\AuthenticationException $exception
+ *
* @return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
/**
* Convert a validation exception into a JSON response.
*
- * @param \Illuminate\Http\Request $request
- * @param \Illuminate\Validation\ValidationException $exception
+ * @param \Illuminate\Http\Request $request
+ * @param \Illuminate\Validation\ValidationException $exception
+ *
* @return \Illuminate\Http\JsonResponse
*/
protected function invalidJson($request, ValidationException $exception)
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class ImageUploadException extends PrettyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
class JsonDebugException extends Exception
{
-
protected $data;
/**
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class LdapException extends PrettyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class LoginAttemptEmailNeededException extends LoginAttemptException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class LoginAttemptException extends \Exception
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
class MoveOperationException extends Exception
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class NotFoundException extends PrettyException
{
-
/**
* NotFoundException constructor.
*/
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Support\Responsable;
/**
* NotifyException constructor.
*/
- public function __construct(string $message, string $redirectLocation = "/")
+ public function __construct(string $message, string $redirectLocation = '/')
{
$this->message = $message;
$this->redirectLocation = $redirectLocation;
/**
* Send the response for this type of exception.
+ *
* @inheritdoc
*/
public function toResponse($request)
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
class PermissionsException extends Exception
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Support\Responsable;
/**
* Render a response for when this exception occurs.
+ *
* @inheritdoc
*/
public function toResponse($request)
{
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
+
return response()->view('errors.' . $code, [
- 'message' => $this->getMessage(),
+ 'message' => $this->getMessage(),
'subtitle' => $this->subtitle,
- 'details' => $this->details,
+ 'details' => $this->details,
], $code);
}
public function setSubtitle(string $subtitle): self
{
$this->subtitle = $subtitle;
+
return $this;
}
public function setDetails(string $details): self
{
$this->details = $details;
+
return $this;
}
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class SamlException extends NotifyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends PrettyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class SocialSignInAccountNotUsed extends SocialSignInException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
use Exception;
class SortOperationException extends Exception
{
-
}
class UnauthorizedException extends Exception
{
-
/**
* ApiAuthException constructor.
*/
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class UserTokenExpiredException extends \Exception
{
-
public $userId;
/**
* UserTokenExpiredException constructor.
+ *
* @param string $message
- * @param int $userId
+ * @param int $userId
*/
public function __construct(string $message, int $userId)
{
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class UserTokenNotFoundException extends \Exception
{
-
}
-<?php namespace BookStack\Exceptions;
+<?php
+
+namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException
{
-<?php namespace BookStack\Facades;
+<?php
+
+namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
-<?php namespace BookStack\Facades;
+<?php
+
+namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
-<?php namespace BookStack\Facades;
+<?php
+
+namespace BookStack\Facades;
use BookStack\Theming\ThemeService;
use Illuminate\Support\Facades\Facade;
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Api\ListingResponseBuilder;
use BookStack\Http\Controllers\Controller;
abstract class ApiController extends Controller
{
-
protected $rules = [];
/**
protected function apiListingResponse(Builder $query, array $fields): JsonResponse
{
$listing = new ListingResponseBuilder($query, request(), $fields);
+
return $listing->toResponse();
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Api\ApiDocsGenerator;
class ApiDocsController extends ApiController
{
-
/**
* Load the docs page for the API.
*/
{
$docs = ApiDocsGenerator::generateConsideringCache();
$this->setPageTitle(trans('settings.users_api_tokens_docs'));
+
return view('api-docs.index', [
'docs' => $docs,
]);
public function json()
{
$docs = ApiDocsGenerator::generateConsideringCache();
+
return response()->json($docs);
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookRepo;
-use BookStack\Exceptions\NotifyException;
-use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookApiController extends ApiController
{
-
protected $bookRepo;
protected $rules = [
'create' => [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'tags' => 'array',
+ 'tags' => 'array',
],
'update' => [
- 'name' => 'string|min:1|max:255',
+ 'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
- 'tags' => 'array',
+ 'tags' => 'array',
],
];
public function list()
{
$books = Book::visible();
+
return $this->apiListingResponse($books, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
]);
/**
* Create a new book in the system.
+ *
* @throws ValidationException
*/
public function create(Request $request)
$requestData = $this->validate($request, $this->rules['create']);
$book = $this->bookRepo->create($requestData);
+
return response()->json($book);
}
public function read(string $id)
{
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id);
+
return response()->json($book);
}
/**
* Update the details of a single book.
+ *
* @throws ValidationException
*/
public function update(Request $request, string $id)
/**
* Delete a single book.
* This will typically send the book to the recycle bin.
+ *
* @throws \Exception
*/
public function delete(string $id)
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
+
return response('', 204);
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\ExportFormatter;
/**
* Export a book as a PDF file.
+ *
* @throws Throwable
*/
public function exportPdf(int $id)
{
$book = Book::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->bookToPdf($book);
+
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
}
/**
* Export a book as a contained HTML file.
+ *
* @throws Throwable
*/
public function exportHtml(int $id)
{
$book = Book::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
+
return $this->downloadResponse($htmlContent, $book->slug . '.html');
}
{
$book = Book::visible()->findOrFail($id);
$textContent = $this->exportFormatter->bookToPlainText($book);
+
return $this->downloadResponse($textContent, $book->slug . '.txt');
}
{
$book = Book::visible()->findOrFail($id);
$markdown = $this->exportFormatter->bookToMarkdown($book);
+
return $this->downloadResponse($markdown, $book->slug . '.md');
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
-use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Repos\BookshelfRepo;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
class BookshelfApiController extends ApiController
{
-
/**
* @var BookshelfRepo
*/
protected $rules = [
'create' => [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'books' => 'array',
+ 'books' => 'array',
],
'update' => [
- 'name' => 'string|min:1|max:255',
+ 'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
- 'books' => 'array',
+ 'books' => 'array',
],
];
public function list()
{
$shelves = Bookshelf::visible();
+
return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
]);
* Create a new shelf in the system.
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided.
+ *
* @throws ValidationException
*/
public function create(Request $request)
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
'books' => function (BelongsToMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
- }
+ },
])->findOrFail($id);
+
return response()->json($shelf);
}
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided and overwrite
* any existing book assignments.
+ *
* @throws ValidationException
*/
public function update(Request $request, string $id)
$bookIds = $request->get('books', null);
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
+
return response()->json($shelf);
}
-
-
/**
* Delete a single shelf.
* This will typically send the shelf to the recycle bin.
+ *
* @throws Exception
*/
public function delete(string $id)
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);
+
return response('', 204);
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
-use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Repos\ChapterRepo;
-use BookStack\Facades\Activity;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
protected $rules = [
'create' => [
- 'book_id' => 'required|integer',
- 'name' => 'required|string|max:255',
+ 'book_id' => 'required|integer',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'tags' => 'array',
+ 'tags' => 'array',
],
'update' => [
- 'book_id' => 'integer',
- 'name' => 'string|min:1|max:255',
+ 'book_id' => 'integer',
+ 'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
- 'tags' => 'array',
+ 'tags' => 'array',
],
];
public function list()
{
$chapters = Chapter::visible();
+
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
+
return response()->json($chapter->load(['tags']));
}
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}])->findOrFail($id);
+
return response()->json($chapter);
}
$this->checkOwnablePermission('chapter-update', $chapter);
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
+
return response()->json($updatedChapter->load(['tags']));
}
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
+
return response('', 204);
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\ExportFormatter;
/**
* Export a chapter as a PDF file.
+ *
* @throws Throwable
*/
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
+
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
}
/**
* Export a chapter as a contained HTML file.
+ *
* @throws Throwable
*/
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
+
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
}
{
$chapter = Chapter::visible()->findOrFail($id);
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
+
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
}
{
$chapter = Chapter::visible()->findOrFail($id);
$markdown = $this->exportFormatter->chapterToMarkdown($chapter);
+
return $this->downloadResponse($markdown, $chapter->slug . '.md');
}
}
protected $rules = [
'create' => [
- 'book_id' => 'required_without:chapter_id|integer',
+ 'book_id' => 'required_without:chapter_id|integer',
'chapter_id' => 'required_without:book_id|integer',
- 'name' => 'required|string|max:255',
- 'html' => 'required_without:markdown|string',
- 'markdown' => 'required_without:html|string',
- 'tags' => 'array',
+ 'name' => 'required|string|max:255',
+ 'html' => 'required_without:markdown|string',
+ 'markdown' => 'required_without:html|string',
+ 'tags' => 'array',
],
'update' => [
- 'book_id' => 'required|integer',
+ 'book_id' => 'required|integer',
'chapter_id' => 'required|integer',
- 'name' => 'string|min:1|max:255',
- 'html' => 'string',
- 'markdown' => 'string',
- 'tags' => 'array',
+ 'name' => 'string|min:1|max:255',
+ 'html' => 'string',
+ 'markdown' => 'string',
+ 'tags' => 'array',
],
];
public function list()
{
$pages = Page::visible();
+
return $this->apiListingResponse($pages, [
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
'draft', 'template',
public function read(string $id)
{
$page = $this->pageRepo->getById($id, []);
+
return response()->json($page->forJsonDisplay());
}
$parent = null;
if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
- } else if ($request->has('book_id')) {
+ } elseif ($request->has('book_id')) {
$parent = Book::visible()->findOrFail($request->get('book_id'));
}
if ($parent && !$parent->matches($page->getParent())) {
$this->checkOwnablePermission('page-delete', $page);
+
try {
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
} catch (Exception $exception) {
}
$updatedPage = $this->pageRepo->update($page, $request->all());
+
return response()->json($updatedPage->forJsonDisplay());
}
$this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);
+
return response('', 204);
}
}
-<?php namespace BookStack\Http\Controllers\Api;
+<?php
+
+namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\ExportFormatter;
/**
* Export a page as a PDF file.
+ *
* @throws Throwable
*/
public function exportPdf(int $id)
{
$page = Page::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->pageToPdf($page);
+
return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
}
/**
* Export a page as a contained HTML file.
+ *
* @throws Throwable
*/
public function exportHtml(int $id)
{
$page = Page::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
+
return $this->downloadResponse($htmlContent, $page->slug . '.html');
}
{
$page = Page::visible()->findOrFail($id);
$textContent = $this->exportFormatter->pageToPlainText($page);
+
return $this->downloadResponse($textContent, $page->slug . '.txt');
}
{
$page = Page::visible()->findOrFail($id);
$markdown = $this->exportFormatter->pageToMarkdown($page);
+
return $this->downloadResponse($markdown, $page->slug . '.md');
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\FileUploadException;
$this->pageRepo = $pageRepo;
}
-
/**
* Endpoint at which attachments are uploaded to.
+ *
* @throws ValidationException
* @throws NotFoundException
*/
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
- 'file' => 'required|file'
+ 'file' => 'required|file',
]);
$pageId = $request->get('uploaded_to');
/**
* Update an uploaded attachment.
+ *
* @throws ValidationException
*/
public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
- 'file' => 'required|file'
+ 'file' => 'required|file',
]);
$attachment = Attachment::query()->findOrFail($attachmentId);
/**
* Get the update form for an attachment.
+ *
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function getUpdateForm(string $attachmentId)
{
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
+
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
- 'attachment_edit_url' => 'string|min:1|max:255|safe_url'
+ 'attachment_edit_url' => 'string|min:1|max:255|safe_url',
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
'attachment' => $attachment,
- 'errors' => new MessageBag($exception->errors()),
+ 'errors' => new MessageBag($exception->errors()),
]), 422);
}
/**
* Attach a link to a page.
+ *
* @throws NotFoundException
*/
public function attachLink(Request $request)
try {
$this->validate($request, [
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
- 'attachment_link_name' => 'required|string|min:1|max:255',
- 'attachment_link_url' => 'required|string|min:1|max:255|safe_url'
+ 'attachment_link_name' => 'required|string|min:1|max:255',
+ 'attachment_link_url' => 'required|string|min:1|max:255|safe_url',
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
+
return view('attachments.manager-list', [
'attachments' => $page->attachments->all(),
]);
/**
* Update the attachment sorting.
+ *
* @throws ValidationException
* @throws NotFoundException
*/
$attachmentOrder = $request->get('order');
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
+
return response()->json(['message' => trans('entities.attachments_order_updated')]);
}
/**
* Get an attachment from storage.
+ *
* @throws FileNotFoundException
* @throws NotFoundException
*/
{
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
+
try {
$page = $this->pageRepo->getById($attachment->uploaded_to);
} catch (NotFoundException $exception) {
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
}
+
return $this->downloadResponse($attachmentContents, $fileName);
}
/**
* Delete a specific attachment in the system.
+ *
* @throws Exception
*/
public function delete(string $attachmentId)
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
+
return response()->json(['message' => trans('entities.attachments_deleted')]);
}
}
class AuditLogController extends Controller
{
-
public function index(Request $request)
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$listDetails = [
- 'order' => $request->get('order', 'desc'),
- 'event' => $request->get('event', ''),
- 'sort' => $request->get('sort', 'created_at'),
+ 'order' => $request->get('order', 'desc'),
+ 'event' => $request->get('event', ''),
+ 'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
- 'date_to' => $request->get('date_to', ''),
- 'user' => $request->get('user', ''),
+ 'date_to' => $request->get('date_to', ''),
+ 'user' => $request->get('user', ''),
];
$query = Activity::query()
'entity' => function ($query) {
$query->withTrashed();
},
- 'user'
+ 'user',
])
->orderBy($listDetails['sort'], $listDetails['order']);
$types = DB::table('activities')->select('type')->distinct()->pluck('type');
$this->setPageTitle(trans('settings.audit'));
+
return view('settings.audit', [
- 'activities' => $activities,
- 'listDetails' => $listDetails,
+ 'activities' => $activities,
+ 'listDetails' => $listDetails,
'activityTypes' => $types,
]);
}
$this->userRepo = $userRepo;
}
-
/**
* Show the page to tell the user to check their email
* and confirm their address.
/**
* Shows a notice that a user's email address has not been confirmed,
* Also has the option to re-send the confirmation email.
+ *
* @return View
*/
public function showAwaiting()
/**
* Confirms an email via a token and logs the user into the system.
+ *
* @param $token
- * @return RedirectResponse|Redirector
+ *
* @throws ConfirmationEmailException
* @throws Exception
+ *
+ * @return RedirectResponse|Redirector
*/
public function confirm($token)
{
} catch (Exception $exception) {
if ($exception instanceof UserTokenNotFoundException) {
$this->showErrorNotification(trans('errors.email_confirmation_invalid'));
+
return redirect('/register');
}
$user = $this->userRepo->getById($exception->userId);
$this->emailConfirmationService->sendConfirmation($user);
$this->showErrorNotification(trans('errors.email_confirmation_expired'));
+
return redirect('/register/confirm');
}
return redirect('/');
}
-
/**
- * Resend the confirmation email
+ * Resend the confirmation email.
+ *
* @param Request $request
+ *
* @return View
*/
public function resend(Request $request)
{
$this->validate($request, [
- 'email' => 'required|email|exists:users,email'
+ 'email' => 'required|email|exists:users,email',
]);
$user = $this->userRepo->getByEmail($request->get('email'));
$this->emailConfirmationService->sendConfirmation($user);
} catch (Exception $e) {
$this->showErrorNotification(trans('auth.email_confirm_send_error'));
+
return redirect('/register/confirm');
}
$this->showSuccessNotification(trans('auth.email_confirm_resent'));
+
return redirect('/register/confirm');
}
}
$this->middleware('guard:standard');
}
-
/**
* Send a reset link to the given user.
*
- * @param \Illuminate\Http\Request $request
+ * @param \Illuminate\Http\Request $request
+ *
* @return \Illuminate\Http\RedirectResponse
*/
public function sendResetLinkEmail(Request $request)
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
$this->showSuccessNotification($message);
+
return back()->with('status', trans($response));
}
use AuthenticatesUsers;
/**
- * Redirection paths
+ * Redirection paths.
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
if ($request->has('email')) {
session()->flashInput([
- 'email' => $request->get('email'),
- 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
+ 'email' => $request->get('email'),
+ 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '',
]);
}
}
return view('auth.login', [
- 'socialDrivers' => $socialDrivers,
- 'authMethod' => $authMethod,
+ 'socialDrivers' => $socialDrivers,
+ 'authMethod' => $authMethod,
]);
}
/**
* Handle a login request to the application.
*
- * @param \Illuminate\Http\Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
+ * @param \Illuminate\Http\Request $request
*
* @throws \Illuminate\Validation\ValidationException
+ *
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$this->fireLockoutEvent($request);
Activity::logFailedLogin($username);
+
return $this->sendLockoutResponse($request);
}
}
} catch (LoginAttemptException $exception) {
Activity::logFailedLogin($username);
+
return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
$this->incrementLoginAttempts($request);
Activity::logFailedLogin($username);
+
return $this->sendFailedLoginResponse($request);
}
/**
* The user has been authenticated.
*
- * @param \Illuminate\Http\Request $request
- * @param mixed $user
+ * @param \Illuminate\Http\Request $request
+ * @param mixed $user
+ *
* @return mixed
*/
protected function authenticated(Request $request, $user)
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
+
return redirect()->intended($this->redirectPath());
}
/**
* Validate the user login request.
*
- * @param \Illuminate\Http\Request $request
- * @return void
+ * @param \Illuminate\Http\Request $request
*
* @throws \Illuminate\Validation\ValidationException
+ *
+ * @return void
*/
protected function validateLogin(Request $request)
{
/**
* Get the failed login response instance.
*
- * @param \Illuminate\Http\Request $request
- * @return \Symfony\Component\HttpFoundation\Response
+ * @param \Illuminate\Http\Request $request
*
* @throws \Illuminate\Validation\ValidationException
+ *
+ * @return \Symfony\Component\HttpFoundation\Response
*/
protected function sendFailedLoginResponse(Request $request)
{
protected function validator(array $data)
{
return Validator::make($data, [
- 'name' => 'required|min:2|max:255',
- 'email' => 'required|email|max:255|unique:users',
+ 'name' => 'required|min:2|max:255',
+ 'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:8',
]);
}
/**
* Show the application registration form.
+ *
* @throws UserRegistrationException
*/
public function getRegister()
{
$this->registrationService->ensureRegistrationAllowed();
$socialDrivers = $this->socialAuthService->getActiveDrivers();
+
return view('auth.register', [
'socialDrivers' => $socialDrivers,
]);
/**
* Handle a registration request for the application.
+ *
* @throws UserRegistrationException
*/
public function postRegister(Request $request)
if ($exception->getMessage()) {
$this->showErrorNotification($exception->getMessage());
}
+
return redirect($exception->redirectLocation);
}
$this->showSuccessNotification(trans('auth.register_success'));
+
return redirect($this->redirectPath());
}
/**
* Create a new user instance after a valid registration.
- * @param array $data
+ *
+ * @param array $data
+ *
* @return User
*/
protected function create(array $data)
{
return User::create([
- 'name' => $data['name'],
- 'email' => $data['email'],
+ 'name' => $data['name'],
+ 'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
* Get the response for a successful password reset.
*
* @param Request $request
- * @param string $response
+ * @param string $response
+ *
* @return \Illuminate\Http\Response
*/
protected function sendResetResponse(Request $request, $response)
$message = trans('auth.reset_password_success');
$this->showSuccessNotification($message);
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
+
return redirect($this->redirectPath())
->with('status', trans($response));
}
/**
* Get the response for a failed password reset.
*
- * @param \Illuminate\Http\Request $request
- * @param string $response
+ * @param \Illuminate\Http\Request $request
+ * @param string $response
+ *
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
class Saml2Controller extends Controller
{
-
protected $samlService;
/**
public function metadata()
{
$metaData = $this->samlService->metadata();
+
return response()->make($metaData, 200, [
- 'Content-Type' => 'text/xml'
+ 'Content-Type' => 'text/xml',
]);
}
{
$requestId = session()->pull('saml2_logout_request_id', null);
$redirect = $this->samlService->processSlsResponse($requestId) ?? '/';
+
return redirect($redirect);
}
$user = $this->samlService->processAcsResponse($requestId);
if ($user === null) {
$this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')]));
+
return redirect('/login');
}
class SocialController extends Controller
{
-
protected $socialAuthService;
protected $registrationService;
/**
* Redirect to the relevant social site.
+ *
* @throws SocialDriverNotConfigured
*/
public function login(string $socialDriver)
{
session()->put('social-callback', 'login');
+
return $this->socialAuthService->startLogIn($socialDriver);
}
/**
* Redirect to the social site for authentication intended to register.
+ *
* @throws SocialDriverNotConfigured
* @throws UserRegistrationException
*/
{
$this->registrationService->ensureRegistrationAllowed();
session()->put('social-callback', 'register');
+
return $this->socialAuthService->startRegister($socialDriver);
}
/**
* The callback for social login services.
+ *
* @throws SocialSignInException
* @throws SocialDriverNotConfigured
* @throws UserRegistrationException
if ($request->has('error') && $request->has('error_description')) {
throw new SocialSignInException(trans('errors.social_login_bad_response', [
'socialAccount' => $socialDriver,
- 'error' => $request->get('error_description'),
+ 'error' => $request->get('error_description'),
]), '/login');
}
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
+
throw $exception;
}
}
{
$this->socialAuthService->detachSocialAccount($socialDriver);
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
+
return redirect(user()->getEditUrl());
}
/**
* Register a new user after a registration callback.
+ *
* @throws UserRegistrationException
*/
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
// Create an array of the user data to create a new user instance
$userData = [
- 'name' => $socialUser->getName(),
- 'email' => $socialUser->getEmail(),
- 'password' => Str::random(32)
+ 'name' => $socialUser->getName(),
+ 'email' => $socialUser->getEmail(),
+ 'password' => Str::random(32),
];
// Take name from email address if empty
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
$this->showSuccessNotification(trans('auth.register_success'));
+
return redirect('/');
}
}
/**
* Show the page for the user to set the password for their account.
+ *
* @throws Exception
*/
public function showSetPassword(string $token)
/**
* Sets the password for an invited user and then grants them access.
+ *
* @throws Exception
*/
public function setPassword(Request $request, string $token)
{
$this->validate($request, [
- 'password' => 'required|min:8'
+ 'password' => 'required|min:8',
]);
try {
/**
* Check and validate the exception thrown when checking an invite token.
- * @return RedirectResponse|Redirector
+ *
* @throws Exception
+ *
+ * @return RedirectResponse|Redirector
*/
protected function handleTokenException(Exception $exception)
{
if ($exception instanceof UserTokenExpiredException) {
$this->showErrorNotification(trans('errors.invite_token_expired'));
+
return redirect('/password/email');
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Actions\ActivityType;
use BookStack\Actions\View;
-use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
-use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\ImageUploadException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookController extends Controller
{
-
protected $bookRepo;
protected $entityContextManager;
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
+
return view('books.index', [
- 'books' => $books,
+ 'books' => $books,
'recents' => $recents,
'popular' => $popular,
- 'new' => $new,
- 'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
+ 'new' => $new,
+ 'view' => $view,
+ 'sort' => $sort,
+ 'order' => $order,
]);
}
}
$this->setPageTitle(trans('entities.books_create'));
+
return view('books.create', [
- 'bookshelf' => $bookshelf
+ 'bookshelf' => $bookshelf,
]);
}
/**
* Store a newly created book in storage.
+ *
* @throws ImageUploadException
* @throws ValidationException
*/
{
$this->checkPermission('book-create-all');
$this->validate($request, [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => 'nullable|' . $this->getImageValidationRules(),
+ 'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$bookshelf = null;
}
$this->setPageTitle($book->getShortName());
+
return view('books.show', [
- 'book' => $book,
- 'current' => $book,
- 'bookChildren' => $bookChildren,
+ 'book' => $book,
+ 'current' => $book,
+ 'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
- 'activity' => Activity::entityActivity($book, 20, 1)
+ 'activity' => Activity::entityActivity($book, 20, 1),
]);
}
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
+
return view('books.edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
+ *
* @throws ImageUploadException
* @throws ValidationException
* @throws Throwable
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => 'nullable|' . $this->getImageValidationRules(),
+ 'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$book = $this->bookRepo->update($book, $request->all());
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
+
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
* Remove the specified book from the system.
+ *
* @throws Throwable
*/
public function destroy(string $bookSlug)
/**
* Set the restrictions for this book.
+ *
* @throws Throwable
*/
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
$permissionsUpdater->updateFromPermissionsForm($book, $request);
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
+
return redirect($book->getUrl());
}
}
namespace BookStack\Http\Controllers;
-use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Tools\ExportFormatter;
use Throwable;
class BookExportController extends Controller
{
-
protected $bookRepo;
protected $exportFormatter;
/**
* Export a book as a PDF file.
+ *
* @throws Throwable
*/
public function pdf(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$pdfContent = $this->exportFormatter->bookToPdf($book);
+
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
}
/**
* Export a book as a contained HTML file.
+ *
* @throws Throwable
*/
public function html(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
+
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
}
{
$book = $this->bookRepo->getBySlug($bookSlug);
$textContent = $this->exportFormatter->bookToPlainText($book);
+
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
{
$book = $this->bookRepo->getBySlug($bookSlug);
$textContent = $this->exportFormatter->bookToMarkdown($book);
+
return $this->downloadResponse($textContent, $bookSlug . '.md');
}
}
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
-use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Tools\BookContents;
use BookStack\Exceptions\SortOperationException;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
class BookSortController extends Controller
{
-
protected $bookRepo;
public function __construct(BookRepo $bookRepo)
$bookChildren = (new BookContents($book))->getTree(false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
+
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
{
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = (new BookContents($book))->getTree();
+
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
-use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
-use Views;
class BookshelfController extends Controller
{
-
protected $bookshelfRepo;
protected $entityContextManager;
protected $imageRepo;
$sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
$order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
- 'name' => trans('common.sort_name'),
+ 'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
+
return view('shelves.index', [
- 'shelves' => $shelves,
- 'recents' => $recents,
- 'popular' => $popular,
- 'new' => $new,
- 'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
+ 'shelves' => $shelves,
+ 'recents' => $recents,
+ 'popular' => $popular,
+ 'new' => $new,
+ 'view' => $view,
+ 'sort' => $sort,
+ 'order' => $order,
'sortOptions' => $sortOptions,
]);
}
$this->checkPermission('bookshelf-create-all');
$books = Book::hasPermission('update')->get();
$this->setPageTitle(trans('entities.shelves_create'));
+
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
+ *
* @throws ValidationException
* @throws ImageUploadException
*/
{
$this->checkPermission('bookshelf-create-all');
$this->validate($request, [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => 'nullable|' . $this->getImageValidationRules(),
+ 'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$bookIds = explode(',', $request->get('books', ''));
/**
* Display the bookshelf of the given slug.
+ *
* @throws NotFoundException
*/
public function show(string $slug)
$view = setting()->getForCurrentUser('bookshelf_view_type');
$this->setPageTitle($shelf->getShortName());
+
return view('shelves.show', [
- 'shelf' => $shelf,
+ 'shelf' => $shelf,
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
- 'view' => $view,
- 'activity' => Activity::entityActivity($shelf, 20, 1),
- 'order' => $order,
- 'sort' => $sort
+ 'view' => $view,
+ 'activity' => Activity::entityActivity($shelf, 20, 1),
+ 'order' => $order,
+ 'sort' => $sort,
]);
}
$books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
+
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
/**
* Update the specified bookshelf in storage.
+ *
* @throws ValidationException
* @throws ImageUploadException
* @throws NotFoundException
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
- 'name' => 'required|string|max:255',
+ 'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => 'nullable|' . $this->getImageValidationRules(),
+ 'image' => 'nullable|' . $this->getImageValidationRules(),
]);
-
$bookIds = explode(',', $request->get('books', ''));
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
$resetCover = $request->has('image_reset');
}
/**
- * Shows the page to confirm deletion
+ * Shows the page to confirm deletion.
*/
public function showDelete(string $slug)
{
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
+
return view('shelves.delete', ['shelf' => $shelf]);
}
/**
* Remove the specified bookshelf from storage.
+ *
* @throws Exception
*/
public function destroy(string $slug)
$permissionsUpdater->updateFromPermissionsForm($shelf, $request);
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
+
return redirect($shelf->getUrl());
}
$updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+
return redirect($shelf->getUrl());
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
-use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
class ChapterController extends Controller
{
-
protected $chapterRepo;
/**
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
+
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
* Store a newly created chapter in storage.
+ *
* @throws ValidationException
*/
public function store(Request $request, string $bookSlug)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255',
]);
$book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
View::incrementFor($chapter);
$this->setPageTitle($chapter->getShortName());
+
return view('chapters.show', [
- 'book' => $chapter->book,
- 'chapter' => $chapter,
- 'current' => $chapter,
+ 'book' => $chapter->book,
+ 'chapter' => $chapter,
+ 'current' => $chapter,
'sidebarTree' => $sidebarTree,
- 'pages' => $pages,
- 'next' => $nextPreviousLocator->getNext(),
- 'previous' => $nextPreviousLocator->getPrevious(),
+ 'pages' => $pages,
+ 'next' => $nextPreviousLocator->getNext(),
+ 'previous' => $nextPreviousLocator->getPrevious(),
]);
}
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
+
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Update the specified chapter in storage.
+ *
* @throws NotFoundException
*/
public function update(Request $request, string $bookSlug, string $chapterSlug)
/**
* Shows the page to confirm deletion of this chapter.
+ *
* @throws NotFoundException
*/
public function showDelete(string $bookSlug, string $chapterSlug)
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
+
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Remove the specified chapter from storage.
+ *
* @throws NotFoundException
* @throws Throwable
*/
/**
* Show the page for moving a chapter.
+ *
* @throws NotFoundException
*/
public function showMove(string $bookSlug, string $chapterSlug)
return view('chapters.move', [
'chapter' => $chapter,
- 'book' => $chapter->book
+ 'book' => $chapter->book,
]);
}
/**
* Perform the move action for a chapter.
+ *
* @throws NotFoundException
*/
public function move(Request $request, string $bookSlug, string $chapterSlug)
$newBook = $this->chapterRepo->move($chapter, $entitySelection);
} catch (MoveOperationException $exception) {
$this->showErrorNotification(trans('errors.selected_book_not_found'));
+
return redirect()->back();
}
$this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
+
return redirect($chapter->getUrl());
}
/**
* Show the Restrictions view.
+ *
* @throws NotFoundException
*/
public function showPermissions(string $bookSlug, string $chapterSlug)
/**
* Set the restrictions for this chapter.
+ *
* @throws NotFoundException
*/
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug)
$permissionsUpdater->updateFromPermissionsForm($chapter, $request);
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
+
return redirect($chapter->getUrl());
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
-use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Exceptions\NotFoundException;
use Throwable;
class ChapterExportController extends Controller
{
-
protected $chapterRepo;
protected $exportFormatter;
/**
* Exports a chapter to pdf.
+ *
* @throws NotFoundException
* @throws Throwable
*/
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
+
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
}
/**
* Export a chapter to a self-contained HTML file.
+ *
* @throws NotFoundException
* @throws Throwable
*/
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
+
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
}
/**
* Export a chapter to a simple plaintext .txt file.
+ *
* @throws NotFoundException
*/
public function plainText(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToPlainText($chapter);
+
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
}
/**
* Export a chapter to a simple markdown file.
+ *
* @throws NotFoundException
*/
public function markdown(string $bookSlug, string $chapterSlug)
// TODO: This should probably export to a zip file.
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$chapterText = $this->exportFormatter->chapterToMarkdown($chapter);
+
return $this->downloadResponse($chapterText, $chapterSlug . '.md');
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
-use Activity;
-use BookStack\Actions\ActivityType;
use BookStack\Actions\CommentRepo;
use BookStack\Entities\Models\Page;
use Illuminate\Http\Request;
}
/**
- * Save a new comment for a Page
+ * Save a new comment for a Page.
+ *
* @throws ValidationException
*/
public function savePageComment(Request $request, int $pageId)
{
$this->validate($request, [
- 'text' => 'required|string',
+ 'text' => 'required|string',
'parent_id' => 'nullable|integer',
]);
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
+
return view('comments.comment', ['comment' => $comment]);
}
/**
* Update an existing comment.
+ *
* @throws ValidationException
*/
public function update(Request $request, int $commentId)
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->get('text'));
+
return view('comments.comment', ['comment' => $comment]);
}
$this->checkOwnablePermission('comment-delete', $comment);
$this->commentRepo->delete($comment);
+
return response()->json(['message' => trans('entities.comment_deleted')]);
}
}
use BookStack\Facades\Activity;
use BookStack\Interfaces\Loggable;
-use BookStack\HasCreatorAndUpdater;
use BookStack\Model;
use finfo;
use Illuminate\Foundation\Bus\DispatchesJobs;
abstract class Controller extends BaseController
{
- use DispatchesJobs, ValidatesRequests;
+ use DispatchesJobs;
+ use ValidatesRequests;
/**
* Check if the current user is signed in.
/**
* Send back a json error message.
*/
- protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse
+ protected function jsonError(string $messageText = '', int $statusCode = 500): JsonResponse
{
return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
- 'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
+ 'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
]);
}
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
+
return response()->make($content, 200, [
'Content-Type' => $mime,
- 'Content-Disposition' => 'inline; filename="' . $fileName . '"'
+ 'Content-Disposition' => 'inline; filename="' . $fileName . '"',
]);
}
/**
* Log an activity in the system.
+ *
* @param string|Loggable
*/
protected function logActivity(string $type, $detail = ''): void
{
$viewCount = 20;
$page = intval($request->get('page', 1));
- $favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
+ $favourites = (new TopFavourites())->run($viewCount + 1, (($page - 1) * $viewCount));
- $hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
+ $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
return view('common.detailed-listing-with-more', [
- 'title' => trans('entities.my_favourites'),
- 'entities' => $favourites->slice(0, $viewCount),
+ 'title' => trans('entities.my_favourites'),
+ 'entities' => $favourites->slice(0, $viewCount),
'hasMoreLink' => $hasMoreLink,
]);
}
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
'name' => $favouritable->name,
]));
+
return redirect()->back();
}
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
'name' => $favouritable->name,
]));
+
return redirect()->back();
}
{
$modelInfo = $this->validate($request, [
'type' => 'required|string',
- 'id' => 'required|integer',
+ 'id' => 'required|integer',
]);
if (!class_exists($modelInfo['type'])) {
}
/** @var Model $model */
- $model = new $modelInfo['type'];
- if (! $model instanceof Favouritable) {
+ $model = new $modelInfo['type']();
+ if (!$model instanceof Favouritable) {
throw new \Exception('Model not favouritable');
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Queries\RecentlyViewed;
use BookStack\Entities\Queries\TopFavourites;
-use BookStack\Entities\Tools\PageContent;
-use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Tools\PageContent;
use Views;
class HomeController extends Controller
{
-
/**
* Display the homepage.
*/
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
- (new RecentlyViewed)->run(12*$recentFactor, 1)
+ (new RecentlyViewed())->run(12 * $recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
- $favourites = (new TopFavourites)->run(6);
+ $favourites = (new TopFavourites())->run(6);
$recentlyUpdatedPages = Page::visible()->with('book')
->where('draft', false)
->orderBy('updated_at', 'desc')
}
$commonData = [
- 'activity' => $activity,
- 'recents' => $recents,
+ 'activity' => $activity,
+ 'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
- 'draftPages' => $draftPages,
- 'favourites' => $favourites,
+ 'draftPages' => $draftPages,
+ 'favourites' => $favourites,
];
// Add required list ordering & sorting for books & shelves views.
$order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
$sortOptions = [
- 'name' => trans('common.sort_name'),
+ 'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$commonData = array_merge($commonData, [
- 'view' => $view,
- 'sort' => $sort,
- 'order' => $order,
+ 'view' => $view,
+ 'sort' => $sort,
+ 'order' => $order,
'sortOptions' => $sortOptions,
]);
}
if ($homepageOption === 'bookshelves') {
$shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['shelves' => $shelves]);
+
return view('common.home-shelves', $data);
}
$bookRepo = app(BookRepo::class);
$books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
+
return view('common.home-book', $data);
}
$customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
$pageContent = new PageContent($customHomepage);
$customHomepage->html = $pageContent->render(true);
+
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
/**
* Get custom head HTML, Used in ajax calls to show in editor.
+ *
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function customHeadContent()
}
/**
- * Show the view for /robots.txt
+ * Show the view for /robots.txt.
*/
public function getRobots()
{
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
+use BookStack\Http\Controllers\Controller;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\Request;
-use BookStack\Http\Controllers\Controller;
class DrawioImageController extends Controller
{
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
+
return view('components.image-manager-list', [
- 'images' => $imgData['images'],
+ 'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
}
/**
* Store a new gallery image in the system.
+ *
* @throws Exception
*/
public function create(Request $request)
{
$this->validate($request, [
- 'image' => 'required|string',
- 'uploaded_to' => 'required|integer'
+ 'image' => 'required|string',
+ 'uploaded_to' => 'required|integer',
]);
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$page = $image->getPage();
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
- return $this->jsonError("Image data could not be found");
+ return $this->jsonError('Image data could not be found');
}
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
- return $this->jsonError("Image data could not be found");
+ return $this->jsonError('Image data could not be found');
}
return response()->json([
- 'content' => base64_encode($imageData)
+ 'content' => base64_encode($imageData),
]);
}
}
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
+use BookStack\Http\Controllers\Controller;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
-use BookStack\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
+
return view('components.image-manager-list', [
- 'images' => $imgData['images'],
+ 'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
}
/**
* Store a new gallery image in the system.
+ *
* @throws ValidationException
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
- 'file' => $this->getImageValidationRules()
+ 'file' => $this->getImageValidationRules(),
]);
try {
-<?php namespace BookStack\Http\Controllers\Images;
+<?php
+
+namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
/**
* Provide an image file from storage.
+ *
* @throws NotFoundException
*/
public function showImage(string $path)
return response()->file($path);
}
-
/**
- * Update image details
+ * Update image details.
+ *
* @throws ImageUploadException
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$this->validate($request, [
- 'name' => 'required|min:2|string'
+ 'name' => 'required|min:2|string',
]);
$image = $this->imageRepo->getById($id);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
$this->imageRepo->loadThumbs($image);
+
return view('components.image-manager-form', [
- 'image' => $image,
+ 'image' => $image,
'dependantPages' => null,
]);
}
/**
* Get the form for editing the given image.
+ *
* @throws Exception
*/
public function edit(Request $request, string $id)
}
$this->imageRepo->loadThumbs($image);
+
return view('components.image-manager-form', [
- 'image' => $image,
+ 'image' => $image,
'dependantPages' => $dependantPages ?? null,
]);
}
/**
- * Deletes an image and all thumbnail/image files
+ * Deletes an image and all thumbnail/image files.
+ *
* @throws Exception
*/
public function destroy(string $id)
$this->checkImagePermission($image);
$this->imageRepo->destroyImage($image);
+
return response('');
}
$recycleStats = (new TrashCan())->getTrashedCounts();
return view('settings.maintenance', [
- 'version' => $version,
+ 'version' => $version,
'recycleStats' => $recycleStats,
]);
}
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
$this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
+
return redirect('/settings/maintenance')->withInput();
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
-use BookStack\Entities\Models\Page;
-use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
class PageController extends Controller
{
-
protected $pageRepo;
/**
/**
* Show the form for creating a new page.
+ *
* @throws Throwable
*/
public function create(string $bookSlug, string $chapterSlug = null)
// Redirect to draft edit screen if signed in
if ($this->isSignedIn()) {
$draft = $this->pageRepo->getNewDraftPage($parent);
+
return redirect($draft->getUrl());
}
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
+
return view('pages.guest-create', ['parent' => $parent]);
}
/**
* Create a new page as a guest user.
+ *
* @throws ValidationException
*/
public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255',
]);
$parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$page = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($page, [
'name' => $request->get('name'),
- 'html' => ''
+ 'html' => '',
]);
return redirect($page->getUrl('/edit'));
/**
* Show form to continue editing a draft page.
+ *
* @throws NotFoundException
*/
public function editDraft(string $bookSlug, int $pageId)
$templates = $this->pageRepo->getTemplates(10);
return view('pages.edit', [
- 'page' => $draft,
- 'book' => $draft->book,
- 'isDraft' => true,
+ 'page' => $draft,
+ 'book' => $draft->book,
+ 'isDraft' => true,
'draftsEnabled' => $draftsEnabled,
- 'templates' => $templates,
+ 'templates' => $templates,
]);
}
/**
* Store a new page by changing a draft into a page.
+ *
* @throws NotFoundException
* @throws ValidationException
*/
public function store(Request $request, string $bookSlug, int $pageId)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255',
]);
$draftPage = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-create', $draftPage->getParent());
/**
* Display the specified page.
* If the page is not found via the slug the revisions are searched for a match.
+ *
* @throws NotFoundException
*/
public function show(string $bookSlug, string $pageSlug)
View::incrementFor($page);
$this->setPageTitle($page->getShortName());
+
return view('pages.show', [
- 'page' => $page,
- 'book' => $page->book,
- 'current' => $page,
- 'sidebarTree' => $sidebarTree,
+ 'page' => $page,
+ 'book' => $page->book,
+ 'current' => $page,
+ 'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
- 'pageNav' => $pageNav,
- 'next' => $nextPreviousLocator->getNext(),
- 'previous' => $nextPreviousLocator->getPrevious(),
+ 'pageNav' => $pageNav,
+ 'next' => $nextPreviousLocator->getNext(),
+ 'previous' => $nextPreviousLocator->getPrevious(),
]);
}
/**
* Get page from an ajax request.
+ *
* @throws NotFoundException
*/
public function getPageAjax(int $pageId)
$page = $this->pageRepo->getById($pageId);
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->addHidden(['book']);
+
return response()->json($page);
}
/**
* Show the form for editing the specified page.
+ *
* @throws NotFoundException
*/
public function edit(string $bookSlug, string $pageSlug)
$templates = $this->pageRepo->getTemplates(10);
$draftsEnabled = $this->isSignedIn();
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
+
return view('pages.edit', [
- 'page' => $page,
- 'book' => $page->book,
- 'current' => $page,
+ 'page' => $page,
+ 'book' => $page->book,
+ 'current' => $page,
'draftsEnabled' => $draftsEnabled,
- 'templates' => $templates,
+ 'templates' => $templates,
]);
}
/**
* Update the specified page in storage.
+ *
* @throws ValidationException
* @throws NotFoundException
*/
public function update(Request $request, string $bookSlug, string $pageSlug)
{
$this->validate($request, [
- 'name' => 'required|string|max:255'
+ 'name' => 'required|string|max:255',
]);
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
/**
* Save a draft update as a revision.
+ *
* @throws NotFoundException
*/
public function saveDraft(Request $request, int $pageId)
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
+
return response()->json([
- 'status' => 'success',
- 'message' => trans('entities.pages_edit_draft_save_at'),
- 'timestamp' => $updateTime
+ 'status' => 'success',
+ 'message' => trans('entities.pages_edit_draft_save_at'),
+ 'timestamp' => $updateTime,
]);
}
/**
* Redirect from a special link url which uses the page id rather than the name.
+ *
* @throws NotFoundException
*/
public function redirectFromLink(int $pageId)
{
$page = $this->pageRepo->getById($pageId);
+
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
+ *
* @throws NotFoundException
*/
public function showDelete(string $bookSlug, string $pageSlug)
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
+
return view('pages.delete', [
- 'book' => $page->book,
- 'page' => $page,
- 'current' => $page
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page,
]);
}
/**
* Show the deletion page for the specified page.
+ *
* @throws NotFoundException
*/
public function showDeleteDraft(string $bookSlug, int $pageId)
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
+
return view('pages.delete', [
- 'book' => $page->book,
- 'page' => $page,
- 'current' => $page
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page,
]);
}
/**
* Remove the specified page from storage.
+ *
* @throws NotFoundException
* @throws Throwable
*/
/**
* Remove the specified draft page from storage.
+ *
* @throws NotFoundException
* @throws Throwable
*/
if ($chapter && userCan('view', $chapter)) {
return redirect($chapter->getUrl());
}
+
return redirect($book->getUrl());
}
->setPath(url('/pages/recently-updated'));
return view('common.detailed-listing-paginated', [
- 'title' => trans('entities.recently_updated_pages'),
- 'entities' => $pages
+ 'title' => trans('entities.recently_updated_pages'),
+ 'entities' => $pages,
]);
}
/**
* Show the view to choose a new parent to move a page into.
+ *
* @throws NotFoundException
*/
public function showMove(string $bookSlug, string $pageSlug)
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
+
return view('pages.move', [
'book' => $page->book,
- 'page' => $page
+ 'page' => $page,
]);
}
/**
* Does the action of moving the location of a page.
+ *
* @throws NotFoundException
* @throws Throwable
*/
}
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+
return redirect()->back();
}
$this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
+
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
+ *
* @throws NotFoundException
*/
public function showCopy(string $bookSlug, string $pageSlug)
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
+
return view('pages.copy', [
'book' => $page->book,
- 'page' => $page
+ 'page' => $page,
]);
}
-
/**
* Create a copy of a page within the requested target destination.
+ *
* @throws NotFoundException
* @throws Throwable
*/
}
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+
return redirect()->back();
}
$this->showSuccessNotification(trans('entities.pages_copy_success'));
+
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
+ *
* @throws NotFoundException
*/
public function showPermissions(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
+
return view('pages.permissions', [
'page' => $page,
]);
/**
* Set the permissions for this page.
+ *
* @throws NotFoundException
* @throws Throwable
*/
$permissionsUpdater->updateFromPermissionsForm($page, $request);
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
+
return redirect($page->getUrl());
}
}
namespace BookStack\Http\Controllers;
+use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Tools\PageContent;
-use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use Throwable;
class PageExportController extends Controller
{
-
protected $pageRepo;
protected $exportFormatter;
/**
* Exports a page to a PDF.
- * https://github.com/barryvdh/laravel-dompdf
+ * https://github.com/barryvdh/laravel-dompdf.
+ *
* @throws NotFoundException
* @throws Throwable
*/
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$pdfContent = $this->exportFormatter->pageToPdf($page);
+
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
/**
* Export a page to a self-contained HTML file.
+ *
* @throws NotFoundException
* @throws Throwable
*/
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
$containedHtml = $this->exportFormatter->pageToContainedHtml($page);
+
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
}
/**
* Export a page to a simple plaintext .txt file.
+ *
* @throws NotFoundException
*/
public function plainText(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToPlainText($page);
+
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
* Export a page to a simple markdown .md file.
+ *
* @throws NotFoundException
*/
public function markdown(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$pageText = $this->exportFormatter->pageToMarkdown($page);
+
return $this->downloadResponse($pageText, $pageSlug . '.md');
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
-use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use Ssddanbrown\HtmlDiff\Diff;
class PageRevisionController extends Controller
{
-
protected $pageRepo;
/**
/**
* Shows the last revisions for this page.
+ *
* @throws NotFoundException
*/
public function index(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+
return view('pages.revisions', [
- 'page' => $page,
- 'current' => $page
+ 'page' => $page,
+ 'current' => $page,
]);
}
/**
* Shows a preview of a single revision.
+ *
* @throws NotFoundException
*/
public function show(string $bookSlug, string $pageSlug, int $revisionId)
$page->html = (new PageContent($page))->render();
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+
return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => null,
- 'revision' => $revision
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => null,
+ 'revision' => $revision,
]);
}
/**
* Shows the changes of a single revision.
+ *
* @throws NotFoundException
*/
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => $diff,
- 'revision' => $revision
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => $diff,
+ 'revision' => $revision,
]);
}
/**
* Restores a page using the content of the specified revision.
+ *
* @throws NotFoundException
*/
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
/**
* Deletes a revision using the id of the specified revision.
+ *
* @throws NotFoundException
*/
public function destroy(string $bookSlug, string $pageSlug, int $revId)
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
$this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
+
return redirect($page->getUrl('/revisions'));
}
$revision->delete();
$this->showSuccessNotification(trans('entities.revision_delete_success'));
+
return redirect($page->getUrl('/revisions'));
}
}
protected $pageRepo;
/**
- * PageTemplateController constructor
+ * PageTemplateController constructor.
*/
public function __construct(PageRepo $pageRepo)
{
}
return view('pages.template-manager-list', [
- 'templates' => $templates
+ 'templates' => $templates,
]);
}
/**
* Get the content of a template.
+ *
* @throws NotFoundException
*/
public function get(int $templateId)
}
return response()->json([
- 'html' => $page->html,
+ 'html' => $page->html,
'markdown' => $page->markdown,
]);
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Deletion;
class RecycleBinController extends Controller
{
-
protected $recycleBinBaseUrl = '/settings/recycle-bin';
/**
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
+
return $next($request);
});
}
-
/**
* Show the top-level listing for the recycle bin.
*/
$deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
$this->setPageTitle(trans('settings.recycle_bin'));
+
return view('settings.recycle-bin.index', [
'deletions' => $deletions,
]);
$parentDeletion = ($currentDeletable === $deletion->deletable) ? null : $currentDeletable->deletions()->first();
return view('settings.recycle-bin.restore', [
- 'deletion' => $deletion,
+ 'deletion' => $deletion,
'parentDeletion' => $parentDeletion,
]);
}
/**
* Restore the element attached to the given deletion.
+ *
* @throws \Exception
*/
public function restore(string $id)
$restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
$this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
+
return redirect($this->recycleBinBaseUrl);
}
/**
* Permanently delete the content associated with the given deletion.
+ *
* @throws \Exception
*/
public function destroy(string $id)
$deleteCount = (new TrashCan())->destroyFromDeletion($deletion);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
+
return redirect($this->recycleBinBaseUrl);
}
/**
* Empty out the recycle bin.
+ *
* @throws \Exception
*/
public function empty()
$this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
$this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
+
return redirect($this->recycleBinBaseUrl);
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Exceptions\PermissionsException;
class RoleController extends Controller
{
-
protected $permissionsRepo;
/**
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
+
return view('settings.roles.index', ['roles' => $roles]);
}
/**
- * Show the form to create a new role
+ * Show the form to create a new role.
*/
public function create()
{
$this->checkPermission('user-roles-manage');
+
return view('settings.roles.create');
}
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:180',
- 'description' => 'max:180'
+ 'description' => 'max:180',
]);
$this->permissionsRepo->saveNewRole($request->all());
$this->showSuccessNotification(trans('settings.role_create_success'));
+
return redirect('/settings/roles');
}
/**
* Show the form for editing a user role.
+ *
* @throws PermissionsException
*/
public function edit(string $id)
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
+
return view('settings.roles.edit', ['role' => $role]);
}
/**
* Updates a user role.
+ *
* @throws ValidationException
*/
public function update(Request $request, string $id)
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:180',
- 'description' => 'max:180'
+ 'description' => 'max:180',
]);
$this->permissionsRepo->updateRole($id, $request->all());
$this->showSuccessNotification(trans('settings.role_update_success'));
+
return redirect('/settings/roles');
}
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole);
+
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
}
/**
* Delete a role from the system,
* Migrate from a previous role if set.
+ *
* @throws Exception
*/
public function delete(Request $request, string $id)
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage());
+
return redirect()->back();
}
$this->showSuccessNotification(trans('settings.role_delete_success'));
+
return redirect('/settings/roles');
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Entities\Queries\Popular;
+use BookStack\Entities\Tools\SearchOptions;
use BookStack\Entities\Tools\SearchRunner;
use BookStack\Entities\Tools\ShelfContext;
-use BookStack\Entities\Tools\SearchOptions;
use BookStack\Entities\Tools\SiblingFetcher;
use Illuminate\Http\Request;
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
$page = intval($request->get('page', '0')) ?: 1;
- $nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1));
+ $nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page + 1));
$results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
return view('search.all', [
- 'entities' => $results['results'],
+ 'entities' => $results['results'],
'totalResults' => $results['total'],
- 'searchTerm' => $fullSearchString,
- 'hasNextPage' => $results['has_more'],
+ 'searchTerm' => $fullSearchString,
+ 'hasNextPage' => $results['has_more'],
'nextPageLink' => $nextPageLink,
- 'options' => $searchOpts,
+ 'options' => $searchOpts,
]);
}
{
$term = $request->get('term', '');
$results = $this->searchRunner->searchBook($bookId, $term);
+
return view('partials.entity-list', ['entities' => $results]);
}
{
$term = $request->get('term', '');
$results = $this->searchRunner->searchChapter($chapterId, $term);
+
return view('partials.entity-list', ['entities' => $results]);
}
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
- $searchTerm = $request->get('term', false);
+ $searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
- $searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
+ $searchTerm .= ' {type:' . implode('|', $entityTypes) . '}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
- $entities = (new Popular)->run(20, 0, $entityTypes, $permission);
+ $entities = (new Popular())->run(20, 0, $entityTypes, $permission);
}
return view('search.entity-ajax-list', ['entities' => $entities]);
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
- $entities = (new SiblingFetcher)->fetch($type, $id);
+ $entities = (new SiblingFetcher())->fetch($type, $id);
+
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
$version = trim(file_get_contents(base_path('version')));
return view('settings.index', [
- 'version' => $version,
- 'guestUser' => User::getDefault()
+ 'version' => $version,
+ 'guestUser' => User::getDefault(),
]);
}
$this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
$this->showSuccessNotification(trans('settings.settings_save_success'));
$redirectLocation = '/settings#' . $section;
+
return redirect(rtrim($redirectLocation, '#'));
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatusController extends Controller
{
-
/**
* Show the system status as a simple json page.
*/
'cache' => $this->trueWithoutError(function () {
$rand = Str::random();
Cache::set('status_test', $rand);
+
return Cache::get('status_test') === $rand;
}),
'session' => $this->trueWithoutError(function () {
$rand = Str::random();
Session::put('status_test', $rand);
+
return Session::get('status_test') === $rand;
}),
];
$hasError = in_array(false, $statuses);
+
return response()->json($statuses, $hasError ? 500 : 200);
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\TagRepo;
use Illuminate\Http\Request;
class TagController extends Controller
{
-
protected $tagRepo;
/**
{
$searchTerm = $request->get('search', null);
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
+
return response()->json($suggestions);
}
$searchTerm = $request->get('search', null);
$tagName = $request->get('name', null);
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
+
return response()->json($suggestions);
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Api\ApiToken;
class UserApiTokenController extends Controller
{
-
/**
* Show the form to create a new API token.
*/
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$user = User::query()->findOrFail($userId);
+
return view('users.api-tokens.create', [
'user' => $user,
]);
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$this->validate($request, [
- 'name' => 'required|max:250',
+ 'name' => 'required|max:250',
'expires_at' => 'date_format:Y-m-d',
]);
$secret = Str::random(32);
$token = (new ApiToken())->forceFill([
- 'name' => $request->get('name'),
- 'token_id' => Str::random(32),
- 'secret' => Hash::make($secret),
- 'user_id' => $user->id,
+ 'name' => $request->get('name'),
+ 'token_id' => Str::random(32),
+ 'secret' => Hash::make($secret),
+ 'user_id' => $user->id,
'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
]);
$secret = session()->pull('api-token-secret:' . $token->id, null);
return view('users.api-tokens.edit', [
- 'user' => $user,
- 'token' => $token,
- 'model' => $token,
+ 'user' => $user,
+ 'token' => $token,
+ 'model' => $token,
'secret' => $secret,
]);
}
public function update(Request $request, int $userId, int $tokenId)
{
$this->validate($request, [
- 'name' => 'required|max:250',
+ 'name' => 'required|max:250',
'expires_at' => 'date_format:Y-m-d',
]);
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
$token->fill([
- 'name' => $request->get('name'),
+ 'name' => $request->get('name'),
'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
])->save();
$this->showSuccessNotification(trans('settings.user_api_token_update_success'));
$this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
+
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
}
public function delete(int $userId, int $tokenId)
{
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
+
return view('users.api-tokens.delete', [
- 'user' => $user,
+ 'user' => $user,
'token' => $token,
]);
}
$user = User::query()->findOrFail($userId);
$token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();
+
return [$user, $token];
}
}
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\SocialAuthService;
class UserController extends Controller
{
-
protected $user;
protected $userRepo;
protected $inviteService;
{
$this->checkPermission('users-manage');
$listDetails = [
- 'order' => $request->get('order', 'asc'),
+ 'order' => $request->get('order', 'asc'),
'search' => $request->get('search', ''),
- 'sort' => $request->get('sort', 'name'),
+ 'sort' => $request->get('sort', 'name'),
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
+
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
}
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
+
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Store a newly created user in storage.
+ *
* @throws UserUpdateException
* @throws ValidationException
*/
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
- 'email' => 'required|email|unique:users,email'
+ 'email' => 'required|email|unique:users,email',
];
$authMethod = config('auth.method');
$this->userRepo->downloadAndAssignUserAvatar($user);
$this->logActivity(ActivityType::USER_CREATE, $user);
+
return redirect('/settings/users');
}
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
+
return view('users.edit', [
- 'user' => $user,
+ 'user' => $user,
'activeSocialDrivers' => $activeSocialDrivers,
- 'authMethod' => $authMethod,
- 'roles' => $roles
+ 'authMethod' => $authMethod,
+ 'roles' => $roles,
]);
}
/**
* Update the specified user in storage.
+ *
* @throws UserUpdateException
* @throws ImageUploadException
* @throws ValidationException
$this->logActivity(ActivityType::USER_UPDATE, $user);
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
+
return redirect($redirectUrl);
}
$user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
+
return view('users.delete', ['user' => $user]);
}
/**
* Remove the specified user from storage.
+ *
* @throws Exception
*/
public function destroy(Request $request, int $id)
if ($this->userRepo->isOnlyAdmin($user)) {
$this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
+
return redirect($user->getEditUrl());
}
if ($user->system_name === 'public') {
$this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
+
return redirect($user->getEditUrl());
}
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
+
return $this->changeListSort($id, $request, $type);
}
{
$enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
+
return redirect()->back();
}
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
- return response("Invalid key", 500);
+ return response('Invalid key', 500);
}
$newState = $request->get('expand', 'false');
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
- return response("", 204);
+
+ return response('', 204);
}
/**
-<?php namespace BookStack\Http\Controllers;
+<?php
+
+namespace BookStack\Http\Controllers;
use BookStack\Auth\UserRepo;
class UserProfileController extends Controller
{
/**
- * Show the user profile page
+ * Show the user profile page.
*/
public function show(UserRepo $repo, string $slug)
{
$assetCounts = $repo->getAssetCounts($user);
return view('users.profile', [
- 'user' => $user,
- 'activity' => $userActivity,
+ 'user' => $user,
+ 'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
- 'assetCounts' => $assetCounts
+ 'assetCounts' => $assetCounts,
]);
}
}
}
$users = $query->get();
+
return view('components.user-select-list', compact('users'));
}
}
-<?php namespace BookStack\Http;
+<?php
+
+namespace BookStack\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
/**
* Ensure the current user can access authenticated API routes, either via existing session
* authentication or via API Token authentication.
+ *
* @throws UnauthorizedException
*/
protected function ensureAuthorizedBySessionOrToken(): void
if (!user()->can('access-api')) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
+
return;
}
{
return response()->json([
'error' => [
- 'code' => $code,
+ 'code' => $code,
'message' => $message,
- ]
+ ],
], $code);
}
}
if ($request->wantsJson()) {
return response()->json([
'error' => [
- 'code' => 401,
- 'message' => trans('errors.email_confirmation_awaiting')
- ]
+ 'code' => 401,
+ 'message' => trans('errors.email_confirmation_awaiting'),
+ ],
], 401);
}
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @param string $allowedGuards
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @param string $allowedGuards
+ *
* @return mixed
*/
public function handle($request, Closure $next, ...$allowedGuards)
$activeGuard = config('auth.method');
if (!in_array($activeGuard, $allowedGuards)) {
session()->flash('error', trans('errors.permission'));
+
return redirect('/');
}
namespace BookStack\Http\Middleware;
use BookStack\Exceptions\UnauthorizedException;
-use Illuminate\Http\Request;
trait ChecksForEmailConfirmation
{
/**
* Check if the current user has a confirmed email if the instance deems it as required.
* Throws if confirmation is required by the user.
+ *
* @throws UnauthorizedException
*/
protected function ensureEmailConfirmedIfRequested()
namespace BookStack\Http\Middleware;
use Closure;
-use Symfony\Component\HttpFoundation\Response;
/**
* Sets CSP headers to restrict the hosts that BookStack can be
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
* @return mixed
*/
public function handle($request, Closure $next)
$response = $next($request);
$cspValue = 'frame-ancestors ' . $iframeHosts->join(' ');
$response->headers->set('Content-Security-Policy', $cspValue);
+
return $response;
}
}
-<?php namespace BookStack\Http\Middleware;
+<?php
+
+namespace BookStack\Http\Middleware;
use Carbon\Carbon;
use Closure;
class Localization
{
-
/**
- * Array of right-to-left locales
+ * Array of right-to-left locales.
*/
protected $rtlLocales = ['ar', 'he'];
* Map of BookStack locale names to best-estimate system locale names.
*/
protected $localeMap = [
- 'ar' => 'ar',
- 'bg' => 'bg_BG',
- 'bs' => 'bs_BA',
- 'ca' => 'ca',
- 'da' => 'da_DK',
- 'de' => 'de_DE',
+ 'ar' => 'ar',
+ 'bg' => 'bg_BG',
+ 'bs' => 'bs_BA',
+ 'ca' => 'ca',
+ 'da' => 'da_DK',
+ 'de' => 'de_DE',
'de_informal' => 'de_DE',
- 'en' => 'en_GB',
- 'es' => 'es_ES',
- 'es_AR' => 'es_AR',
- 'fr' => 'fr_FR',
- 'he' => 'he_IL',
- 'hr' => 'hr_HR',
- 'id' => 'id_ID',
- 'it' => 'it_IT',
- 'ja' => 'ja',
- 'ko' => 'ko_KR',
- 'lv' => 'lv_LV',
- 'nl' => 'nl_NL',
- 'nb' => 'nb_NO',
- 'pl' => 'pl_PL',
- 'pt' => 'pt_PT',
- 'pt_BR' => 'pt_BR',
- 'ru' => 'ru',
- 'sk' => 'sk_SK',
- 'sl' => 'sl_SI',
- 'sv' => 'sv_SE',
- 'uk' => 'uk_UA',
- 'vi' => 'vi_VN',
- 'zh_CN' => 'zh_CN',
- 'zh_TW' => 'zh_TW',
- 'tr' => 'tr_TR',
+ 'en' => 'en_GB',
+ 'es' => 'es_ES',
+ 'es_AR' => 'es_AR',
+ 'fr' => 'fr_FR',
+ 'he' => 'he_IL',
+ 'hr' => 'hr_HR',
+ 'id' => 'id_ID',
+ 'it' => 'it_IT',
+ 'ja' => 'ja',
+ 'ko' => 'ko_KR',
+ 'lv' => 'lv_LV',
+ 'nl' => 'nl_NL',
+ 'nb' => 'nb_NO',
+ 'pl' => 'pl_PL',
+ 'pt' => 'pt_PT',
+ 'pt_BR' => 'pt_BR',
+ 'ru' => 'ru',
+ 'sk' => 'sk_SK',
+ 'sl' => 'sl_SI',
+ 'sv' => 'sv_SE',
+ 'uk' => 'uk_UA',
+ 'vi' => 'vi_VN',
+ 'zh_CN' => 'zh_CN',
+ 'zh_TW' => 'zh_TW',
+ 'tr' => 'tr_TR',
];
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
* @return mixed
*/
public function handle($request, Closure $next)
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
+
return $next($request);
}
return $lang;
}
}
+
return $default;
}
/**
- * Get the ISO version of a BookStack language name
+ * Get the ISO version of a BookStack language name.
*/
public function getLocaleIso(string $locale): string
{
namespace BookStack\Http\Middleware;
use Closure;
-use Illuminate\Support\Facades\Session;
class PermissionMiddleware
{
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
- * @param $permission
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @param $permission
+ *
* @return mixed
*/
public function handle($request, Closure $next, $permission)
{
-
if (!$request->user() || !$request->user()->can($permission)) {
session()->flash('error', trans('errors.permission'));
+
return redirect()->back();
}
-<?php namespace BookStack\Http\Middleware;
+<?php
+
+namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
/**
* Create a new filter instance.
*
- * @param Guard $auth
+ * @param Guard $auth
+ *
* @return void
*/
public function __construct(Guard $auth)
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
* @return mixed
*/
public function handle($request, Closure $next)
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ *
* @return mixed
*/
public function handle($request, Closure $next)
$response = $next($request);
$response = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_AFTER, $request, $response) ?? $response;
+
return $response;
}
}
class ThrottleApiRequests extends Middleware
{
-
/**
* Resolve the number of attempts if the user is authenticated or not.
*/
/**
* Handle the request, Set the correct user-configured proxy information.
+ *
* @param Request $request
* @param Closure $next
+ *
* @return mixed
*/
public function handle(Request $request, Closure $next)
* @var array
*/
protected $except = [
- 'saml2/*'
+ 'saml2/*',
];
}
-<?php namespace BookStack\Http;
+<?php
+
+namespace BookStack\Http;
use Illuminate\Http\Request as LaravelRequest;
class Request extends LaravelRequest
{
-
/**
* Override the default request methods to get the scheme and host
* to set the custom APP_URL, if set.
+ *
* @return \Illuminate\Config\Repository|mixed|string
*/
public function getSchemeAndHttpHost()
if ($base) {
$base = trim($base, '/');
} else {
- $base = $this->getScheme().'://'.$this->getHttpHost();
+ $base = $this->getScheme() . '://' . $this->getHttpHost();
}
return $base;
-<?php namespace BookStack\Interfaces;
+<?php
+
+namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
* Get the related favourite instances.
*/
public function favourites(): MorphMany;
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Interfaces;
+<?php
+
+namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Builder;
/**
- * Interface Sluggable
+ * Interface Sluggable.
*
* Assigned to models that can have slugs.
* Must have the below properties.
*
- * @property int $id
+ * @property int $id
* @property string $name
+ *
* @method Builder newQuery
*/
interface Sluggable
{
-
/**
* Regenerate the slug for this model.
*/
public function refreshSlug(): string;
-
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Interfaces;
+<?php
+
+namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
* Get all view instances for this viewable model.
*/
public function views(): MorphMany;
-}
\ No newline at end of file
+}
-<?php namespace BookStack;
+<?php
+
+namespace BookStack;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends EloquentModel
{
-
/**
* Provides public access to get the raw attribute value from the model.
* Used in areas where no mutations are required but performance is critical.
+ *
* @param $key
+ *
* @return mixed
*/
public function getRawAttribute($key)
-<?php namespace BookStack\Notifications;
+<?php
+
+namespace BookStack\Notifications;
class ConfirmEmail extends MailNotification
{
/**
* Create a new notification instance.
+ *
* @param string $token
*/
public function __construct($token)
/**
* Get the mail representation of the notification.
*
- * @param mixed $notifiable
+ * @param mixed $notifiable
+ *
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$appName = ['appName' => setting('app-name')];
+
return $this->newMailMessage()
->subject(trans('auth.email_confirm_subject', $appName))
->greeting(trans('auth.email_confirm_greeting', $appName))
-<?php namespace BookStack\Notifications;
+<?php
+
+namespace BookStack\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
/**
* Get the notification's channels.
*
- * @param mixed $notifiable
+ * @param mixed $notifiable
+ *
* @return array|string
*/
public function via($notifiable)
/**
* Create a new mail message.
+ *
* @return MailMessage
*/
protected function newMailMessage()
{
- return (new MailMessage)->view([
+ return (new MailMessage())->view([
'html' => 'vendor.notifications.email',
- 'text' => 'vendor.notifications.email-plain'
+ 'text' => 'vendor.notifications.email-plain',
]);
}
}
-<?php namespace BookStack\Notifications;
+<?php
+
+namespace BookStack\Notifications;
class ResetPassword extends MailNotification
{
/**
* Create a notification instance.
*
- * @param string $token
+ * @param string $token
*/
public function __construct($token)
{
*/
public function toMail()
{
- return $this->newMailMessage()
+ return $this->newMailMessage()
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
->line(trans('auth.email_reset_text'))
->action(trans('auth.reset_password'), url('password/reset/' . $this->token))
-<?php namespace BookStack\Notifications;
+<?php
+
+namespace BookStack\Notifications;
class TestEmail extends MailNotification
{
/**
* Get the mail representation of the notification.
*
- * @param mixed $notifiable
+ * @param mixed $notifiable
+ *
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
-<?php namespace BookStack\Notifications;
+<?php
+
+namespace BookStack\Notifications;
class UserInvite extends MailNotification
{
/**
* Create a new notification instance.
+ *
* @param string $token
*/
public function __construct($token)
/**
* Get the mail representation of the notification.
*
- * @param mixed $notifiable
+ * @param mixed $notifiable
+ *
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$appName = ['appName' => setting('app-name')];
+
return $this->newMailMessage()
->subject(trans('auth.user_invite_email_subject', $appName))
->greeting(trans('auth.user_invite_email_greeting', $appName))
-<?php namespace BookStack\Providers;
+<?php
+
+namespace BookStack\Providers;
use Blade;
use BookStack\Auth\Access\SocialAuthService;
+use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Settings\Setting;
// Set morph-map due to namespace changes
Relation::morphMap([
'BookStack\\Bookshelf' => Bookshelf::class,
- 'BookStack\\Book' => Book::class,
- 'BookStack\\Chapter' => Chapter::class,
- 'BookStack\\Page' => Page::class,
+ 'BookStack\\Book' => Book::class,
+ 'BookStack\\Chapter' => Chapter::class,
+ 'BookStack\\Page' => Page::class,
]);
// View Composers
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
});
- $this->app->singleton(SocialAuthService::class, function($app) {
+ $this->app->singleton(SocialAuthService::class, function ($app) {
return new SocialAuthService($app->make(SocialiteFactory::class));
});
}
use BookStack\Auth\Access\Guards\Saml2SessionGuard;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Access\RegistrationService;
-use BookStack\Auth\UserRepo;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
Auth::extend('ldap-session', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
+
return new LdapSessionGuard(
$name,
$provider,
Auth::extend('saml2-session', function ($app, $name, array $config) {
$provider = Auth::createUserProvider($config['provider']);
+
return new Saml2SessionGuard(
$name,
$provider,
class CustomValidationServiceProvider extends ServiceProvider
{
-
/**
* Register our custom validation rules when the application boots.
*/
{
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
+
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
});
$cleanLinkName = strtolower(trim($value));
$isJs = strpos($cleanLinkName, 'javascript:') === 0;
$isData = strpos($cleanLinkName, 'data:') === 0;
+
return !$isJs && !$isData;
});
}
-<?php namespace BookStack\Providers;
+<?php
+
+namespace BookStack\Providers;
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
use Illuminate\Pagination\Paginator;
class PaginationServiceProvider extends IlluminatePaginationServiceProvider
{
-
/**
* Register the service provider.
*
$this->mapWebRoutes();
$this->mapApiRoutes();
}
+
/**
* Define the "web" routes for the application.
*
{
Route::group([
'middleware' => 'web',
- 'namespace' => $this->namespace,
+ 'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
+
/**
* Define the "api" routes for the application.
*
{
Route::group([
'middleware' => 'api',
- 'namespace' => $this->namespace . '\Api',
- 'prefix' => 'api',
+ 'namespace' => $this->namespace . '\Api',
+ 'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
});
public function register()
{
$this->app->singleton(ThemeService::class, function ($app) {
- return new ThemeService;
+ return new ThemeService();
});
}
-<?php namespace BookStack\Providers;
+<?php
+
+namespace BookStack\Providers;
use BookStack\Translation\FileLoader;
use Illuminate\Translation\TranslationServiceProvider as BaseProvider;
class TranslationServiceProvider extends BaseProvider
{
-
/**
* Register the translation line loader.
* Overrides the default register action from Laravel so a custom loader can be used.
+ *
* @return void
*/
protected function registerLoader()
-<?php namespace BookStack\Settings;
+<?php
+
+namespace BookStack\Settings;
use BookStack\Model;
-<?php namespace BookStack\Settings;
+<?php
+
+namespace BookStack\Settings;
use BookStack\Auth\User;
use Illuminate\Contracts\Cache\Repository as Cache;
$value = $this->getValueFromStore($key) ?? $default;
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
+
return $formatted;
}
protected function getFromSession(string $key, $default = false)
{
$value = session()->get($key, $default);
+
return $this->formatValue($value, $default);
}
if ($user->isDefault()) {
return $this->getFromSession($key, $default);
}
+
return $this->get($this->userKey($user->id, $key), $default);
}
}
$this->cache->forever($cacheKey, $value);
+
return $value;
}
}
/**
- * Format a settings value
+ * Format a settings value.
*/
protected function formatValue($value, $default)
{
// Change string booleans to actual booleans
if ($value === 'true') {
$value = true;
- } else if ($value === 'false') {
+ } elseif ($value === 'false') {
$value = false;
}
if ($value === '') {
$value = $default;
}
+
return $value;
}
public function has(string $key): bool
{
$setting = $this->getSettingObjectByKey($key);
+
return $setting !== null;
}
public function put(string $key, $value): bool
{
$setting = $this->setting->newQuery()->firstOrNew([
- 'setting_key' => $key
+ 'setting_key' => $key,
]);
$setting->type = 'string';
$setting->value = $value;
$setting->save();
$this->clearFromCache($key);
+
return true;
}
$values = collect($value)->values()->filter(function (array $item) {
return count(array_filter($item)) > 0;
});
+
return json_encode($values);
}
{
if ($user->isDefault()) {
session()->put($key, $value);
+
return true;
}
-<?php namespace BookStack\Theming;
+<?php
+
+namespace BookStack\Theming;
/**
* The ThemeEvents used within BookStack.
/**
* Application boot-up.
* After main services are registered.
+ *
* @param \BookStack\Application $app
*/
const APP_BOOT = 'app_boot';
* that depend on the current session user (Localization for example).
* Provides the original request to use.
* Return values, if provided, will be used as a new response to use.
+ *
* @param \Illuminate\Http\Request $request
* @returns \Illuminate\Http\Response|null
*/
* Runs after the request is handled but before the response is sent.
* Provides both the original request and the currently resolved response.
* Return values, if provided, will be used as a new response to use.
+ *
* @param \Illuminate\Http\Request $request
* @returns \Illuminate\Http\Response|null
*/
* Runs right after a user is logged-in to the application by any authentication
* system as a standard app user. This includes a user becoming logged in
* after registration. This is not emitted upon API usage.
- * @param string $authSystem
+ *
+ * @param string $authSystem
* @param \BookStack\Auth\User $user
*/
const AUTH_LOGIN = 'auth_login';
* Runs right after a user is newly registered to the application by any authentication
* system as a standard app user. This includes auto-registration systems used
* by LDAP, SAML and social systems. It only includes self-registrations.
- * @param string $authSystem
+ *
+ * @param string $authSystem
* @param \BookStack\Auth\User $user
*/
const AUTH_REGISTER = 'auth_register';
* Provides the commonmark library environment for customization
* before its used to render markdown content.
* If the listener returns a non-null value, that will be used as an environment instead.
+ *
* @param \League\CommonMark\ConfigurableEnvironmentInterface $environment
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
*/
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Theming;
+<?php
+
+namespace BookStack\Theming;
use BookStack\Auth\Access\SocialAuthService;
*
* If a callback returns a non-null value, this method will
* stop and return that value itself.
+ *
* @return mixed
*/
public function dispatch(string $event, ...$args)
return $result;
}
}
+
return null;
}
$socialAuthService = app()->make(SocialAuthService::class);
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
}
-}
\ No newline at end of file
+}
-<?php namespace BookStack\Traits;
+<?php
+
+namespace BookStack\Traits;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-<?php namespace BookStack\Traits;
+<?php
+
+namespace BookStack\Traits;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-<?php namespace BookStack\Translation;
+<?php
+
+namespace BookStack\Translation;
use Illuminate\Translation\FileLoader as BaseLoader;
* Load the messages for the given locale.
* Extends Laravel's translation FileLoader to look in multiple directories
* so that we can load in translation overrides from the theme file if wanted.
- * @param string $locale
- * @param string $group
- * @param string|null $namespace
+ *
+ * @param string $locale
+ * @param string $group
+ * @param string|null $namespace
+ *
* @return array
*/
public function load($locale, $group, $namespace = null)
if (is_null($namespace) || $namespace === '*') {
$themeTranslations = $this->loadPath(theme_path('lang'), $locale, $group);
- $originalTranslations = $this->loadPath($this->path, $locale, $group);
+ $originalTranslations = $this->loadPath($this->path, $locale, $group);
+
return array_merge($originalTranslations, $themeTranslations);
}
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Entities\Models\Page;
use BookStack\Model;
/**
* Get the downloadable file name for this upload.
+ *
* @return mixed|string
*/
public function getFileName()
if (strpos($this->name, '.') !== false) {
return $this->name;
}
+
return $this->name . '.' . $this->extension;
}
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
+
return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));
}
*/
public function htmlLink(): string
{
- return '<a target="_blank" href="'.e($this->getUrl()).'">'.e($this->name).'</a>';
+ return '<a target="_blank" href="' . e($this->getUrl()) . '">' . e($this->name) . '</a>';
}
/**
*/
public function markdownLink(): string
{
- return '['. $this->name .']('. $this->getUrl() .')';
+ return '[' . $this->name . '](' . $this->getUrl() . ')';
}
}
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Exceptions\FileUploadException;
use Exception;
class AttachmentService
{
-
protected $fileSystem;
/**
$this->fileSystem = $fileSystem;
}
-
/**
* Get the storage that will be used for storing files.
*/
/**
* Get an attachment from storage.
+ *
* @throws FileNotFoundException
*/
public function getAttachmentFromStorage(Attachment $attachment): string
/**
* Store a new attachment upon user upload.
+ *
* @param UploadedFile $uploadedFile
- * @param int $page_id
- * @return Attachment
+ * @param int $page_id
+ *
* @throws FileUploadException
+ *
+ * @return Attachment
*/
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
- 'name' => $attachmentName,
- 'path' => $attachmentPath,
- 'extension' => $uploadedFile->getClientOriginalExtension(),
+ 'name' => $attachmentName,
+ 'path' => $attachmentPath,
+ 'extension' => $uploadedFile->getClientOriginalExtension(),
'uploaded_to' => $page_id,
- 'created_by' => user()->id,
- 'updated_by' => user()->id,
- 'order' => $largestExistingOrder + 1
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ 'order' => $largestExistingOrder + 1,
]);
return $attachment;
/**
* Store a upload, saving to a file and deleting any existing uploads
* attached to that file.
+ *
* @param UploadedFile $uploadedFile
- * @param Attachment $attachment
- * @return Attachment
+ * @param Attachment $attachment
+ *
* @throws FileUploadException
+ *
+ * @return Attachment
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
{
$attachment->external = false;
$attachment->extension = $uploadedFile->getClientOriginalExtension();
$attachment->save();
+
return $attachment;
}
public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
+
return Attachment::forceCreate([
- 'name' => $name,
- 'path' => $link,
- 'external' => true,
- 'extension' => '',
+ 'name' => $name,
+ 'path' => $link,
+ 'external' => true,
+ 'extension' => '',
'uploaded_to' => $page_id,
- 'created_by' => user()->id,
- 'updated_by' => user()->id,
- 'order' => $largestExistingOrder + 1
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ 'order' => $largestExistingOrder + 1,
]);
}
}
}
-
/**
* Update the details of a file.
*/
}
$attachment->save();
+
return $attachment;
}
/**
* Delete a File from the database and storage.
+ *
* @param Attachment $attachment
+ *
* @throws Exception
*/
public function deleteFile(Attachment $attachment)
{
if ($attachment->external) {
$attachment->delete();
+
return;
}
-
+
$this->deleteFileInStorage($attachment);
$attachment->delete();
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
+ *
* @param Attachment $attachment
*/
protected function deleteFileInStorage(Attachment $attachment)
}
/**
- * Store a file in storage with the given filename
+ * Store a file in storage with the given filename.
+ *
* @param UploadedFile $uploadedFile
- * @return string
+ *
* @throws FileUploadException
+ *
+ * @return string
*/
protected function putFileInStorage(UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
- $basePath = 'uploads/files/' . Date('Y-m-M') . '/';
+ $basePath = 'uploads/files/' . date('Y-m-M') . '/';
$uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
}
$attachmentPath = $basePath . $uploadFileName;
+
try {
$storage->put($attachmentPath, $attachmentData);
} catch (Exception $e) {
Log::error('Error when attempting file upload:' . $e->getMessage());
+
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
}
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Exceptions\HttpFetchException;
class HttpFetcher
{
-
/**
* Fetch content from an external URI.
+ *
* @param string $uri
- * @return bool|string
+ *
* @throws HttpFetchException
+ *
+ * @return bool|string
*/
public function fetch(string $uri)
{
$ch = curl_init();
curl_setopt_array($ch, [
- CURLOPT_URL => $uri,
+ CURLOPT_URL => $uri,
CURLOPT_RETURNTRANSFER => 1,
- CURLOPT_CONNECTTIMEOUT => 5
+ CURLOPT_CONNECTTIMEOUT => 5,
]);
$data = curl_exec($ch);
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
/**
- * @property int $id
+ * @property int $id
* @property string $name
* @property string $url
* @property string $path
* @property string $type
- * @property int $uploaded_to
- * @property int $created_by
- * @property int $updated_by
+ * @property int $uploaded_to
+ * @property int $created_by
+ * @property int $updated_by
*/
class Image extends Model
{
/**
* Get a thumbnail for this image.
+ *
* @throws \Exception
*/
public function getThumb(int $width, int $height, bool $keepRatio = false): string
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Page;
class ImageRepo
{
-
protected $image;
protected $imageService;
protected $restrictionService;
$this->page = $page;
}
-
/**
* Get an image with the given id.
*/
});
return [
- 'images' => $returnImages,
- 'has_more' => $hasMore
+ 'images' => $returnImages,
+ 'has_more' => $hasMore,
];
}
/**
* Save a new image into storage and return the new image.
+ *
* @throws ImageUploadException
*/
public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
+
return $image;
}
/**
* Save a new image from an existing image data string.
+ *
* @throws ImageUploadException
*/
public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0)
{
$image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
$this->loadThumbs($image);
+
return $image;
}
/**
* Save a drawing the the database.
+ *
* @throws ImageUploadException
*/
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
{
$name = 'Drawing-' . strval(user()->id) . '-' . strval(time()) . '.png';
+
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
}
-
/**
* Update the details of an image via an array of properties.
+ *
* @throws ImageUploadException
* @throws Exception
*/
$image->fill($updateDetails);
$image->save();
$this->loadThumbs($image);
+
return $image;
}
/**
* Destroys an Image object along with its revisions, files and thumbnails.
+ *
* @throws Exception
*/
public function destroyImage(Image $image = null): bool
if ($image) {
$this->imageService->destroy($image);
}
+
return true;
}
/**
* Destroy all images of a certain type.
+ *
* @throws Exception
*/
public function destroyByType(string $imageType)
}
}
-
/**
* Load thumbnails onto an image object.
+ *
* @throws Exception
*/
public function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150, false),
- 'display' => $this->getThumbnail($image, 1680, null, true)
+ 'display' => $this->getThumbnail($image, 1680, null, true),
];
}
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
+ *
* @throws Exception
*/
protected function getThumbnail(Image $image, ?int $width = 220, ?int $height = 220, bool $keepRatio = false): ?string
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Exceptions\ImageUploadException;
use DB;
use Exception;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
-use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
+use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
/**
* Saves a new image from an upload.
- * @return mixed
+ *
* @throws ImageUploadException
+ *
+ * @return mixed
*/
public function saveNewFromUpload(
UploadedFile $uploadedFile,
/**
* Save a new image from a uri-encoded base64 string of data.
+ *
* @throws ImageUploadException
*/
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, int $uploadedTo = 0): Image
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
- throw new ImageUploadException("Invalid base64 image data provided");
+ throw new ImageUploadException('Invalid base64 image data provided');
}
$data = base64_decode($splitData[1]);
+
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/**
* Save a new image into storage.
+ *
* @throws ImageUploadException
*/
public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
$secureUploads = setting('app-secure-images');
$fileName = $this->cleanImageFileName($imageName);
- $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
+ $imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/';
while ($storage->exists($imagePath . $fileName)) {
$fileName = Str::random(3) . $fileName;
$this->saveImageDataInPublicSpace($storage, $fullPath, $imageData);
} catch (Exception $e) {
\Log::error('Error when attempting image upload:' . $e->getMessage());
+
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
}
$imageDetails = [
- 'name' => $imageName,
- 'path' => $fullPath,
- 'url' => $this->getPublicUrl($fullPath),
- 'type' => $type,
- 'uploaded_to' => $uploadedTo
+ 'name' => $imageName,
+ 'path' => $fullPath,
+ 'url' => $this->getPublicUrl($fullPath),
+ 'type' => $type,
+ 'uploaded_to' => $uploadedTo,
];
if (user()->id !== 0) {
$image = $this->image->newInstance();
$image->forceFill($imageDetails)->save();
+
return $image;
}
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
+ *
* @param Image $image
- * @param int $width
- * @param int $height
- * @param bool $keepRatio
- * @return string
+ * @param int $width
+ * @param int $height
+ * @param bool $keepRatio
+ *
* @throws Exception
* @throws ImageUploadException
+ *
+ * @return string
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
$this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData);
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
-
return $this->getPublicUrl($thumbFilePath);
}
/**
* Resize image data.
+ *
* @param string $imageData
- * @param int $width
- * @param int $height
- * @param bool $keepRatio
- * @return string
+ * @param int $width
+ * @param int $height
+ * @param bool $keepRatio
+ *
* @throws ImageUploadException
+ *
+ * @return string
*/
protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
{
if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
}
+
throw $e;
}
$thumb->fit($width, $height);
}
- $thumbData = (string)$thumb->encode();
+ $thumbData = (string) $thumb->encode();
// Use original image data if we're keeping the ratio
// and the resizing does not save any space.
/**
* Get the raw data content from an image.
+ *
* @throws FileNotFoundException
*/
public function getImageData(Image $image): string
{
$imagePath = $image->path;
$storage = $this->getStorage();
+
return $storage->get($imagePath);
}
/**
* Destroy an image along with its revisions, thumbnails and remaining folders.
+ *
* @throws Exception
*/
public function destroy(Image $image)
{
$files = $storage->files($path);
$folders = $storage->directories($path);
- return (count($files) === 0 && count($folders) === 0);
+
+ return count($files) === 0 && count($folders) === 0;
}
/**
}
}
});
+
return $deletedPaths;
}
* Attempts to convert the URL to a system storage url then
* fetch the data from the disk or storage location.
* Returns null if the image data cannot be fetched from storage.
+ *
* @throws FileNotFoundException
*/
public function imageUriToBase64(string $uri): ?string
if (strpos(strtolower($url), 'uploads/images') === 0) {
return trim($url, '/');
}
+
return null;
}
}
$basePath = ($this->storageUrl == false) ? url('/') : $this->storageUrl;
+
return rtrim($basePath, '/') . $filePath;
}
}
-<?php namespace BookStack\Uploads;
+<?php
+
+namespace BookStack\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
/**
* Save an avatar image from an external service.
+ *
* @throws Exception
*/
protected function saveAvatarImage(User $user, int $size = 500): Image
$email = strtolower(trim($user->email));
$replacements = [
- '${hash}' => md5($email),
- '${size}' => $size,
+ '${hash}' => md5($email),
+ '${size}' => $size,
'${email}' => urlencode($email),
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageData = $this->getAvatarImageData($userAvatarUrl);
+
return $this->createAvatarImageFromData($user, $imageData, 'png');
}
/**
* Gets an image from url and returns it as a string of image data.
+ *
* @throws Exception
*/
protected function getAvatarImageData(string $url): string
} catch (HttpFetchException $exception) {
throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
+
return $imageData;
}
protected function avatarFetchEnabled(): bool
{
$fetchUrl = $this->getAvatarUrl();
+
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
}
-<?php namespace BookStack\Util;
+<?php
+
+namespace BookStack\Util;
use DOMDocument;
use DOMNodeList;
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
- /** @var \DOMAttr $attr*/
+ /** @var \DOMAttr $attr */
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
/**
* Get the path to a versioned file.
+ *
* @throws Exception
*/
function versioned_asset(string $file = ''): string
}
$path = $file . '?version=' . urlencode($version) . $additional;
+
return url($path);
}
// Check permission on ownable item
$permissionService = app(PermissionService::class);
+
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
function userCanOnAny(string $permission, string $entityClass = null): bool
{
$permissionService = app(PermissionService::class);
+
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
}
/**
* Helper to access system settings.
+ *
* @return mixed|SettingService
*/
function setting(string $key = null, $default = null)
return '';
}
- return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path));
+ return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path));
}
/**
], $attrs);
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
- $attrString .= $attrName . '="' . $attr . '" ';
+ $attrString .= $attrName . '="' . $attr . '" ';
}
$iconPath = resource_path('icons/' . $name . '.svg');
if ($themeIconPath && file_exists($themeIconPath)) {
$iconPath = $themeIconPath;
- } else if (!file_exists($iconPath)) {
+ } elseif (!file_exists($iconPath)) {
return '';
}
$fileContents = file_get_contents($iconPath);
+
return str_replace('<svg', '<svg' . $attrString, $fileContents);
}
$factory->define(\BookStack\Auth\User::class, function ($faker) {
$name = $faker->name;
+
return [
- 'name' => $name,
- 'email' => $faker->email,
- 'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
- 'password' => Str::random(10),
- 'remember_token' => Str::random(10),
- 'email_confirmed' => 1
+ 'name' => $name,
+ 'email' => $faker->email,
+ 'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
+ 'password' => Str::random(10),
+ 'remember_token' => Str::random(10),
+ 'email_confirmed' => 1,
];
});
$factory->define(\BookStack\Entities\Models\Bookshelf::class, function ($faker) {
return [
- 'name' => $faker->sentence,
- 'slug' => Str::random(10),
- 'description' => $faker->paragraph
+ 'name' => $faker->sentence,
+ 'slug' => Str::random(10),
+ 'description' => $faker->paragraph,
];
});
$factory->define(\BookStack\Entities\Models\Book::class, function ($faker) {
return [
- 'name' => $faker->sentence,
- 'slug' => Str::random(10),
- 'description' => $faker->paragraph
+ 'name' => $faker->sentence,
+ 'slug' => Str::random(10),
+ 'description' => $faker->paragraph,
];
});
$factory->define(\BookStack\Entities\Models\Chapter::class, function ($faker) {
return [
- 'name' => $faker->sentence,
- 'slug' => Str::random(10),
- 'description' => $faker->paragraph
+ 'name' => $faker->sentence,
+ 'slug' => Str::random(10),
+ 'description' => $faker->paragraph,
];
});
$factory->define(\BookStack\Entities\Models\Page::class, function ($faker) {
$html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
+
return [
- 'name' => $faker->sentence,
- 'slug' => Str::random(10),
- 'html' => $html,
- 'text' => strip_tags($html),
- 'revision_count' => 1
+ 'name' => $faker->sentence,
+ 'slug' => Str::random(10),
+ 'html' => $html,
+ 'text' => strip_tags($html),
+ 'revision_count' => 1,
];
});
$factory->define(\BookStack\Auth\Role::class, function ($faker) {
return [
'display_name' => $faker->sentence(3),
- 'description' => $faker->sentence(10)
+ 'description' => $faker->sentence(10),
];
});
$factory->define(\BookStack\Actions\Tag::class, function ($faker) {
return [
- 'name' => $faker->city,
- 'value' => $faker->sentence(3)
+ 'name' => $faker->city,
+ 'value' => $faker->sentence(3),
];
});
$factory->define(\BookStack\Uploads\Image::class, function ($faker) {
return [
- 'name' => $faker->slug . '.jpg',
- 'url' => $faker->url,
- 'path' => $faker->url,
- 'type' => 'gallery',
- 'uploaded_to' => 0
+ 'name' => $faker->slug . '.jpg',
+ 'url' => $faker->url,
+ 'path' => $faker->url,
+ 'type' => 'gallery',
+ 'uploaded_to' => 0,
];
});
-$factory->define(\BookStack\Actions\Comment::class, function($faker) {
+$factory->define(\BookStack\Actions\Comment::class, function ($faker) {
$text = $faker->paragraph(1);
- $html = '<p>' . $text. '</p>';
+ $html = '<p>' . $text . '</p>';
+
return [
- 'html' => $html,
- 'text' => $text,
- 'parent_id' => null
+ 'html' => $html,
+ 'text' => $text,
+ 'parent_id' => null,
];
-});
\ No newline at end of file
+});
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateUsersTable extends Migration
{
// Create the initial admin user
DB::table('users')->insert([
- 'name' => 'Admin',
- 'password' => bcrypt('password'),
+ 'name' => 'Admin',
+ 'password' => bcrypt('password'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreatePasswordResetsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateBooksTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreatePagesTable extends Migration
{
*/
public function up()
{
-
-
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->integer('book_id');
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateImagesTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateChaptersTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddUsersToEntities extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreatePageRevisionsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateActivitiesTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
/**
* Much of this code has been taken from entrust,
* Full attribution of the database Schema shown below goes to the entrust project.
*
* @license MIT
- * @package Zizaco\Entrust
* @url https://github.com/Zizaco/entrust
*/
class AddRolesAndPermissions extends Migration
$table->primary(['permission_id', 'role_id']);
});
-
// Create default roles
$adminId = DB::table('roles')->insertGetId([
- 'name' => 'admin',
+ 'name' => 'admin',
'display_name' => 'Admin',
- 'description' => 'Administrator of the whole application',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'description' => 'Administrator of the whole application',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
$editorId = DB::table('roles')->insertGetId([
- 'name' => 'editor',
+ 'name' => 'editor',
'display_name' => 'Editor',
- 'description' => 'User can edit Books, Chapters & Pages',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'description' => 'User can edit Books, Chapters & Pages',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
$viewerId = DB::table('roles')->insertGetId([
- 'name' => 'viewer',
+ 'name' => 'viewer',
'display_name' => 'Viewer',
- 'description' => 'User can view books & their content behind authentication',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'description' => 'User can view books & their content behind authentication',
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
-
// Create default CRUD permissions and allocate to admins and editors
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower($op),
+ 'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
['permission_id' => $newPermId, 'role_id' => $adminId],
- ['permission_id' => $newPermId, 'role_id' => $editorId]
+ ['permission_id' => $newPermId, 'role_id' => $editorId],
]);
}
}
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower($op),
+ 'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity,
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
'permission_id' => $newPermId,
- 'role_id' => $adminId
+ 'role_id' => $adminId,
]);
}
}
foreach ($users as $user) {
DB::table('role_user')->insert([
'role_id' => $adminId,
- 'user_id' => $user->id
+ 'user_id' => $user->id,
]);
}
-
}
/**
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateSettingsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddSearchIndexes extends Migration
{
$chapters = $sm->listTableDetails('chapters');
if ($pages->hasIndex('search')) {
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('search');
});
}
if ($books->hasIndex('search')) {
- Schema::table('books', function(Blueprint $table) {
+ Schema::table('books', function (Blueprint $table) {
$table->dropIndex('search');
});
}
if ($chapters->hasIndex('search')) {
- Schema::table('chapters', function(Blueprint $table) {
+ Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('search');
});
}
-
}
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateSocialAccountsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddEmailConfirmationTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateViewsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddEntityIndexes extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class FulltextWeighting extends Migration
{
$chapters = $sm->listTableDetails('chapters');
if ($pages->hasIndex('name_search')) {
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('name_search');
});
}
if ($books->hasIndex('name_search')) {
- Schema::table('books', function(Blueprint $table) {
+ Schema::table('books', function (Blueprint $table) {
$table->dropIndex('name_search');
});
}
if ($chapters->hasIndex('name_search')) {
- Schema::table('chapters', function(Blueprint $table) {
+ Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('name_search');
});
}
<?php
use BookStack\Uploads\Image;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddImageUploadTypes extends Migration
{
$table->string('type')->index();
});
- Image::all()->each(function($image) {
+ Image::all()->each(function ($image) {
$image->path = $image->url;
$image->type = 'gallery';
$image->save();
$table->dropColumn('type');
$table->dropColumn('path');
});
-
}
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddUserAvatars extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddExternalAuthToUsers extends Migration
{
$table->dropColumn('external_auth_id');
});
}
-}
\ No newline at end of file
+}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddSlugToRevisions extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
// Create & attach new admin permissions
$permissionsToCreate = [
- 'settings-manage' => 'Manage Settings',
- 'users-manage' => 'Manage Users',
- 'user-roles-manage' => 'Manage Roles & Permissions',
+ 'settings-manage' => 'Manage Settings',
+ 'users-manage' => 'Manage Users',
+ 'user-roles-manage' => 'Manage Roles & Permissions',
'restrictions-manage-all' => 'Manage All Entity Permissions',
- 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
+ 'restrictions-manage-own' => 'Manage Entity Permissions On Own Content',
];
foreach ($permissionsToCreate as $name => $displayName) {
$permissionId = DB::table('permissions')->insertGetId([
- 'name' => $name,
+ 'name' => $name,
'display_name' => $displayName,
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
+ 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
if ($editorRole !== null) {
DB::table('permission_role')->insert([
- 'role_id' => $editorRole->id,
- 'permission_id' => $permissionId
+ 'role_id' => $editorRole->id,
+ 'permission_id' => $permissionId,
]);
}
}
}
-
}
/**
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower($op),
+ 'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
}
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower($op),
+ 'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity,
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddEntityAccessControls extends Migration
{
$table->index('restricted');
});
- Schema::create('restrictions', function(Blueprint $table) {
+ Schema::create('restrictions', function (Blueprint $table) {
$table->increments('id');
$table->integer('restrictable_id');
$table->string('restrictable_type');
$table->dropColumn('restricted');
});
-
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('restricted');
});
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddPageRevisionTypes extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddPageDrafts extends Migration
{
*/
public function up()
{
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->boolean('draft')->default(false);
$table->index('draft');
});
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class AddMarkdownSupport extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddViewPermissionsToRoles extends Migration
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permId = DB::table('permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
+ 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
// Assign view permission to all current roles
foreach ($currentRoles as $role) {
DB::table('permission_role')->insert([
- 'role_id' => $role->id,
- 'permission_id' => $permId
+ 'role_id' => $role->id,
+ 'permission_id' => $permId,
]);
}
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateJointPermissionsTable extends Migration
{
// Create the new public role
$publicRoleData = [
- 'name' => 'public',
+ 'name' => 'public',
'display_name' => 'Public',
- 'description' => 'The role given to public visitors if allowed',
- 'system_name' => 'public',
- 'hidden' => true,
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'description' => 'The role given to public visitors if allowed',
+ 'system_name' => 'public',
+ 'hidden' => true,
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
];
// Ensure unique name
// Assign view permission to public
DB::table('permission_role')->insert([
'permission_id' => $permission->id,
- 'role_id' => $publicRoleId
+ 'role_id' => $publicRoleId,
]);
}
}
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
class CreateTagsTable extends Migration
{
<?php
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSummaryToPageRevisions extends Migration
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class RemoveHiddenRoles extends Migration
{
public function up()
{
// Remove the hidden property from roles
- Schema::table('roles', function(Blueprint $table) {
+ Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('hidden');
});
// Add column to mark system users
- Schema::table('users', function(Blueprint $table) {
+ Schema::table('users', function (Blueprint $table) {
$table->string('system_name')->nullable()->index();
});
// Insert our new public system user.
$publicUserId = DB::table('users')->insertGetId([
- 'name' => 'Guest',
- 'system_name' => 'public',
+ 'name' => 'Guest',
+ 'system_name' => 'public',
'email_confirmed' => true,
- 'created_at' => \Carbon\Carbon::now(),
- 'updated_at' => \Carbon\Carbon::now(),
+ 'created_at' => \Carbon\Carbon::now(),
+ 'updated_at' => \Carbon\Carbon::now(),
]);
-
+
// Get the public role
$publicRole = DB::table('roles')->where('system_name', '=', 'public')->first();
// Connect the new public user to the public role
DB::table('role_user')->insert([
'user_id' => $publicUserId,
- 'role_id' => $publicRole->id
+ 'role_id' => $publicRole->id,
]);
}
*/
public function down()
{
- Schema::table('roles', function(Blueprint $table) {
+ Schema::table('roles', function (Blueprint $table) {
$table->boolean('hidden')->default(false);
$table->index('hidden');
});
DB::table('users')->where('system_name', '=', 'public')->delete();
- Schema::table('users', function(Blueprint $table) {
+ Schema::table('users', function (Blueprint $table) {
$table->dropColumn('system_name');
});
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateAttachmentsTable extends Migration
{
$entity = 'Attachment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
+ 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
-
}
/**
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateCacheTable extends Migration
{
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateSessionsTable extends Migration
{
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateSearchIndexTable extends Migration
{
$chapters = $sm->listTableDetails('chapters');
if ($pages->hasIndex('search')) {
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
}
if ($books->hasIndex('search')) {
- Schema::table('books', function(Blueprint $table) {
+ Schema::table('books', function (Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
}
if ($chapters->hasIndex('search')) {
- Schema::table('chapters', function(Blueprint $table) {
+ Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
// DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
-
+
Schema::dropIfExists('search_terms');
}
}
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class AddRevisionCounts extends Migration
{
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateCommentsTable extends Migration
{
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
- 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
+ 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
-
});
}
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class AddCoverImageDisplay extends Migration
{
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class AddRoleExternalAuthId extends Migration
{
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class CreateBookshelvesTable extends Migration
{
DB::statement("ALTER TABLE {$prefix}pages ENGINE = InnoDB;");
DB::statement("ALTER TABLE {$prefix}chapters ENGINE = InnoDB;");
DB::statement("ALTER TABLE {$prefix}books ENGINE = InnoDB;");
- } catch (Exception $exception) {}
+ } catch (Exception $exception) {
+ }
// Here we have table drops before the creations due to upgrade issues
// people were having due to the bookshelves_books table creation failing.
->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
$permId = DB::table('role_permissions')->insertGetId([
- 'name' => 'bookshelf-' . $dbOpName,
+ 'name' => 'bookshelf-' . $dbOpName,
'display_name' => $op . ' ' . 'BookShelves',
- 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
- 'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
+ 'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
+ 'updated_at' => \Carbon\Carbon::now()->toDateTimeString(),
]);
- $rowsToInsert = $roleIdsWithBookPermission->filter(function($roleId) {
+ $rowsToInsert = $roleIdsWithBookPermission->filter(function ($roleId) {
return !is_null($roleId);
- })->map(function($roleId) use ($permId) {
+ })->map(function ($roleId) use ($permId) {
return [
- 'role_id' => $roleId,
- 'permission_id' => $permId
+ 'role_id' => $roleId,
+ 'permission_id' => $permId,
];
})->toArray();
public function down()
{
// Drop created permissions
- $ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
+ $ops = ['bookshelf-create-all', 'bookshelf-create-own', 'bookshelf-delete-all', 'bookshelf-delete-own', 'bookshelf-update-all', 'bookshelf-update-own', 'bookshelf-view-all', 'bookshelf-view-own'];
$permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
->get(['id'])->pluck('id')->toArray();
<?php
use Carbon\Carbon;
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class AddTemplateSupport extends Migration
{
// Create new templates-manage permission and assign to admin role
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
$permissionId = DB::table('role_permissions')->insertGetId([
- 'name' => 'templates-manage',
+ 'name' => 'templates-manage',
'display_name' => 'Manage Page Templates',
- 'created_at' => Carbon::now()->toDateTimeString(),
- 'updated_at' => Carbon::now()->toDateTimeString()
+ 'created_at' => Carbon::now()->toDateTimeString(),
+ 'updated_at' => Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
<?php
-use Illuminate\Support\Facades\Schema;
-use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
class AddUserInvitesTable extends Migration
{
{
// Add API tokens table
- Schema::create('api_tokens', function(Blueprint $table) {
+ Schema::create('api_tokens', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('token_id')->unique();
// Add access-api permission
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
$permissionId = DB::table('role_permissions')->insertGetId([
- 'name' => 'access-api',
+ 'name' => 'access-api',
'display_name' => 'Access system API',
- 'created_at' => Carbon::now()->toDateTimeString(),
- 'updated_at' => Carbon::now()->toDateTimeString()
+ 'created_at' => Carbon::now()->toDateTimeString(),
+ 'updated_at' => Carbon::now()->toDateTimeString(),
]);
DB::table('permission_role')->insert([
- 'role_id' => $adminRoleId,
- 'permission_id' => $permissionId
+ 'role_id' => $adminRoleId,
+ 'permission_id' => $permissionId,
]);
}
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
class RemoveRoleNameField extends Migration
{
});
DB::table('roles')->update([
- "name" => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
+ 'name' => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
]);
}
}
*/
public function up()
{
- Schema::table('activities', function(Blueprint $table) {
+ Schema::table('activities', function (Blueprint $table) {
$table->index('key');
$table->index('created_at');
});
*/
public function down()
{
- Schema::table('activities', function(Blueprint $table) {
+ Schema::table('activities', function (Blueprint $table) {
$table->dropIndex('activities_key_index');
$table->dropIndex('activities_created_at_index');
});
*/
public function up()
{
- Schema::table('bookshelves', function(Blueprint $table) {
+ Schema::table('bookshelves', function (Blueprint $table) {
$table->softDeletes();
});
- Schema::table('books', function(Blueprint $table) {
+ Schema::table('books', function (Blueprint $table) {
$table->softDeletes();
});
- Schema::table('chapters', function(Blueprint $table) {
+ Schema::table('chapters', function (Blueprint $table) {
$table->softDeletes();
});
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->softDeletes();
});
}
*/
public function down()
{
- Schema::table('bookshelves', function(Blueprint $table) {
+ Schema::table('bookshelves', function (Blueprint $table) {
$table->dropSoftDeletes();
});
- Schema::table('books', function(Blueprint $table) {
+ Schema::table('books', function (Blueprint $table) {
$table->dropSoftDeletes();
});
- Schema::table('chapters', function(Blueprint $table) {
+ Schema::table('chapters', function (Blueprint $table) {
$table->dropSoftDeletes();
});
- Schema::table('pages', function(Blueprint $table) {
+ Schema::table('pages', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
class SimplifyActivitiesTable extends Migration
{
DB::table('activities')
->where('entity_id', '=', 0)
->update([
- 'entity_id' => null,
+ 'entity_id' => null,
'entity_type' => null,
]);
}
DB::table('activities')
->whereNull('entity_id')
->update([
- 'entity_id' => 0,
+ 'entity_id' => 0,
'entity_type' => '',
]);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
-use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
class AddOwnedByFieldToEntities extends Migration
{
<?php
-use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];
factory(\BookStack\Entities\Models\Book::class, 5)->create($byData)
- ->each(function($book) use ($editorUser, $byData) {
+ ->each(function ($book) use ($byData) {
$chapters = factory(Chapter::class, 3)->create($byData)
- ->each(function($chapter) use ($editorUser, $book, $byData){
+ ->each(function ($chapter) use ($book, $byData) {
$pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
$apiPermission = RolePermission::getByName('access-api');
$editorRole->attachPermission($apiPermission);
$token = (new ApiToken())->forceFill([
- 'user_id' => $editorUser->id,
- 'name' => 'Testing API key',
+ 'user_id' => $editorUser->id,
+ 'name' => 'Testing API key',
'expires_at' => ApiToken::defaultExpiry(),
- 'secret' => Hash::make('password'),
- 'token_id' => 'apitoken',
+ 'secret' => Hash::make('password'),
+ 'token_id' => 'apitoken',
]);
$token->save();
<?php
/**
- * Laravel - A PHP Framework For Web Artisans
+ * Laravel - A PHP Framework For Web Artisans.
*
- * @package Laravel
*/
-
define('LARAVEL_START', microtime(true));
/*
|
*/
-require __DIR__.'/../vendor/autoload.php';
+require __DIR__ . '/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
|
*/
-$app = require_once __DIR__.'/../bootstrap/app.php';
+$app = require_once __DIR__ . '/../bootstrap/app.php';
$app->alias('request', \BookStack\Http\Request::class);
/*
$response->send();
-$kernel->terminate($request, $response);
\ No newline at end of file
+$kernel->terminate($request, $response);
/**
* Routes for the BookStack API.
* Routes have a uri prefix of /api/.
- * Controllers are all within app/Http/Controllers/Api
+ * Controllers are all within app/Http/Controllers/Api.
*/
-
Route::get('docs', 'ApiDocsController@display');
Route::get('docs.json', 'ApiDocsController@json');
Route::get('/custom-head-content', 'HomeController@customHeadContent');
// Settings
- Route::group(['prefix' => 'settings'], function() {
+ Route::group(['prefix' => 'settings'], function () {
Route::get('/', 'SettingController@index')->name('settings');
Route::post('/', 'SettingController@update');
Route::get('/roles/{id}', 'RoleController@edit');
Route::put('/roles/{id}', 'RoleController@update');
});
-
});
// Social auth routes
Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
Route::post('/password/reset', 'Auth\ResetPasswordController@reset');
-Route::fallback('HomeController@getNotFound')->name('fallback');
\ No newline at end of file
+Route::fallback('HomeController@getNotFound')->name('fallback');
<?php
/**
- * Laravel - A PHP Framework For Web Artisans
+ * Laravel - A PHP Framework For Web Artisans.
*
- * @package Laravel
*/
-
$uri = urldecode(
parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);
// This file allows us to emulate Apache's "mod_rewrite" functionality from the
// built-in PHP web server. This provides a convenient way to test a Laravel
// application without having installed a "real" web server software here.
-if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
+if ($uri !== '/' && file_exists(__DIR__ . '/public' . $uri)) {
return false;
}
-require_once __DIR__.'/public/index.php';
+require_once __DIR__ . '/public/index.php';
-<?php namespace Tests;
+<?php
+namespace Tests;
use BookStack\Entities\Models\Book;
class ActivityTrackingTest extends BrowserKitTest
{
-
public function test_recently_viewed_books()
{
$books = Book::all()->take(10);
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\User;
{
$resp = $this->get($this->endpoint);
$resp->assertStatus(401);
- $resp->assertJson($this->errorResponse("No authorization token found on the request", 401));
+ $resp->assertJson($this->errorResponse('No authorization token found on the request', 401));
}
public function test_bad_token_format_throws_error()
{
- $resp = $this->get($this->endpoint, ['Authorization' => "Token abc123"]);
+ $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc123']);
$resp->assertStatus(401);
- $resp->assertJson($this->errorResponse("An authorization token was found on the request but the format appeared incorrect", 401));
+ $resp->assertJson($this->errorResponse('An authorization token was found on the request but the format appeared incorrect', 401));
}
public function test_token_with_non_existing_id_throws_error()
{
- $resp = $this->get($this->endpoint, ['Authorization' => "Token abc:123"]);
+ $resp = $this->get($this->endpoint, ['Authorization' => 'Token abc:123']);
$resp->assertStatus(401);
- $resp->assertJson($this->errorResponse("No matching API token was found for the provided authorization token", 401));
+ $resp->assertJson($this->errorResponse('No matching API token was found for the provided authorization token', 401));
}
public function test_token_with_bad_secret_value_throws_error()
{
$resp = $this->get($this->endpoint, ['Authorization' => "Token {$this->apiTokenId}:123"]);
$resp->assertStatus(401);
- $resp->assertJson($this->errorResponse("The secret provided for the given used API token is incorrect", 401));
+ $resp->assertJson($this->errorResponse('The secret provided for the given used API token is incorrect', 401));
}
public function test_api_access_permission_required_to_access_api()
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
$resp->assertStatus(403);
- $resp->assertJson($this->errorResponse("The owner of the used API token does not have permission to make API calls", 403));
+ $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
}
public function test_api_access_permission_required_to_access_api_with_session_auth()
$this->actingAs($editor, 'standard');
$resp = $this->get($this->endpoint);
$resp->assertStatus(403);
- $resp->assertJson($this->errorResponse("The owner of the used API token does not have permission to make API calls", 403));
+ $resp->assertJson($this->errorResponse('The owner of the used API token does not have permission to make API calls', 403));
}
public function test_token_expiry_checked()
$token->save();
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
- $resp->assertJson($this->errorResponse("The authorization token used has expired", 403));
+ $resp->assertJson($this->errorResponse('The authorization token used has expired', 403));
}
public function test_email_confirmation_checked_using_api_auth()
$resp = $this->get($this->endpoint, $this->apiAuthHeader());
$resp->assertStatus(401);
- $resp->assertJson($this->errorResponse("The email address for the account in use needs to be confirmed", 401));
+ $resp->assertJson($this->errorResponse('The email address for the account in use needs to be confirmed', 401));
}
public function test_rate_limit_headers_active_on_requests()
$resp->assertJson([
'error' => [
'code' => 429,
- ]
+ ],
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use Tests\TestCase;
$resp = $this->actingAsApiEditor()->get($this->endpoint);
$resp->assertHeader('x-ratelimit-limit', 10);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Auth\User;
use Tests\TestCase;
$resp->assertStatus(200);
$resp->assertHeader('Content-Type', 'application/json');
$resp->assertJson([
- 'docs' => [ [
+ 'docs' => [[
'name' => 'docs-display',
- 'uri' => 'api/docs'
- ] ]
+ 'uri' => 'api/docs',
+ ]],
]);
}
$resp = $this->get('/api/docs');
$resp->assertStatus(200);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Entities\Models\Book;
use Tests\TestCase;
$books = Book::visible()->orderBy('id')->take(3)->get();
$resp = $this->get($this->endpoint . '?count=1');
- $resp->assertJsonMissing(['name' => $books[1]->name ]);
+ $resp->assertJsonMissing(['name' => $books[1]->name]);
$resp = $this->get($this->endpoint . '?count=1&offset=1000');
$resp->assertJsonCount(0, 'data');
$this->actingAsApiEditor();
$sortChecks = [
- '-id' => Book::visible()->orderBy('id', 'desc')->first(),
+ '-id' => Book::visible()->orderBy('id', 'desc')->first(),
'+name' => Book::visible()->orderBy('name', 'asc')->first(),
- 'name' => Book::visible()->orderBy('name', 'asc')->first(),
- '-name' => Book::visible()->orderBy('name', 'desc')->first()
+ 'name' => Book::visible()->orderBy('name', 'asc')->first(),
+ '-name' => Book::visible()->orderBy('name', 'desc')->first(),
];
foreach ($sortChecks as $sortOption => $result) {
$resp = $this->get($this->endpoint . '?count=1&sort=' . $sortOption);
$resp->assertJson(['data' => [
[
- 'id' => $result->id,
+ 'id' => $result->id,
'name' => $result->name,
- ]
+ ],
]]);
}
}
$filterChecks = [
// Test different types of filter
- "filter[id]={$book->id}" => 1,
- "filter[id:ne]={$book->id}" => Book::visible()->where('id', '!=', $book->id)->count(),
- "filter[id:gt]={$book->id}" => Book::visible()->where('id', '>', $book->id)->count(),
- "filter[id:gte]={$book->id}" => Book::visible()->where('id', '>=', $book->id)->count(),
- "filter[id:lt]={$book->id}" => Book::visible()->where('id', '<', $book->id)->count(),
+ "filter[id]={$book->id}" => 1,
+ "filter[id:ne]={$book->id}" => Book::visible()->where('id', '!=', $book->id)->count(),
+ "filter[id:gt]={$book->id}" => Book::visible()->where('id', '>', $book->id)->count(),
+ "filter[id:gte]={$book->id}" => Book::visible()->where('id', '>=', $book->id)->count(),
+ "filter[id:lt]={$book->id}" => Book::visible()->where('id', '<', $book->id)->count(),
"filter[name:like]={$encodedNameSubstr}%" => Book::visible()->where('name', 'like', $nameSubstr . '%')->count(),
// Test mulitple filters 'and' together
$this->actingAsApiEditor();
$bookCount = Book::query()->count();
$resp = $this->get($this->endpoint . '?count=1');
- $resp->assertJson(['total' => $bookCount ]);
+ $resp->assertJson(['total' => $bookCount]);
}
public function test_total_on_results_shows_correctly_when_offset_provided()
$this->actingAsApiEditor();
$bookCount = Book::query()->count();
$resp = $this->get($this->endpoint . '?count=1&offset=1');
- $resp->assertJson(['total' => $bookCount ]);
+ $resp->assertJson(['total' => $bookCount]);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Entities\Models\Book;
use Tests\TestCase;
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
$resp->assertJson(['data' => [
[
- 'id' => $firstBook->id,
+ 'id' => $firstBook->id,
'name' => $firstBook->name,
'slug' => $firstBook->slug,
- ]
+ ],
]]);
}
{
$this->actingAsApiEditor();
$details = [
- 'name' => 'My API book',
+ 'name' => 'My API book',
'description' => 'A book created via the API',
];
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson([
- "error" => [
- "message" => "The given data was invalid.",
- "validation" => [
- "name" => ["The name field is required."]
+ 'error' => [
+ 'message' => 'The given data was invalid.',
+ 'validation' => [
+ 'name' => ['The name field is required.'],
],
- "code" => 422,
+ 'code' => 422,
],
]);
}
$resp->assertStatus(200);
$resp->assertJson([
- 'id' => $book->id,
- 'slug' => $book->slug,
+ 'id' => $book->id,
+ 'slug' => $book->slug,
'created_by' => [
'name' => $book->createdBy->name,
],
'name' => $book->createdBy->name,
],
'owned_by' => [
- 'name' => $book->ownedBy->name
+ 'name' => $book->ownedBy->name,
],
]);
}
$this->actingAsApiEditor();
$book = Book::visible()->first();
$details = [
- 'name' => 'My updated API book',
+ 'name' => 'My updated API book',
'description' => 'A book created via the API',
];
$resp->assertSee('# ' . $book->pages()->first()->name);
$resp->assertSee('# ' . $book->chapters()->first()->name);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
$resp->assertJson(['data' => [
[
- 'id' => $firstChapter->id,
- 'name' => $firstChapter->name,
- 'slug' => $firstChapter->slug,
- 'book_id' => $firstChapter->book->id,
+ 'id' => $firstChapter->id,
+ 'name' => $firstChapter->name,
+ 'slug' => $firstChapter->slug,
+ 'book_id' => $firstChapter->book->id,
'priority' => $firstChapter->priority,
- ]
+ ],
]]);
}
$this->actingAsApiEditor();
$book = Book::query()->first();
$details = [
- 'name' => 'My API chapter',
+ 'name' => 'My API chapter',
'description' => 'A chapter created via the API',
- 'book_id' => $book->id,
- 'tags' => [
+ 'book_id' => $book->id,
+ 'tags' => [
[
- 'name' => 'tagname',
+ 'name' => 'tagname',
'value' => 'tagvalue',
- ]
- ]
+ ],
+ ],
];
$resp = $this->postJson($this->baseEndpoint, $details);
$newItem = Chapter::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
$resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
$this->assertDatabaseHas('tags', [
- 'entity_id' => $newItem->id,
+ 'entity_id' => $newItem->id,
'entity_type' => $newItem->getMorphClass(),
- 'name' => 'tagname',
- 'value' => 'tagvalue',
+ 'name' => 'tagname',
+ 'value' => 'tagvalue',
]);
$resp->assertJsonMissing(['pages' => []]);
$this->assertActivityExists('chapter_create', $newItem);
$this->actingAsApiEditor();
$book = Book::query()->first();
$details = [
- 'book_id' => $book->id,
+ 'book_id' => $book->id,
'description' => 'A chapter created via the API',
];
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson($this->validationResponse([
- "name" => ["The name field is required."]
+ 'name' => ['The name field is required.'],
]));
}
{
$this->actingAsApiEditor();
$details = [
- 'name' => 'My api chapter',
+ 'name' => 'My api chapter',
'description' => 'A chapter created via the API',
];
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson($this->validationResponse([
- "book_id" => ["The book id field is required."]
+ 'book_id' => ['The book id field is required.'],
]));
}
$resp = $this->getJson($this->baseEndpoint . "/{$chapter->id}");
$resp->assertStatus(200);
$resp->assertJson([
- 'id' => $chapter->id,
- 'slug' => $chapter->slug,
+ 'id' => $chapter->id,
+ 'slug' => $chapter->slug,
'created_by' => [
'name' => $chapter->createdBy->name,
],
- 'book_id' => $chapter->book_id,
+ 'book_id' => $chapter->book_id,
'updated_by' => [
'name' => $chapter->createdBy->name,
],
'owned_by' => [
- 'name' => $chapter->ownedBy->name
+ 'name' => $chapter->ownedBy->name,
],
'pages' => [
[
- 'id' => $page->id,
+ 'id' => $page->id,
'slug' => $page->slug,
'name' => $page->name,
- ]
+ ],
],
]);
$resp->assertJsonCount($chapter->pages()->count(), 'pages');
$this->actingAsApiEditor();
$chapter = Chapter::visible()->first();
$details = [
- 'name' => 'My updated API chapter',
+ 'name' => 'My updated API chapter',
'description' => 'A chapter created via the API',
- 'tags' => [
+ 'tags' => [
[
- 'name' => 'freshtag',
+ 'name' => 'freshtag',
'value' => 'freshtagval',
- ]
+ ],
],
];
$resp->assertStatus(200);
$resp->assertJson(array_merge($details, [
- 'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id
+ 'id' => $chapter->id, 'slug' => $chapter->slug, 'book_id' => $chapter->book_id,
]));
$this->assertActivityExists('chapter_update', $chapter);
}
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $chapter->pages()->first()->name);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
$resp->assertJson(['data' => [
[
- 'id' => $firstPage->id,
- 'name' => $firstPage->name,
- 'slug' => $firstPage->slug,
- 'book_id' => $firstPage->book->id,
+ 'id' => $firstPage->id,
+ 'name' => $firstPage->name,
+ 'slug' => $firstPage->slug,
+ 'book_id' => $firstPage->book->id,
'priority' => $firstPage->priority,
- ]
+ ],
]]);
}
$this->actingAsApiEditor();
$book = Book::query()->first();
$details = [
- 'name' => 'My API page',
+ 'name' => 'My API page',
'book_id' => $book->id,
- 'html' => '<p>My new page content</p>',
- 'tags' => [
+ 'html' => '<p>My new page content</p>',
+ 'tags' => [
[
- 'name' => 'tagname',
+ 'name' => 'tagname',
'value' => 'tagvalue',
- ]
- ]
+ ],
+ ],
];
$resp = $this->postJson($this->baseEndpoint, $details);
$newItem = Page::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
$resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
$this->assertDatabaseHas('tags', [
- 'entity_id' => $newItem->id,
+ 'entity_id' => $newItem->id,
'entity_type' => $newItem->getMorphClass(),
- 'name' => 'tagname',
- 'value' => 'tagvalue',
+ 'name' => 'tagname',
+ 'value' => 'tagvalue',
]);
$resp->assertSeeText('My new page content');
$resp->assertJsonMissing(['book' => []]);
$book = Book::query()->first();
$details = [
'book_id' => $book->id,
- 'html' => '<p>A page created via the API</p>',
+ 'html' => '<p>A page created via the API</p>',
];
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson($this->validationResponse([
- "name" => ["The name field is required."]
+ 'name' => ['The name field is required.'],
]));
}
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson($this->validationResponse([
- "book_id" => ["The book id field is required when chapter id is not present."],
- "chapter_id" => ["The chapter id field is required when book id is not present."]
+ 'book_id' => ['The book id field is required when chapter id is not present.'],
+ 'chapter_id' => ['The chapter id field is required when book id is not present.'],
]));
$chapter = Chapter::visible()->first();
$this->actingAsApiEditor();
$book = Book::visible()->first();
$details = [
- 'book_id' => $book->id,
- 'name' => 'My api page',
+ 'book_id' => $book->id,
+ 'name' => 'My api page',
'markdown' => "# A new API page \n[link](https://example.com)",
];
$resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
$resp->assertStatus(200);
$resp->assertJson([
- 'id' => $page->id,
- 'slug' => $page->slug,
+ 'id' => $page->id,
+ 'slug' => $page->slug,
'created_by' => [
'name' => $page->createdBy->name,
],
- 'book_id' => $page->book_id,
+ 'book_id' => $page->book_id,
'updated_by' => [
'name' => $page->createdBy->name,
],
'owned_by' => [
- 'name' => $page->ownedBy->name
+ 'name' => $page->ownedBy->name,
],
]);
}
'html' => '<p>A page created via the API</p>',
'tags' => [
[
- 'name' => 'freshtag',
+ 'name' => 'freshtag',
'value' => 'freshtagval',
- ]
+ ],
],
];
$resp->assertStatus(200);
unset($details['html']);
$resp->assertJson(array_merge($details, [
- 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id
+ 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id,
]));
$this->assertActivityExists('page_update', $page);
}
$page = Page::visible()->first();
$chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
$details = [
- 'name' => 'My updated API page',
+ 'name' => 'My updated API page',
'chapter_id' => $chapter->id,
- 'html' => '<p>A page created via the API</p>',
+ 'html' => '<p>A page created via the API</p>',
];
$resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
$resp->assertStatus(200);
$resp->assertJson([
'chapter_id' => $chapter->id,
- 'book_id' => $chapter->book_id,
+ 'book_id' => $chapter->book_id,
]);
}
$chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
$this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]);
$details = [
- 'name' => 'My updated API page',
+ 'name' => 'My updated API page',
'chapter_id' => $chapter->id,
- 'html' => '<p>A page created via the API</p>',
+ 'html' => '<p>A page created via the API</p>',
];
$resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
$resp->assertSee('# ' . $page->name);
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.md"');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
$resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
$resp->assertJson(['data' => [
[
- 'id' => $firstBookshelf->id,
+ 'id' => $firstBookshelf->id,
'name' => $firstBookshelf->name,
'slug' => $firstBookshelf->slug,
- ]
+ ],
]]);
}
$books = Book::query()->take(2)->get();
$details = [
- 'name' => 'My API shelf',
+ 'name' => 'My API shelf',
'description' => 'A shelf created via the API',
];
foreach ($books as $index => $book) {
$this->assertDatabaseHas('bookshelves_books', [
'bookshelf_id' => $newItem->id,
- 'book_id' => $book->id,
- 'order' => $index,
+ 'book_id' => $book->id,
+ 'order' => $index,
]);
}
}
$resp = $this->postJson($this->baseEndpoint, $details);
$resp->assertStatus(422);
$resp->assertJson([
- "error" => [
- "message" => "The given data was invalid.",
- "validation" => [
- "name" => ["The name field is required."]
+ 'error' => [
+ 'message' => 'The given data was invalid.',
+ 'validation' => [
+ 'name' => ['The name field is required.'],
],
- "code" => 422,
+ 'code' => 422,
],
]);
}
$resp->assertStatus(200);
$resp->assertJson([
- 'id' => $shelf->id,
- 'slug' => $shelf->slug,
+ 'id' => $shelf->id,
+ 'slug' => $shelf->slug,
'created_by' => [
'name' => $shelf->createdBy->name,
],
'name' => $shelf->createdBy->name,
],
'owned_by' => [
- 'name' => $shelf->ownedBy->name
+ 'name' => $shelf->ownedBy->name,
],
]);
}
$this->actingAsApiEditor();
$shelf = Bookshelf::visible()->first();
$details = [
- 'name' => 'My updated API shelf',
+ 'name' => 'My updated API shelf',
'description' => 'A shelf created via the API',
];
$resp->assertStatus(204);
$this->assertActivityExists('bookshelf_delete');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Api;
+<?php
+
+namespace Tests\Api;
trait TestsApi
{
-
protected $apiTokenId = 'apitoken';
protected $apiTokenSecret = 'password';
protected function actingAsApiEditor()
{
$this->actingAs($this->getEditor(), 'api');
+
return $this;
}
*/
protected function errorResponse(string $message, int $code): array
{
- return ["error" => ["code" => $code, "message" => $message]];
+ return ['error' => ['code' => $code, 'message' => $message]];
}
/**
*/
protected function validationResponse(array $messages): array
{
- $err = $this->errorResponse("The given data was invalid.", 422);
+ $err = $this->errorResponse('The given data was invalid.', 422);
$err['error']['validation'] = $messages;
+
return $err;
}
+
/**
* Get an approved API auth header.
*/
protected function apiAuthHeader(): array
{
return [
- "Authorization" => "Token {$this->apiTokenId}:{$this->apiTokenSecret}"
+ 'Authorization' => "Token {$this->apiTokenId}:{$this->apiTokenSecret}",
];
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityService;
use BookStack\Actions\ActivityType;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Models\Chapter;
-use BookStack\Entities\Tools\TrashCan;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\TrashCan;
use Carbon\Carbon;
class AuditLogTest extends TestCase
{
- /** @var ActivityService */
+ /** @var ActivityService */
protected $activityService;
public function setUp(): void
public function test_shows_name_for_deleted_items()
{
- $this->actingAs( $this->getAdmin());
+ $this->actingAs($this->getAdmin());
$page = Page::query()->first();
$pageName = $page->name;
$this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
$resp = $this->actingAs($admin)->get('settings/audit?user=' . $editor->id);
$resp->assertSeeText($chapter->name);
$resp->assertDontSeeText($page->name);
-
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Auth;
+<?php
+
+namespace Tests\Auth;
use BookStack\Auth\Role;
use BookStack\Auth\User;
class AuthTest extends BrowserKitTest
{
-
public function test_auth_working()
{
$this->visit('/')
// Get confirmation and confirm notification matches
$emailConfirmation = DB::table('email_confirmations')->where('user_id', '=', $dbUser->id)->first();
- Notification::assertSentTo($dbUser, ConfirmEmail::class, function($notification, $channels) use ($emailConfirmation) {
+ Notification::assertSentTo($dbUser, ConfirmEmail::class, function ($notification, $channels) use ($emailConfirmation) {
return $notification->token === $emailConfirmation->token;
});
-
+
// Check confirmation email confirmation activation.
$this->visit('/register/confirm/' . $emailConfirmation->token)
->seePageIs('/')
->press('Save')
->seePageIs('/settings/users');
- $userPassword = User::find($user->id)->password;
- $this->assertTrue(Hash::check('newpassword', $userPassword));
+ $userPassword = User::find($user->id)->password;
+ $this->assertTrue(Hash::check('newpassword', $userPassword));
}
public function test_user_deletion()
->see('A password reset link will be sent to
[email protected] if that email address is found in the system.');
$this->seeInDatabase('password_resets', [
]);
$this->visit('/password/reset/' . $n->first()->token)
->see('Reset Password')
->submitForm('Reset Password', [
- 'password' => 'randompass',
- 'password_confirmation' => 'randompass'
+ 'password' => 'randompass',
+ 'password_confirmation' => 'randompass',
])->seePageIs('/')
->see('Your password has been successfully reset');
}
->see('A password reset link will be sent to
[email protected] if that email address is found in the system.')
->dontSee('We can\'t find a user');
-
$this->visit('/password/reset/arandometokenvalue')
->see('Reset Password')
->submitForm('Reset Password', [
- 'password' => 'randompass',
- 'password_confirmation' => 'randompass'
+ 'password' => 'randompass',
+ 'password_confirmation' => 'randompass',
])->followRedirects()
->seePageIs('/password/reset/arandometokenvalue')
->dontSee('We can\'t find a user')
}
/**
- * Perform a login
+ * Perform a login.
*/
protected function login(string $email, string $password): AuthTest
{
-<?php namespace Tests\Auth;
+<?php
+namespace Tests\Auth;
+
+use BookStack\Auth\Access\Ldap;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Role;
-use BookStack\Auth\Access\Ldap;
use BookStack\Auth\User;
use Mockery\MockInterface;
use Tests\TestCase;
public function setUp(): void
{
parent::setUp();
- if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
+ if (!defined('LDAP_OPT_REFERRALS')) {
+ define('LDAP_OPT_REFERRALS', 1);
+ }
config()->set([
- 'auth.method' => 'ldap',
- 'auth.defaults.guard' => 'ldap',
- 'services.ldap.base_dn' => 'dc=ldap,dc=local',
- 'services.ldap.email_attribute' => 'mail',
+ 'auth.method' => 'ldap',
+ 'auth.defaults.guard' => 'ldap',
+ 'services.ldap.base_dn' => 'dc=ldap,dc=local',
+ 'services.ldap.email_attribute' => 'mail',
'services.ldap.display_name_attribute' => 'cn',
- 'services.ldap.id_attribute' => 'uid',
- 'services.ldap.user_to_groups' => false,
- 'services.ldap.version' => '3',
- 'services.ldap.user_filter' => '(&(uid=${user}))',
- 'services.ldap.follow_referrals' => false,
- 'services.ldap.tls_insecure' => false,
- 'services.ldap.thumbnail_attribute' => null,
+ 'services.ldap.id_attribute' => 'uid',
+ 'services.ldap.user_to_groups' => false,
+ 'services.ldap.version' => '3',
+ 'services.ldap.user_filter' => '(&(uid=${user}))',
+ 'services.ldap.follow_referrals' => false,
+ 'services.ldap.tls_insecure' => false,
+ 'services.ldap.thumbnail_attribute' => null,
]);
$this->mockLdap = \Mockery::mock(Ldap::class);
$this->app[Ldap::class] = $this->mockLdap;
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$resp = $this->mockUserLogin();
$resp->assertElementExists('#home-default');
$resp->assertSee($this->mockUser->name);
$this->assertDatabaseHas('users', [
- 'email' => $this->mockUser->email,
- 'email_confirmed' => false,
- 'external_auth_id' => $this->mockUser->name
+ 'email' => $this->mockUser->email,
+ 'email_confirmed' => false,
+ 'external_auth_id' => $this->mockUser->name,
]);
}
public function test_email_domain_restriction_active_on_new_ldap_login()
{
$this->setSettings([
- 'registration-restrict' => 'testing.com'
+ 'registration-restrict' => 'testing.com',
]);
$this->commonLdapMocks(1, 1, 2, 4, 2);
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$resp = $this->mockUserLogin();
$resp->assertRedirect('/login');
$this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
-
$resp = $this->mockUserLogin($email);
$resp->assertRedirect('/login');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'cn' => [$this->mockUser->name],
- 'dn' => $ldapDn,
- 'mail' => [$this->mockUser->email]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => $ldapDn,
+ 'mail' => [$this->mockUser->email],
]]);
$resp = $this->mockUserLogin();
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'cn' => [$this->mockUser->name],
- 'dn' => $ldapDn,
+ 'cn' => [$this->mockUser->name],
+ 'dn' => $ldapDn,
'my_custom_id' => ['cooluser456'],
- 'mail' => [$this->mockUser->email]
+ 'mail' => [$this->mockUser->email],
]]);
-
$resp = $this->mockUserLogin();
$resp->assertRedirect('/');
$this->followRedirects($resp)->assertSee($this->mockUser->name);
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
$userForm->assertDontSee('Password');
$save = $this->post('/settings/users/create', [
- 'name' => $this->mockUser->name,
+ 'name' => $this->mockUser->name,
'email' => $this->mockUser->email,
]);
$save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
$save = $this->post('/settings/users/create', [
- 'name' => $this->mockUser->name,
- 'email' => $this->mockUser->email,
+ 'name' => $this->mockUser->name,
+ 'email' => $this->mockUser->email,
'external_auth_id' => $this->mockUser->name,
]);
$save->assertRedirect('/settings/users');
$editPage->assertDontSee('Password');
$update = $this->put("/settings/users/{$editUser->id}", [
- 'name' => $editUser->name,
- 'email' => $editUser->email,
+ 'name' => $editUser->name,
+ 'email' => $editUser->email,
'external_auth_id' => 'test_auth_id',
]);
$update->assertRedirect('/settings/users');
$this->mockUser->attachRole($existingRole);
app('config')->set([
- 'services.ldap.user_to_groups' => true,
- 'services.ldap.group_attribute' => 'memberOf',
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => false,
]);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'mail' => [$this->mockUser->email],
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 2,
- 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
- 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
- ]
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
+ ],
]]);
$this->mockUserLogin()->assertRedirect('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive->id
+ 'role_id' => $roleToReceive->id,
]);
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive2->id
+ 'role_id' => $roleToReceive2->id,
]);
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $existingRole->id
+ 'role_id' => $existingRole->id,
]);
}
$this->mockUser->attachRole($existingRole);
app('config')->set([
- 'services.ldap.user_to_groups' => true,
- 'services.ldap.group_attribute' => 'memberOf',
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'mail' => [$this->mockUser->email],
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 1,
- 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
- ]
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ ],
]]);
$this->mockUserLogin()->assertRedirect('/');
$user = User::query()->where('email', $this->mockUser->email)->first();
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive->id
+ 'role_id' => $roleToReceive->id,
]);
$this->assertDatabaseMissing('role_user', [
'user_id' => $user->id,
- 'role_id' => $existingRole->id
+ 'role_id' => $existingRole->id,
]);
}
$roleToNotReceive = factory(Role::class)->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
app('config')->set([
- 'services.ldap.user_to_groups' => true,
- 'services.ldap.group_attribute' => 'memberOf',
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'mail' => [$this->mockUser->email],
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 1,
- 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
- ]
+ 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
+ ],
]]);
$this->mockUserLogin()->assertRedirect('/');
$user = User::query()->where('email', $this->mockUser->email)->first();
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive->id
+ 'role_id' => $roleToReceive->id,
]);
$this->assertDatabaseMissing('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToNotReceive->id
+ 'role_id' => $roleToNotReceive->id,
]);
}
setting()->put('registration-role', $roleToReceive->id);
app('config')->set([
- 'services.ldap.user_to_groups' => true,
- 'services.ldap.group_attribute' => 'memberOf',
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'mail' => [$this->mockUser->email],
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 2,
- 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
- 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
- ]
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
+ ],
]]);
$this->mockUserLogin()->assertRedirect('/');
$user = User::query()->where('email', $this->mockUser->email)->first();
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive->id
+ 'role_id' => $roleToReceive->id,
]);
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive2->id
+ 'role_id' => $roleToReceive2->id,
]);
}
public function test_login_uses_specified_display_name_attribute()
{
app('config')->set([
- 'services.ldap.display_name_attribute' => 'displayName'
+ 'services.ldap.display_name_attribute' => 'displayName',
]);
$this->commonLdapMocks(1, 1, 2, 4, 2);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'displayname' => 'displayNameAttribute'
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'displayname' => 'displayNameAttribute',
]]);
$this->mockUserLogin()->assertRedirect('/login');
public function test_login_uses_default_display_name_attribute_if_specified_not_present()
{
app('config')->set([
- 'services.ldap.display_name_attribute' => 'displayName'
+ 'services.ldap.display_name_attribute' => 'displayName',
]);
$this->commonLdapMocks(1, 1, 2, 4, 2);
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$this->mockUserLogin()->assertRedirect('/login');
$resp->assertRedirect('/');
$this->get('/')->assertSee($this->mockUser->name);
$this->assertDatabaseHas('users', [
- 'email' => $this->mockUser->email,
- 'email_confirmed' => false,
+ 'email' => $this->mockUser->email,
+ 'email_confirmed' => false,
'external_auth_id' => $this->mockUser->name,
- 'name' => $this->mockUser->name
+ 'name' => $this->mockUser->name,
]);
}
protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
{
app('config')->set([
- 'services.ldap.server' => $serverString
+ 'services.ldap.server' => $serverString,
]);
// Standard mocks
$this->commonLdapMocks(0, 1, 1, 2, 1);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$this->mockLdap->shouldReceive('connect')->once()
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$resp = $this->post('/login', [
'password' => $this->mockUser->password,
]);
$resp->assertJsonStructure([
- 'details_from_ldap' => [],
+ 'details_from_ldap' => [],
'details_bookstack_parsed' => [],
]);
}
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
->andReturn(['count' => 1, 0 => [
'uid' => [hex2bin('FFF8F7')],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')]
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]]);
$details = $ldapService->getUserDetails('test');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'uid' => [$this->mockUser->name],
- 'cn' => [$this->mockUser->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
]], ['count' => 1, 0 => [
- 'uid' => ['Barry'],
- 'cn' => ['Scott'],
- 'dn' => ['dc=bscott' . config('services.ldap.base_dn')],
+ 'uid' => ['Barry'],
+ 'cn' => ['Scott'],
+ 'dn' => ['dc=bscott' . config('services.ldap.base_dn')],
]]);
setting()->put('registration-confirmation', 'true');
app('config')->set([
- 'services.ldap.user_to_groups' => true,
- 'services.ldap.group_attribute' => 'memberOf',
+ 'services.ldap.user_to_groups' => true,
+ 'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('searchAndGetEntries')
->times(3)
->andReturn(['count' => 1, 0 => [
- 'uid' => [$user->name],
- 'cn' => [$user->name],
- 'dn' => ['dc=test' . config('services.ldap.base_dn')],
- 'mail' => [$user->email],
+ 'uid' => [$user->name],
+ 'cn' => [$user->name],
+ 'dn' => ['dc=test' . config('services.ldap.base_dn')],
+ 'mail' => [$user->email],
'memberof' => [
'count' => 1,
- 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
- ]
+ 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
+ ],
]]);
$this->followingRedirects()->mockUserLogin()->assertSee('Thanks for registering!');
$this->assertDatabaseHas('users', [
- 'email' => $user->email,
+ 'email' => $user->email,
'email_confirmed' => false,
]);
$user = User::query()->where('email', '=', $user->email)->first();
$this->assertDatabaseHas('role_user', [
'user_id' => $user->id,
- 'role_id' => $roleToReceive->id
+ 'role_id' => $roleToReceive->id,
]);
$homePage = $this->get('/');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
- 'cn' => [$this->mockUser->name],
- 'dn' => $ldapDn,
+ 'cn' => [$this->mockUser->name],
+ 'dn' => $ldapDn,
'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
- 'mail' => [$this->mockUser->email]
+ 'mail' => [$this->mockUser->email],
]]);
$this->mockUserLogin()
-<?php namespace Tests\Auth;
+<?php
+
+namespace Tests\Auth;
use BookStack\Auth\Role;
use BookStack\Auth\User;
class Saml2Test extends TestCase
{
-
public function setUp(): void
{
parent::setUp();
// Set default config for SAML2
config()->set([
- 'auth.method' => 'saml2',
- 'auth.defaults.guard' => 'saml2',
- 'saml2.name' => 'SingleSignOn-Testing',
- 'saml2.email_attribute' => 'email',
- 'saml2.display_name_attributes' => ['first_name', 'last_name'],
- 'saml2.external_id_attribute' => 'uid',
- 'saml2.user_to_groups' => false,
- 'saml2.group_attribute' => 'user_groups',
- 'saml2.remove_from_groups' => false,
- 'saml2.onelogin_overrides' => null,
- 'saml2.onelogin.idp.entityId' => 'http://saml.local/saml2/idp/metadata.php',
- 'saml2.onelogin.idp.singleSignOnService.url' => 'http://saml.local/saml2/idp/SSOService.php',
- 'saml2.onelogin.idp.singleLogoutService.url' => 'http://saml.local/saml2/idp/SingleLogoutService.php',
- 'saml2.autoload_from_metadata' => false,
- 'saml2.onelogin.idp.x509cert' => $this->testCert,
- 'saml2.onelogin.debug' => false,
+ 'auth.method' => 'saml2',
+ 'auth.defaults.guard' => 'saml2',
+ 'saml2.name' => 'SingleSignOn-Testing',
+ 'saml2.email_attribute' => 'email',
+ 'saml2.display_name_attributes' => ['first_name', 'last_name'],
+ 'saml2.external_id_attribute' => 'uid',
+ 'saml2.user_to_groups' => false,
+ 'saml2.group_attribute' => 'user_groups',
+ 'saml2.remove_from_groups' => false,
+ 'saml2.onelogin_overrides' => null,
+ 'saml2.onelogin.idp.entityId' => 'http://saml.local/saml2/idp/metadata.php',
+ 'saml2.onelogin.idp.singleSignOnService.url' => 'http://saml.local/saml2/idp/SSOService.php',
+ 'saml2.onelogin.idp.singleLogoutService.url' => 'http://saml.local/saml2/idp/SingleLogoutService.php',
+ 'saml2.autoload_from_metadata' => false,
+ 'saml2.onelogin.idp.x509cert' => $this->testCert,
+ 'saml2.onelogin.debug' => false,
'saml2.onelogin.security.requestedAuthnContext' => true,
]);
}
$this->assertFalse($this->isAuthenticated());
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
-
$acsPost = $this->post('/saml2/acs');
$acsPost->assertRedirect('/');
$this->assertTrue($this->isAuthenticated());
$this->assertDatabaseHas('users', [
'external_auth_id' => 'user',
- 'email_confirmed' => false,
- 'name' => 'Barry Scott'
+ 'email_confirmed' => false,
+ 'name' => 'Barry Scott',
]);
-
});
}
public function test_group_role_sync_on_login()
{
config()->set([
- 'saml2.onelogin.strict' => false,
- 'saml2.user_to_groups' => true,
+ 'saml2.onelogin.strict' => false,
+ 'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => false,
]);
public function test_group_role_sync_removal_option_works_as_expected()
{
config()->set([
- 'saml2.onelogin.strict' => false,
- 'saml2.user_to_groups' => true,
+ 'saml2.onelogin.strict' => false,
+ 'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => true,
]);
public function test_logout_sls_flow_when_sls_not_configured()
{
config()->set([
- 'saml2.onelogin.strict' => false,
+ 'saml2.onelogin.strict' => false,
'saml2.onelogin.idp.singleLogoutService.url' => null,
]);
public function test_dump_user_details_option_works()
{
config()->set([
- 'saml2.onelogin.strict' => false,
+ 'saml2.onelogin.strict' => false,
'saml2.dump_user_details' => true,
]);
$acsPost = $this->post('/saml2/acs');
$acsPost->assertJsonStructure([
'id_from_idp',
- 'attrs_from_idp' => [],
+ 'attrs_from_idp' => [],
'attrs_after_parsing' => [],
]);
});
public function test_email_domain_restriction_active_on_new_saml_login()
{
$this->setSettings([
- 'registration-restrict' => 'testing.com'
+ 'registration-restrict' => 'testing.com',
]);
config()->set([
'saml2.onelogin.strict' => false,
{
setting()->put('registration-confirmation', 'true');
config()->set([
- 'saml2.onelogin.strict' => false,
- 'saml2.user_to_groups' => true,
+ 'saml2.onelogin.strict' => false,
+ 'saml2.user_to_groups' => true,
'saml2.remove_from_groups' => false,
]);
// Make the user pre-existing in DB with different auth_id
User::query()->forceCreate([
'external_auth_id' => 'old_system_user_id',
- 'email_confirmed' => false,
- 'name' => 'Barry Scott'
+ 'email_confirmed' => false,
+ 'name' => 'Barry Scott',
]);
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
$acsPost->assertRedirect('/login');
$this->assertFalse($this->isAuthenticated());
$this->assertDatabaseHas('users', [
'external_auth_id' => 'old_system_user_id',
]);
$loginGet = $this->get('/login');
- $loginGet->assertSee(
"A user with the email [email protected] already exists but with different credentials");
+ $loginGet->assertSee(
'A user with the email [email protected] already exists but with different credentials');
});
}
$query = explode('?', $location)[1];
$params = [];
parse_str($query, $params);
+
return gzinflate(base64_decode($params['SAMLRequest']));
}
* The post data for a callback for single-sign-in.
* Provides the following attributes:
* array:5 [
- "uid" => array:1 [
- 0 => "user"
- ]
- "first_name" => array:1 [
- 0 => "Barry"
- ]
- "last_name" => array:1 [
- 0 => "Scott"
- ]
- "email" => array:1 [
- ]
- "user_groups" => array:2 [
- 0 => "member"
- 1 => "admin"
- ]
- ]
+ * "uid" => array:1 [
+ * 0 => "user"
+ * ]
+ * "first_name" => array:1 [
+ * 0 => "Barry"
+ * ]
+ * "last_name" => array:1 [
+ * 0 => "Scott"
+ * ]
+ * "email" => array:1 [
+ * ]
+ * "user_groups" => array:2 [
+ * 0 => "member"
+ * 1 => "admin"
+ * ]
+ * ].
*/
protected $acsPostData = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxOS0xMS0xN1QxNzo1MzozOVoiIERlc3RpbmF0aW9uPSJodHRwOi8vYm9va3N0YWNrLmxvY2FsL3NhbWwyL2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl82YTBmNGYzOTkzMDQwZjE5ODdmZDM3MDY4YjUyOTYyMjlhZDUzNjFjIj48c2FtbDpJc3N1ZXI+aHR0cDovL3NhbWwubG9jYWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNfNGRkNDU2NGRjNzk0MDYxZWYxYmFhMDQ2N2Q3OTAyOGNlZDNjZTU0YmVlIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+dm1oL1M3NU5mK2crZWNESkN6QWJaV0tKVmx1ZzdCZnNDKzlhV05lSXJlUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dnJhZ0tKWHNjVm5UNjJFaEk3bGk4MERUWHNOTGJOc3lwNWZ2QnU4WjFYSEtFUVA3QWpPNkcxcVBwaGpWQ2dRMzd6TldVVTZvUytQeFA3UDlHeG5xL3hKejRUT3lHcHJ5N1RoK2pIcHc0YWVzQTdrTmp6VU51UmU2c1ltWTlrRXh2VjMvTmJRZjROMlM2Y2RhRHIzWFRodllVVDcxYzQwNVVHOFJpQjJaY3liWHIxZU1yWCtXUDBnU2Qrc0F2RExqTjBJc3pVWlVUNThadFpEVE1ya1ZGL0pIbFBFQ04vVW1sYVBBeitTcUJ4c25xTndZK1oxYUt3MnlqeFRlNnUxM09Kb29OOVN1REowNE0rK2F3RlY3NkI4cXEyTzMxa3FBbDJibm1wTGxtTWdRNFEraUlnL3dCc09abTV1clphOWJObDNLVEhtTVBXbFpkbWhsLzgvMy9IT1RxN2thWGs3cnlWRHRLcFlsZ3FUajNhRUpuL0dwM2o4SFp5MUVialRiOTRRT1ZQMG5IQzB1V2hCaE13TjdzVjFrUSsxU2NjUlpUZXJKSGlSVUQvR0srTVg3M0YrbzJVTFRIL1Z6Tm9SM2o4N2hOLzZ1UC9JeG5aM1RudGR1MFZPZS9ucEdVWjBSMG9SWFhwa2JTL2poNWk1ZjU0RXN4eXZ1VEM5NHdKaEM8L2RzOlNpZ25hdHVyZVZhbHVlPgo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlFYXpDQ0F0T2dBd0lCQWdJVWU3YTA4OENucjRpem1ybkJFbng1cTNIVE12WXdEUVlKS29aSWh2Y05BUUVMQlFBd1JURUxNQWtHQTFVRUJoTUNSMEl4RXpBUkJnTlZCQWdNQ2xOdmJXVXRVM1JoZEdVeElUQWZCZ05WQkFvTUdFbHVkR1Z5Ym1WMElGZHBaR2RwZEhNZ1VIUjVJRXgwWkRBZUZ3MHhPVEV4TVRZeE1qRTNNVFZhRncweU9URXhNVFV4TWpFM01UVmFNRVV4Q3pBSkJnTlZCQVlUQWtkQ01STXdFUVlEVlFRSURBcFRiMjFsTFZOMFlYUmxNU0V3SHdZRFZRUUtEQmhKYm5SbGNtNWxkQ0JYYVdSbmFYUnpJRkIwZVNCTWRHUXdnZ0dpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCandBd2dnR0tBb0lCZ1FEekxlOUZmZHlwbFR4SHA0U3VROWdRdFpUM3QrU0RmdkVMNzJwcENmRlp3NytCNXM1Qi9UNzNhWHBvUTNTNTNwR0kxUklXQ2dlMmlDVVEydHptMjdhU05IMGl1OWFKWWNVUVovUklUcWQwYXl5RGtzMU5BMlBUM1RXNnQzbTdLVjVyZTRQME5iK1lEZXV5SGRreitqY010cG44Q21Cb1QwSCtza2hhMGhpcUlOa2prUlBpSHZMSFZHcCt0SFVFQS9JNm1ONGFCL1VFeFNUTHM3OU5zTFVmdGVxcXhlOSt0dmRVYVRveURQcmhQRmpPTnMrOU5LQ2t6SUM2dmN2N0o2QXR1S0c2bkVUK3pCOXlPV2d0R1lRaWZYcVFBMnk1ZEw4MUJCMHE1dU1hQkxTMnBxM2FQUGp6VTJGMytFeXNqeVNXVG5Da2ZrN0M1U3NDWFJ1OFErVTk1dHVucE5md2Y1b2xFNldhczQ4Tk1NK1B3VjdpQ05NUGtOemxscTZQQ2lNK1A4RHJNU2N6elVaWlFVU3Y2ZFN3UENvK1lTVmltRU0wT2czWEpUaU5oUTVBTmxhSW42Nkt3NWdmb0JmdWlYbXlJS2lTRHlBaURZbUZhZjQzOTV3V3dMa1RSK2N3OFdmamFIc3dLWlRvbW4xTVIzT0pzWTJVSjBlUkJZTStZU3NDQXdFQUFhTlRNRkV3SFFZRFZSME9CQllFRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01COEdBMVVkSXdRWU1CYUFGSW1wMkNZQ0dmY2I3dzkxSC9jU2hUQ2tYd1IvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0dCQUErZy9DN3VMOWxuK1crcUJrbkxXODFrb2pZZmxnUEsxSTFNSEl3bk12bC9aVEhYNGRSWEtEcms3S2NVcTFLanFhak5WNjZmMWNha3AwM0lpakJpTzBYaTFnWFVaWUxvQ2lOR1V5eXA5WGxvaUl5OVh3MlBpV25ydzAreVp5dlZzc2JlaFhYWUpsNFJpaEJqQld1bDlSNHdNWUxPVVNKRGUyV3hjVUJoSm54eU5ScytQMHhMU1FYNkIybjZueG9Ea280cDA3czhaS1hRa2VpWjJpd0ZkVHh6UmtHanRoTVV2NzA0bnpzVkdCVDBEQ1B0ZlNhTzVLSlpXMXJDczN5aU10aG5CeHE0cUVET1FKRklsKy9MRDcxS2JCOXZaY1c1SnVhdnpCRm1rS0dOcm8vNkcxSTdlbDQ2SVI0d2lqVHlORkNZVXVEOWR0aWduTm1wV3ROOE9XK3B0aUwvanRUeVNXdWtqeXMwcyt2TG44M0NWdmpCMGRKdFZBSVlPZ1hGZEl1aWk2Nmdjend3TS9MR2lPRXhKbjBkVE56c0ovSVlocHhMNEZCRXVQMHBza1kwbzBhVWxKMkxTMmord1NRVFJLc0JnTWp5clVyZWtsZTJPRFN0U3RuM2VhYmpJeDAvRkhscEZyMGpOSW0vb01QN2t3anRVWDR6YU5lNDdRSTRHZz09PC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9Il82ODQyZGY5YzY1OWYxM2ZlNTE5NmNkOWVmNmMyZjAyODM2NGFlOTQzYjEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE5LTExLTE3VDE3OjUzOjM5WiI+PHNhbWw6SXNzdWVyPmh0dHA6Ly9zYW1sLmxvY2FsL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPgogIDxkczpSZWZlcmVuY2UgVVJJPSIjXzY4NDJkZjljNjU5ZjEzZmU1MTk2Y2Q5ZWY2YzJmMDI4MzY0YWU5NDNiMSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPmtyYjV3NlM4dG9YYy9lU3daUFVPQnZRem4zb3M0SkFDdXh4ckpreHBnRnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPjJxcW1Ba3hucXhOa3N5eXh5dnFTVDUxTDg5VS9ZdHpja2t1ekF4ci9hQ1JTK1NPRzg1YkFNWm8vU3puc3d0TVlBYlFRQ0VGb0R1amdNdlpzSFl3NlR2dmFHanlXWUpRNVZyYWhlemZaSWlCVUU0NHBtWGFrOCswV0l0WTVndnBGSXhxWFZaRmdFUkt2VExmZVFCMzhkMVZQc0ZVZ0RYdXQ4VS9Qdm43dXZwdXZjVXorMUUyOUVKR2FZL0dndnhUN0tyWU9SQTh3SitNdVRzUVZtanNlUnhveVJTejA4TmJ3ZTJIOGpXQnpFWWNxWWwyK0ZnK2hwNWd0S216VmhLRnBkNXZBNjdBSXo1NXN0QmNHNSswNHJVaWpFSzRzci9xa0x5QmtKQjdLdkwzanZKcG8zQjhxYkxYeXhLb1dSSmRnazhKNHMvTVp1QWk3QWUxUXNTTjl2Z3ZTdVRlc0VCUjVpSHJuS1lrbEpRWXNrbUQzbSsremE4U1NRbnBlM0UzYUZBY3p6cElUdUQ4YkFCWmRqcUk2TkhrSmFRQXBmb0hWNVQrZ244ejdUTWsrSStUU2JlQURubUxCS3lnMHRabW10L0ZKbDV6eWowVmxwc1dzTVM2OVE2bUZJVStqcEhSanpOb2FLMVM1dlQ3ZW1HbUhKSUp0cWlOdXJRN0tkQlBJPC9kczpTaWduYXR1cmVWYWx1ZT4KPGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJRWF6Q0NBdE9nQXdJQkFnSVVlN2EwODhDbnI0aXptcm5CRW54NXEzSFRNdll3RFFZSktvWklodmNOQVFFTEJRQXdSVEVMTUFrR0ExVUVCaE1DUjBJeEV6QVJCZ05WQkFnTUNsTnZiV1V0VTNSaGRHVXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MFpEQWVGdzB4T1RFeE1UWXhNakUzTVRWYUZ3MHlPVEV4TVRVeE1qRTNNVFZhTUVVeEN6QUpCZ05WQkFZVEFrZENNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWGFXUm5hWFJ6SUZCMGVTQk1kR1F3Z2dHaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQmp3QXdnZ0dLQW9JQmdRRHpMZTlGZmR5cGxUeEhwNFN1UTlnUXRaVDN0K1NEZnZFTDcycHBDZkZadzcrQjVzNUIvVDczYVhwb1EzUzUzcEdJMVJJV0NnZTJpQ1VRMnR6bTI3YVNOSDBpdTlhSlljVVFaL1JJVHFkMGF5eURrczFOQTJQVDNUVzZ0M203S1Y1cmU0UDBOYitZRGV1eUhka3oramNNdHBuOENtQm9UMEgrc2toYTBoaXFJTmtqa1JQaUh2TEhWR3ArdEhVRUEvSTZtTjRhQi9VRXhTVExzNzlOc0xVZnRlcXF4ZTkrdHZkVWFUb3lEUHJoUEZqT05zKzlOS0NreklDNnZjdjdKNkF0dUtHNm5FVCt6Qjl5T1dndEdZUWlmWHFRQTJ5NWRMODFCQjBxNXVNYUJMUzJwcTNhUFBqelUyRjMrRXlzanlTV1RuQ2tmazdDNVNzQ1hSdThRK1U5NXR1bnBOZndmNW9sRTZXYXM0OE5NTStQd1Y3aUNOTVBrTnpsbHE2UENpTStQOERyTVNjenpVWlpRVVN2NmRTd1BDbytZU1ZpbUVNME9nM1hKVGlOaFE1QU5sYUluNjZLdzVnZm9CZnVpWG15SUtpU0R5QWlEWW1GYWY0Mzk1d1d3TGtUUitjdzhXZmphSHN3S1pUb21uMU1SM09Kc1kyVUowZVJCWU0rWVNzQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZJbXAyQ1lDR2ZjYjd3OTFIL2NTaFRDa1h3Ui9NQjhHQTFVZEl3UVlNQmFBRkltcDJDWUNHZmNiN3c5MUgvY1NoVENrWHdSL01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dHQkFBK2cvQzd1TDlsbitXK3FCa25MVzgxa29qWWZsZ1BLMUkxTUhJd25NdmwvWlRIWDRkUlhLRHJrN0tjVXExS2pxYWpOVjY2ZjFjYWtwMDNJaWpCaU8wWGkxZ1hVWllMb0NpTkdVeXlwOVhsb2lJeTlYdzJQaVducncwK3laeXZWc3NiZWhYWFlKbDRSaWhCakJXdWw5UjR3TVlMT1VTSkRlMld4Y1VCaEpueHlOUnMrUDB4TFNRWDZCMm42bnhvRGtvNHAwN3M4WktYUWtlaVoyaXdGZFR4elJrR2p0aE1VdjcwNG56c1ZHQlQwRENQdGZTYU81S0paVzFyQ3MzeWlNdGhuQnhxNHFFRE9RSkZJbCsvTEQ3MUtiQjl2WmNXNUp1YXZ6QkZta0tHTnJvLzZHMUk3ZWw0NklSNHdpalR5TkZDWVV1RDlkdGlnbk5tcFd0TjhPVytwdGlML2p0VHlTV3VranlzMHMrdkxuODNDVnZqQjBkSnRWQUlZT2dYRmRJdWlpNjZnY3p3d00vTEdpT0V4Sm4wZFROenNKL0lZaHB4TDRGQkV1UDBwc2tZMG8wYVVsSjJMUzJqK3dTUVRSS3NCZ01qeXJVcmVrbGUyT0RTdFN0bjNlYWJqSXgwL0ZIbHBGcjBqTkltL29NUDdrd2p0VVg0emFOZTQ3UUk0R2c9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YSIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp0cmFuc2llbnQiPl8yYzdhYjg2ZWI4ZjFkMTA2MzQ0M2YyMTljYzU4NjhmZjY2NzA4OTEyZTM8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMTktMTEtMTdUMTc6NTg6MzlaIiBSZWNpcGllbnQ9Imh0dHA6Ly9ib29rc3RhY2subG9jYWwvc2FtbDIvYWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzZhMGY0ZjM5OTMwNDBmMTk4N2ZkMzcwNjhiNTI5NjIyOWFkNTM2MWMiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxOS0xMS0xN1QxNzo1MzowOVoiIE5vdE9uT3JBZnRlcj0iMjAxOS0xMS0xN1QxNzo1ODozOVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL2Jvb2tzdGFjay5sb2NhbC9zYW1sMi9tZXRhZGF0YTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTktMTEtMTdUMTc6NTM6MzlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE5LTExLTE4VDAxOjUzOjM5WiIgU2Vzc2lvbkluZGV4PSJfNGZlN2MwZDE1NzJkNjRiMjdmOTMwYWE2ZjIzNmE2ZjQyZTkzMDkwMWNjIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZmlyc3RfbmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+QmFycnk8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibGFzdF9uYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5TY290dDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlbWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlckBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1c2VyX2dyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+bWVtYmVyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+';
-<?php namespace Tests\Auth;
+<?php
+
+namespace Tests\Auth;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
class SocialAuthTest extends TestCase
{
-
public function test_social_registration()
{
$user = factory(User::class)->make();
config([
'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
- 'APP_URL' => 'http://localhost'
+ 'APP_URL' => 'http://localhost',
]);
$mockSocialite = $this->mock(Factory::class);
// Test login routes
$resp = $this->get('/login');
$resp->assertElementExists('a#social-login-google[href$="/login/service/google"]');
- $resp = $this->followingRedirects()->get("/login/service/google");
+ $resp = $this->followingRedirects()->get('/login/service/google');
$resp->assertSee('login-form');
// Test social callback
$resp = $this->get('/login');
$resp->assertElementExists('a#social-login-github[href$="/login/service/github"]');
- $resp = $this->followingRedirects()->get("/login/service/github");
+ $resp = $this->followingRedirects()->get('/login/service/github');
$resp->assertSee('login-form');
-
// Test social callback with matching social account
DB::table('social_accounts')->insert([
- 'user_id' => $this->getAdmin()->id,
- 'driver' => 'github',
- 'driver_id' => 'logintest123'
+ 'user_id' => $this->getAdmin()->id,
+ 'driver' => 'github',
+ 'driver_id' => 'logintest123',
]);
$resp = $this->followingRedirects()->get('/login/service/github/callback');
- $resp->assertDontSee("login-form");
+ $resp->assertDontSee('login-form');
}
public function test_social_account_detach()
$editor = $this->getEditor();
config([
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
- 'APP_URL' => 'http://localhost'
+ 'APP_URL' => 'http://localhost',
]);
$socialAccount = SocialAccount::query()->forceCreate([
- 'user_id' => $editor->id,
- 'driver' => 'github',
+ 'user_id' => $editor->id,
+ 'driver' => 'github',
'driver_id' => 'logintest123',
]);
{
config([
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',
- 'APP_URL' => 'http://localhost'
+ 'APP_URL' => 'http://localhost',
]);
$user = factory(User::class)->make();
{
config([
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',
- 'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true
+ 'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true,
]);
$user = factory(User::class)->make();
$user = $user->whereEmail($user->email)->first();
$this->assertDatabaseHas('social_accounts', ['user_id' => $user->id]);
}
-
}
-<?php namespace Tests\Auth;
+<?php
+namespace Tests\Auth;
use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\User;
class UserInviteTest extends TestCase
{
-
public function test_user_creation_creates_invite()
{
Notification::fake();
$email = Str::random(16) . '@example.com';
$resp = $this->actingAs($admin)->post('/settings/users/create', [
- 'name' => 'Barry',
- 'email' => $email,
+ 'name' => 'Barry',
+ 'email' => $email,
'send_invite' => 'true',
]);
$resp->assertRedirect('/settings/users');
Notification::assertSentTo($newUser, UserInvite::class);
$this->assertDatabaseHas('user_invites', [
- 'user_id' => $newUser->id
+ 'user_id' => $newUser->id,
]);
}
]);
$setPasswordResp->assertSee('Password set, you now have access to BookStack!');
$newPasswordValid = auth()->validate([
- 'email' => $user->email,
- 'password' => 'my test password'
+ 'email' => $user->email,
+ 'password' => 'my test password',
]);
$this->assertTrue($newPasswordValid);
$this->assertDatabaseMissing('user_invites', [
- 'user_id' => $user->id
+ 'user_id' => $user->id,
]);
}
$noPassword->assertSee('The password field is required.');
$this->assertDatabaseHas('user_invites', [
- 'user_id' => $user->id
+ 'user_id' => $user->id,
]);
}
$setPasswordPageResp->assertRedirect('/password/email');
$setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');
}
-
-
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+namespace Tests;
+
+use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
-use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Page;
use BookStack\Settings\SettingService;
use DB;
abstract class BrowserKitTest extends TestCase
{
-
use DatabaseTransactions;
use SharedTestHelpers;
/**
* The base URL to use while testing the application.
+ *
* @var string
*/
protected $baseUrl = 'http://localhost';
- public function tearDown() : void
+ public function tearDown(): void
{
DB::disconnect();
parent::tearDown();
*/
public function createApplication()
{
- $app = require __DIR__.'/../bootstrap/app.php';
+ $app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
-
/**
* Quickly sets an array of settings.
+ *
* @param $settingsArray
*/
protected function setSettings($settingsArray)
/**
* Helper for updating entity permissions.
+ *
* @param Entity $entity
*/
protected function updateEntityPermissions(Entity $entity)
$restrictionService->buildJointPermissionsForEntity($entity);
}
-
/**
- * Quick way to create a new user without any permissions
+ * Quick way to create a new user without any permissions.
+ *
* @param array $attributes
+ *
* @return mixed
*/
protected function getNewBlankUser($attributes = [])
{
$user = factory(User::class)->create($attributes);
+
return $user;
}
/**
* Assert that a given string is seen inside an element.
*
- * @param bool|string|null $element
- * @param integer $position
- * @param string $text
- * @param bool $negate
+ * @param bool|string|null $element
+ * @param int $position
+ * @param string $text
+ * @param bool $negate
+ *
* @return $this
*/
protected function seeInNthElement($element, $position, $text, $negate = false)
/**
* Assert that the current page matches a given URI.
*
- * @param string $uri
+ * @param string $uri
+ *
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
- $uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
+ $uri,
+ $this->currentUri,
+ "Did not land on expected page [{$uri}].\n"
);
return $this;
/**
* Do a forced visit that does not error out on exception.
+ *
* @param string $uri
- * @param array $parameters
- * @param array $cookies
- * @param array $files
+ * @param array $parameters
+ * @param array $cookies
+ * @param array $files
+ *
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
+
return $this;
}
/**
* Click the text within the selected element.
+ *
* @param $parentElement
* @param $linkText
+ *
* @return $this
*/
protected function clickInElement($parentElement, $linkText)
$elem = $this->crawler->filter($parentElement);
$link = $elem->selectLink($linkText);
$this->visit($link->link()->getUri());
+
return $this;
}
/**
* Check if the page contains the given element.
- * @param string $selector
+ *
+ * @param string $selector
*/
protected function pageHasElement($selector)
{
$elements = $this->crawler->filter($selector);
- $this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
+ $this->assertTrue(count($elements) > 0, 'The page does not contain an element matching ' . $selector);
+
return $this;
}
/**
* Check if the page contains the given element.
- * @param string $selector
+ *
+ * @param string $selector
*/
protected function pageNotHasElement($selector)
{
$elements = $this->crawler->filter($selector);
- $this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
+ $this->assertFalse(count($elements) > 0, 'The page contains ' . count($elements) . ' elements matching ' . $selector);
+
return $this;
}
}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Auth\User;
use Tests\TestCase;
public function test_add_admin_command()
{
$exitCode = \Artisan::call('bookstack:create-admin', [
- '--name' => 'Admin Test',
+ '--name' => 'Admin Test',
'--password' => 'testing-4',
]);
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseHas('users', [
- 'name' => 'Admin Test'
+ 'name' => 'Admin Test',
]);
$this->assertTrue(User::query()->where('email', '=', '
[email protected]')->first()->hasSystemRole('admin'), 'User has admin role as expected');
$this->assertTrue(\Auth::attempt(['email' => '
[email protected]', 'password' => 'testing-4']), 'Password stored as expected');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Page;
\Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
$this->assertDatabaseHas('activities', [
- 'type' => 'page_update',
+ 'type' => 'page_update',
'entity_id' => $page->id,
- 'user_id' => $this->getEditor()->id
+ 'user_id' => $this->getEditor()->id,
]);
-
DB::rollBack();
$exitCode = \Artisan::call('bookstack:clear-activity');
DB::beginTransaction();
$this->assertTrue($exitCode === 0, 'Command executed successfully');
-
$this->assertDatabaseMissing('activities', [
- 'type' => 'page_update'
+ 'type' => 'page_update',
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
- 'type' => 'version'
+ 'type' => 'version',
]);
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
- 'type' => 'update_draft'
+ 'type' => 'update_draft',
]);
$exitCode = Artisan::call('bookstack:clear-revisions');
$this->assertDatabaseMissing('page_revisions', [
'page_id' => $page->id,
- 'type' => 'version'
+ 'type' => 'version',
]);
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
- 'type' => 'update_draft'
+ 'type' => 'update_draft',
]);
$exitCode = Artisan::call('bookstack:clear-revisions', ['--all' => true]);
$this->assertDatabaseMissing('page_revisions', [
'page_id' => $page->id,
- 'type' => 'update_draft'
+ 'type' => 'update_draft',
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Entities\Models\Page;
use Illuminate\Support\Facades\DB;
class ClearViewsCommandTest extends TestCase
{
-
public function test_clear_views_command()
{
$this->asEditor();
$this->get($page->getUrl());
$this->assertDatabaseHas('views', [
- 'user_id' => $this->getEditor()->id,
+ 'user_id' => $this->getEditor()->id,
'viewable_id' => $page->id,
- 'views' => 1
+ 'views' => 1,
]);
DB::rollBack();
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('views', [
- 'user_id' => $this->getEditor()->id
+ 'user_id' => $this->getEditor()->id,
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Entities\Models\Bookshelf;
use Tests\TestCase;
$shelf = Bookshelf::first();
$child = $shelf->books()->first();
$editorRole = $this->getEditor()->roles()->first();
- $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default");
- $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default");
+ $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
+ $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
$this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]);
$this->artisan('bookstack:copy-shelf-permissions', [
]);
$child = $shelf->books()->first();
- $this->assertTrue(boolval($child->restricted), "Child book should now be restricted");
- $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions");
+ $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
+ $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
}
Bookshelf::query()->where('id', '!=', $shelf->id)->delete();
$child = $shelf->books()->first();
$editorRole = $this->getEditor()->roles()->first();
- $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default");
- $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default");
+ $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
+ $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
$this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]);
$this->artisan('bookstack:copy-shelf-permissions --all')
->expectsQuestion('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?', 'y');
$child = $shelf->books()->first();
- $this->assertTrue(boolval($child->restricted), "Child book should now be restricted");
- $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions");
+ $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
+ $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Actions\Comment;
use Tests\TestCase;
'html' => "<p>some_fresh_content</p>\n",
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Models\Page;
$this->assertDatabaseHas('joint_permissions', ['entity_id' => $page->id]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Commands;
+<?php
+
+namespace Tests\Commands;
use BookStack\Entities\Models\Page;
use Symfony\Component\Console\Exception\RuntimeException;
$this->artisan('bookstack:update-url https://example.com https://cats.example.com')
->expectsQuestion("This will search for \"https://example.com\" in your database and replace it with \"https://cats.example.com\".\nAre you sure you want to proceed?", 'y')
- ->expectsQuestion("This operation could cause issues if used incorrectly. Have you made a backup of your existing database?", 'y');
+ ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');
$this->assertDatabaseHas('pages', [
- 'id' => $page->id,
- 'html' => '<a href="https://cats.example.com/donkeys"></a>'
+ 'id' => $page->id,
+ 'html' => '<a href="https://cats.example.com/donkeys"></a>',
]);
}
public function test_command_requires_valid_url()
{
- $badUrlMessage = "The given urls are expected to be full urls starting with http:// or https://";
+ $badUrlMessage = 'The given urls are expected to be full urls starting with http:// or https://';
$this->artisan('bookstack:update-url //example.com https://cats.example.com')->expectsOutput($badUrlMessage);
$this->artisan('bookstack:update-url https://example.com htts://cats.example.com')->expectsOutput($badUrlMessage);
$this->artisan('bookstack:update-url example.com https://cats.example.com')->expectsOutput($badUrlMessage);
{
$this->artisan("bookstack:update-url {$oldUrl} {$newUrl}")
->expectsQuestion("This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\nAre you sure you want to proceed?", 'y')
- ->expectsQuestion("This operation could cause issues if used incorrectly. Have you made a backup of your existing database?", 'y');
+ ->expectsQuestion('This operation could cause issues if used incorrectly. Have you made a backup of your existing database?', 'y');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use Illuminate\Contracts\Console\Kernel;
*/
public function createApplication()
{
- $app = require __DIR__.'/../bootstrap/app.php';
+ $app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
+
return $app;
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
class BookShelfTest extends TestCase
{
-
use UsesImages;
public function test_shelves_shows_in_header_if_have_view_permissions()
{
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10)
+ 'name' => 'My test book' . Str::random(4),
+ 'description' => 'Test book description ' . Str::random(10),
];
$resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
'books' => $booksToInclude->implode('id', ','),
- 'tags' => [
+ 'tags' => [
[
- 'name' => 'Test Category',
+ 'name' => 'Test Category',
'value' => 'Test Tag Value',
- ]
+ ],
],
]));
$resp->assertRedirect();
public function test_shelves_create_sets_cover_image()
{
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10)
+ 'name' => 'My test book' . Str::random(4),
+ 'description' => 'Test book description ' . Str::random(10),
];
$imageFile = $this->getTestImage('shelf-test.png');
$lastImage = Image::query()->orderByDesc('id')->firstOrFail();
$shelf = Bookshelf::query()->where('name', '=', $shelfInfo['name'])->first();
$this->assertDatabaseHas('bookshelves', [
- 'id' => $shelf->id,
+ 'id' => $shelf->id,
'image_id' => $lastImage->id,
]);
$this->assertEquals($lastImage->id, $shelf->cover->id);
// Set book ordering
$this->asAdmin()->put($shelf->getUrl(), [
'books' => $books->implode('id', ','),
- 'tags' => [], 'description' => 'abc', 'name' => 'abc'
+ 'tags' => [], 'description' => 'abc', 'name' => 'abc',
]);
$this->assertEquals(3, $shelf->books()->count());
$shelf->refresh();
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . Str::random(4),
- 'description' => 'Test book description ' . Str::random(10)
+ 'name' => 'My test book' . Str::random(4),
+ 'description' => 'Test book description ' . Str::random(10),
];
$resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
'books' => $booksToInclude->implode('id', ','),
- 'tags' => [
+ 'tags' => [
[
- 'name' => 'Test Category',
+ 'name' => 'Test Category',
'value' => 'Test Tag Value',
- ]
+ ],
],
]));
$shelf = Bookshelf::find($shelf->id);
$testName = 'Test Book in Shelf Name';
$createBookResp = $this->asEditor()->post($shelf->getUrl('/create-book'), [
- 'name' => $testName,
- 'description' => 'Book in shelf description'
+ 'name' => $testName,
+ 'description' => 'Book in shelf description',
]);
$createBookResp->assertRedirect();
$newBook = Book::query()->orderBy('id', 'desc')->first();
$this->assertDatabaseHas('bookshelves_books', [
'bookshelf_id' => $shelf->id,
- 'book_id' => $newBook->id,
+ 'book_id' => $newBook->id,
]);
$resp = $this->asEditor()->get($shelf->getUrl());
$child = $shelf->books()->first();
$editorRole = $this->getEditor()->roles()->first();
- $this->assertFalse(boolval($child->restricted), "Child book should not be restricted by default");
- $this->assertTrue($child->permissions()->count() === 0, "Child book should have no permissions by default");
+ $this->assertFalse(boolval($child->restricted), 'Child book should not be restricted by default');
+ $this->assertTrue($child->permissions()->count() === 0, 'Child book should have no permissions by default');
$this->setEntityRestrictions($shelf, ['view', 'update'], [$editorRole]);
$resp = $this->post($shelf->getUrl('/copy-permissions'));
$child = $shelf->books()->first();
$resp->assertRedirect($shelf->getUrl());
- $this->assertTrue(boolval($child->restricted), "Child book should now be restricted");
- $this->assertTrue($child->permissions()->count() === 2, "Child book should have copied permissions");
+ $this->assertTrue(boolval($child->restricted), 'Child book should now be restricted');
+ $this->assertTrue($child->permissions()->count() === 2, 'Child book should have copied permissions');
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]);
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]);
}
{
// Create shelf
$shelfInfo = [
- 'name' => 'My test shelf' . Str::random(4),
- 'description' => 'Test shelf description ' . Str::random(10)
+ 'name' => 'My test shelf' . Str::random(4),
+ 'description' => 'Test shelf description ' . Str::random(10),
];
$this->asEditor()->post('/shelves', $shelfInfo);
// Create book and add to shelf
$this->asEditor()->post($shelf->getUrl('/create-book'), [
- 'name' => 'Test book name',
- 'description' => 'Book in shelf description'
+ 'name' => 'Test book name',
+ 'description' => 'Book in shelf description',
]);
$newBook = Book::query()->orderBy('id', 'desc')->first();
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use Tests\TestCase;
$resp->assertElementContains('#sibling-navigation', 'Previous');
$resp->assertElementContains('#sibling-navigation', substr($chapter->name, 0, 20));
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Chapter;
use Tests\TestCase;
$redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
$redirectReq->assertNotificationContains('Chapter Successfully Deleted');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Page;
use Tests\TestCase;
$this->asAdmin()->get($this->page->getUrl())
->assertElementExists('.comments-list');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
-use BookStack\Entities\Models\Page;
use BookStack\Actions\Comment;
+use BookStack\Entities\Models\Page;
use Tests\TestCase;
class CommentTest extends TestCase
{
-
public function test_add_comment()
{
$this->asAdmin();
$pageResp->assertSee($comment->text);
$this->assertDatabaseHas('comments', [
- 'local_id' => 1,
- 'entity_id' => $page->id,
+ 'local_id' => 1,
+ 'entity_id' => $page->id,
'entity_type' => Page::newModelInstance()->getMorphClass(),
- 'text' => $comment->text,
- 'parent_id' => 2
+ 'text' => $comment->text,
+ 'parent_id' => 2,
]);
}
$resp->assertDontSee($comment->text);
$this->assertDatabaseHas('comments', [
- 'text' => $newText,
- 'entity_id' => $page->id
+ 'text' => $newText,
+ 'entity_id' => $page->id,
]);
}
$resp->assertStatus(200);
$this->assertDatabaseMissing('comments', [
- 'id' => $comment->id
+ 'id' => $comment->id,
]);
}
]);
$this->assertDatabaseHas('comments', [
- 'entity_id' => $page->id,
+ 'entity_id' => $page->id,
'entity_type' => $page->getMorphClass(),
- 'text' => '# My Title',
- 'html' => "<h1>My Title</h1>\n",
+ 'text' => '# My Title',
+ 'html' => "<h1>My Title</h1>\n",
]);
$pageView = $this->get($page->getUrl());
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Actions\Tag;
use BookStack\Entities\Models\Book;
class EntitySearchTest extends TestCase
{
-
public function test_page_search()
{
$book = Book::all()->first();
{
$newTags = [
new Tag([
- 'name' => 'animal',
- 'value' => 'cat'
+ 'name' => 'animal',
+ 'value' => 'cat',
]),
new Tag([
- 'name' => 'color',
- 'value' => 'red'
- ])
+ 'name' => 'color',
+ 'value' => 'red',
+ ]),
];
$pageA = Page::first();
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertDontSee($page->name);
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
- $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorSlug.'}'))->assertDontSee($page->name);
+ $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertDontSee($page->name);
$page->created_by = $editorId;
$page->save();
$this->get('/search?term=' . urlencode('danzorbhsing {created_by:me}'))->assertSee($page->name);
- $this->get('/search?term=' . urlencode('danzorbhsing {created_by: '.$editorSlug.'}'))->assertSee($page->name);
+ $this->get('/search?term=' . urlencode('danzorbhsing {created_by: ' . $editorSlug . '}'))->assertSee($page->name);
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertDontSee($page->name);
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
$page->updated_by = $editorId;
$page->save();
$this->get('/search?term=' . urlencode('danzorbhsing {updated_by:me}'))->assertSee($page->name);
- $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:'.$editorSlug.'}'))->assertSee($page->name);
+ $this->get('/search?term=' . urlencode('danzorbhsing {updated_by:' . $editorSlug . '}'))->assertSee($page->name);
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertDontSee($page->name);
$page->owned_by = $editorId;
$page->save();
$this->get('/search?term=' . urlencode('danzorbhsing {owned_by:me}'))->assertSee($page->name);
- $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:'.$editorSlug.'}'))->assertSee($page->name);
+ $this->get('/search?term=' . urlencode('danzorbhsing {owned_by:' . $editorSlug . '}'))->assertSee($page->name);
// Content filters
$this->get('/search?term=' . urlencode('{in_name:danzorbhsing}'))->assertDontSee($page->name);
-<?php namespace Tests\Entity;
+<?php
-use BookStack\Entities\Models\Bookshelf;
+namespace Tests\Entity;
+
+use BookStack\Auth\UserRepo;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
-use BookStack\Auth\UserRepo;
use BookStack\Entities\Repos\PageRepo;
use Carbon\Carbon;
use Tests\BrowserKitTest;
class EntityTest extends BrowserKitTest
{
-
public function test_entity_creation()
{
// Test Creation
public function test_book_sort_page_shows()
{
- $books = Book::all();
+ $books = Book::all();
$bookToSort = $books[0];
$this->asAdmin()
->visit($bookToSort->getUrl())
public function test_book_sort_item_returns_book_content()
{
- $books = Book::all();
+ $books = Book::all();
$bookToSort = $books[0];
$firstPage = $bookToSort->pages[0];
$firstChapter = $bookToSort->chapters[0];
->submitForm('Grid View')
->seePageIs('/books')
->pageHasElement('.featured-image-container');
-
}
public function pageCreation($chapter)
{
$page = factory(Page::class)->make([
- 'name' => 'My First Page'
+ 'name' => 'My First Page',
]);
$this->asAdmin()
->see($page->name);
$page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
+
return $page;
}
public function chapterCreation(Book $book)
{
$chapter = factory(Chapter::class)->make([
- 'name' => 'My First Chapter'
+ 'name' => 'My First Chapter',
]);
$this->asAdmin()
->see($chapter->name)->see($chapter->description);
$chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
+
return $chapter;
}
public function bookCreation()
{
$book = factory(Book::class)->make([
- 'name' => 'My First Book'
+ 'name' => 'My First Book',
]);
$this->asAdmin()
->visit('/books')
$this->assertMatchesRegularExpression($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
$book = Book::where('slug', '=', 'my-first-book')->first();
+
return $book;
}
{
$page = Page::orderBy('updated_at', 'asc')->first();
Page::where('id', '!=', $page->id)->update([
- 'updated_at' => Carbon::now()->subSecond(1)
+ 'updated_at' => Carbon::now()->subSecond(1),
]);
$this->asAdmin()->visit('/')
->dontSeeInElement('#recently-updated-pages', $page->name);
public function test_slug_multi_byte_url_safe()
{
$book = $this->newBook([
- 'name' => 'информация'
+ 'name' => 'информация',
]);
$this->assertEquals('informatsiya', $book->slug);
$book = $this->newBook([
- 'name' => '¿Qué?'
+ 'name' => '¿Qué?',
]);
$this->assertEquals('que', $book->slug);
public function test_slug_format()
{
$book = $this->newBook([
- 'name' => 'PartA / PartB / PartC'
+ 'name' => 'PartA / PartB / PartC',
]);
$this->assertEquals('parta-partb-partc', $book->slug);
->submitForm('Confirm')
->seePageIs($chapter->getUrl());
}
-
}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
class ExportTest extends TestCase
{
-
public function test_page_text_export()
{
$page = Page::query()->first();
{
$page = Page::query()->first();
- $customHeadContent = "<style>p{color: red;}</style>";
+ $customHeadContent = '<style>p{color: red;}</style>';
$this->setSettings(['app-custom-head' => $customHeadContent]);
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
{
$page = Page::query()->first();
- $customHeadContent = "<!-- A comment -->";
+ $customHeadContent = '<!-- A comment -->';
$this->setSettings(['app-custom-head' => $customHeadContent]);
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
{
$page = Page::query()->first();
$page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg"/>'
- .'<img src="http://localhost/uploads/svg_test.svg"/>'
- .'<img src="/uploads/svg_test.svg"/>';
+ . '<img src="http://localhost/uploads/svg_test.svg"/>'
+ . '<img src="/uploads/svg_test.svg"/>';
$storageDisk = Storage::disk('local');
$storageDisk->makeDirectory('uploads/images/gallery');
$storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');
{
$page = Page::query()->first()->forceFill([
'markdown' => '# A header',
- 'html' => '<h1>Dogcat</h1>',
+ 'html' => '<h1>Dogcat</h1>',
]);
$page->save();
{
$page = Page::query()->first()->forceFill([
'markdown' => '',
- 'html' => "<h1>Dogcat</h1><p>Some <strong>bold</strong> text</p>",
+ 'html' => '<h1>Dogcat</h1><p>Some <strong>bold</strong> text</p>',
]);
$page->save();
{
$page = Page::query()->first()->forceFill([
'markdown' => '',
- 'html' => "<h1>Dogcat</h1><p class=\"callout info\">Some callout text</p><p>Another line</p>",
+ 'html' => '<h1>Dogcat</h1><p class="callout info">Some callout text</p><p>Another line</p>',
]);
$page->save();
{
$page = Page::query()->first()->forceFill([
'markdown' => '',
- 'html' => '<h1>Dogcat</h1>'."\r\n".'<pre id="bkmrk-var-a-%3D-%27cat%27%3B"><code class="language-JavaScript">var a = \'cat\';</code></pre><p>Another line</p>',
+ 'html' => '<h1>Dogcat</h1>' . "\r\n" . '<pre id="bkmrk-var-a-%3D-%27cat%27%3B"><code class="language-JavaScript">var a = \'cat\';</code></pre><p>Another line</p>',
]);
$page->save();
$resp->assertSee('# ' . $chapter->name);
$resp->assertSee('# ' . $page->name);
}
-
}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use Tests\BrowserKitTest;
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
->pageHasElement('#html-editor');
}
-
+
public function test_markdown_setting_shows_markdown_editor()
{
$this->setMarkdownEditor();
$this->asAdmin()->visit($this->page->getUrl() . '/edit')
->seeInField('markdown', $this->page->html);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
-use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\PageContent;
use Tests\TestCase;
use Tests\Uploads\UsesImages;
$pageView->assertElementNotContains('.page-content', '<script>');
$pageView->assertElementNotContains('.page-content', '</script>');
}
-
}
public function test_iframe_js_and_base64_urls_are_removed()
'<iframe SRC=" javascript: alert(document.cookie)"></iframe>',
'<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<iframe src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
- '<iframe srcdoc="<script>window.alert(document.cookie)</script>"></iframe>'
+ '<iframe srcdoc="<script>window.alert(document.cookie)</script>"></iframe>',
];
$this->asEditor();
$pageView->assertElementNotContains('.page-content', 'data:');
$pageView->assertElementNotContains('.page-content', 'base64');
}
-
}
public function test_javascript_uri_links_are_removed()
{
$checks = [
'<a id="xss" href="javascript:alert(document.cookie)>Click me</a>',
- '<a id="xss" href="javascript: alert(document.cookie)>Click me</a>'
+ '<a id="xss" href="javascript: alert(document.cookie)>Click me</a>',
];
$this->asEditor();
$pageView->assertElementNotContains('.page-content', 'href=javascript:');
}
}
+
public function test_form_actions_with_javascript_are_removed()
{
$checks = [
'<form><input id="xss" type=submit formaction=javascript:alert(document.domain) value=Submit><input></form>',
'<form ><button id="xss" formaction=javascript:alert(document.domain)>Click me</button></form>',
- '<form id="xss" action=javascript:alert(document.domain)><input type=submit value=Submit></form>'
+ '<form id="xss" action=javascript:alert(document.domain)><input type=submit value=Submit></form>',
];
$this->asEditor();
$pageView->assertElementNotContains('.page-content', 'formaction=javascript:');
}
}
-
+
public function test_metadata_redirects_are_removed()
{
$checks = [
$pageView->assertElementNotContains('.page-content', 'external_url');
}
}
+
public function test_page_inline_on_attributes_removed_by_default()
{
$this->asEditor();
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', 'onclick');
}
-
}
public function test_page_content_scripts_show_when_configured()
$pageA->html = $content;
$pageA->save();
- $pageB->html = '<ul id="bkmrk-xxx-%28"></ul> <p>{{@'. $pageA->id .'#test}}</p>';
+ $pageB->html = '<ul id="bkmrk-xxx-%28"></ul> <p>{{@' . $pageA->id . '#test}}</p>';
$pageB->save();
$pageView = $this->get($pageB->getUrl());
$content = '<ul id="bkmrk-test"><li>test a</li><li><ul id="bkmrk-test"><li>test b</li></ul></li></ul>';
$pageSave = $this->put($page->getUrl(), [
- 'name' => $page->name,
- 'html' => $content,
- 'summary' => ''
+ 'name' => $page->name,
+ 'html' => $content,
+ 'summary' => '',
]);
$pageSave->assertRedirect();
$updatedPage = Page::query()->where('id', '=', $page->id)->first();
- $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
+ $this->assertEquals(substr_count($updatedPage->html, 'bkmrk-test"'), 1);
}
public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save()
$content = '<h1 id="non-standard-id">test</h1><p><a href="#non-standard-id">link</a></p>';
$this->put($page->getUrl(), [
- 'name' => $page->name,
- 'html' => $content,
- 'summary' => ''
+ 'name' => $page->name,
+ 'html' => $content,
+ 'summary' => '',
]);
$updatedPage = Page::query()->where('id', '=', $page->id)->first();
$this->assertCount(3, $navMap);
$this->assertArrayMapIncludes([
'nodeName' => 'h1',
- 'link' => '#testa',
- 'text' => 'Hello',
- 'level' => 1,
+ 'link' => '#testa',
+ 'text' => 'Hello',
+ 'level' => 1,
], $navMap[0]);
$this->assertArrayMapIncludes([
'nodeName' => 'h2',
- 'link' => '#testb',
- 'text' => 'There',
- 'level' => 2,
+ 'link' => '#testb',
+ 'text' => 'There',
+ 'level' => 2,
], $navMap[1]);
$this->assertArrayMapIncludes([
'nodeName' => 'h3',
- 'link' => '#testc',
- 'text' => 'Donkey',
- 'level' => 3,
+ 'link' => '#testc',
+ 'text' => 'Donkey',
+ 'level' => 3,
], $navMap[2]);
}
$this->assertCount(1, $navMap);
$this->assertArrayMapIncludes([
'nodeName' => 'h1',
- 'link' => '#testa',
- 'text' => 'Hello'
+ 'link' => '#testa',
+ 'text' => 'Hello',
], $navMap[0]);
}
$this->assertCount(3, $navMap);
$this->assertArrayMapIncludes([
'nodeName' => 'h4',
- 'level' => 1,
+ 'level' => 1,
], $navMap[0]);
$this->assertArrayMapIncludes([
'nodeName' => 'h5',
- 'level' => 2,
+ 'level' => 2,
], $navMap[1]);
$this->assertArrayMapIncludes([
'nodeName' => 'h6',
- 'level' => 3,
+ 'level' => 3,
], $navMap[2]);
}
| Paragraph | Text |';
$this->put($page->getUrl(), [
'name' => $page->name, 'markdown' => $content,
- 'html' => '', 'summary' => ''
+ 'html' => '', 'summary' => '',
]);
$page->refresh();
- [x] Item b';
$this->put($page->getUrl(), [
'name' => $page->name, 'markdown' => $content,
- 'html' => '', 'summary' => ''
+ 'html' => '', 'summary' => '',
]);
$page->refresh();
$content = '~~some crossed out text~~';
$this->put($page->getUrl(), [
'name' => $page->name, 'markdown' => $content,
- 'html' => '', 'summary' => ''
+ 'html' => '', 'summary' => '',
]);
$page->refresh();
$content = '<!-- Test Comment -->';
$this->put($page->getUrl(), [
'name' => $page->name, 'markdown' => $content,
- 'html' => '', 'summary' => ''
+ 'html' => '', 'summary' => '',
]);
$page->refresh();
$this->put($page->getUrl(), [
'name' => $page->name, 'summary' => '',
- 'html' => '<p>test<img src="data:image/jpeg;base64,'.$this->base64Jpeg.'"/></p>',
+ 'html' => '<p>test<img src="data:image/jpeg;base64,' . $this->base64Jpeg . '"/></p>',
]);
$page->refresh();
$base64PngWithoutWhitespace = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQAB';
$this->put($page->getUrl(), [
'name' => $page->name, 'summary' => '',
- 'html' => '<p>test<img src="data:image/png;base64,'.$base64PngWithWhitespace.'"/></p>',
+ 'html' => '<p>test<img src="data:image/png;base64,' . $base64PngWithWhitespace . '"/></p>',
]);
$page->refresh();
$this->put($page->getUrl(), [
'name' => $page->name, 'summary' => '',
- 'html' => '<p>test<img src="data:image/jiff;base64,'.$this->base64Jpeg.'"/></p>',
+ 'html' => '<p>test<img src="data:image/jiff;base64,' . $this->base64Jpeg . '"/></p>',
]);
$page->refresh();
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
$this->actingAs($newUser)
->visit($this->page->getUrl('/edit'))
->see('Admin has started editing this page');
- $this->flushSession();
+ $this->flushSession();
$this->visit($nonEditedPage->getUrl() . '/edit')
->dontSeeInElement('.notification', 'Admin has started editing this page');
}
'html' => $page->html,
]);
}
-
}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
$page = Page::first();
$pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
$pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
- $page = Page::find($page->id);
-
+ $page = Page::find($page->id);
$pageView = $this->get($page->getUrl());
$pageView->assertDontSee('abc123');
$revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
$restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
- $page = Page::find($page->id);
+ $page = Page::find($page->id);
$restoreReq->assertStatus(302);
$restoreReq->assertRedirect($page->getUrl());
$pageView = $this->get($page->getUrl());
$this->assertDatabaseHas('pages', [
- 'id' => $page->id,
+ 'id' => $page->id,
'markdown' => '## New Content def456',
]);
$pageView->assertSee('abc123');
$this->assertDatabaseHas('page_revisions', [
'page_id' => $page->id,
- 'text' => 'new contente def456',
- 'type' => 'version',
+ 'text' => 'new contente def456',
+ 'type' => 'version',
'summary' => "Restored from #{$revToRestore->id}; My first update",
]);
}
$resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$resp->assertStatus(302);
- $this->assertTrue(Page::find($page->id)->revision_count === $startCount+1);
+ $this->assertTrue(Page::find($page->id)->revision_count === $startCount + 1);
}
public function test_revision_count_shown_in_page_meta()
$pageView->assertSee('Revision #' . $page->revision_count);
}
- public function test_revision_deletion() {
+ public function test_revision_deletion()
+ {
$page = Page::first();
$this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$revisionCount = $page->revisions()->count();
$this->assertEquals(12, $revisionCount);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Page;
use Tests\TestCase;
$this->actingAs($editor);
$pageUpdateData = [
- 'name' => $page->name,
- 'html' => $page->html,
+ 'name' => $page->name,
+ 'html' => $page->html,
'template' => 'true',
];
$this->put($page->getUrl(), $pageUpdateData);
$this->assertDatabaseHas('pages', [
- 'id' => $page->id,
+ 'id' => $page->id,
'template' => false,
]);
$this->put($page->getUrl(), $pageUpdateData);
$this->assertDatabaseHas('pages', [
- 'id' => $page->id,
+ 'id' => $page->id,
'template' => true,
]);
}
$templateFetch = $this->get('/templates/' . $page->id);
$templateFetch->assertStatus(200);
$templateFetch->assertJson([
- 'html' => $content,
+ 'html' => $content,
'markdown' => '',
]);
}
$templatesFetch->assertSee($page->name);
$templatesFetch->assertSee('pagination');
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
class PageTest extends TestCase
{
-
public function test_page_view_when_creator_is_deleted_but_owner_exists()
{
$page = Page::query()->first();
$details = [
'markdown' => '# a title',
- 'html' => '<h1>a title</h1>',
- 'name' => 'my page',
+ 'html' => '<h1>a title</h1>',
+ 'name' => 'my page',
];
$resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details);
$resp->assertRedirect();
$this->assertDatabaseHas('pages', [
'markdown' => $details['markdown'],
- 'name' => $details['name'],
- 'id' => $draft->id,
- 'draft' => false
+ 'name' => $details['name'],
+ 'id' => $draft->id,
+ 'draft' => false,
]);
$draft->refresh();
- $resp = $this->get($draft->getUrl("/edit"));
- $resp->assertSee("# a title");
+ $resp = $this->get($draft->getUrl('/edit'));
+ $resp->assertSee('# a title');
}
public function test_page_delete()
$movePageResp = $this->post($page->getUrl('/copy'), [
'entity_selection' => 'book:' . $newBook->id,
- 'name' => 'My copied test page'
+ 'name' => 'My copied test page',
]);
$pageCopy = Page::where('name', '=', 'My copied test page')->first();
$this->asEditor()->post($page->getUrl('/copy'), [
'entity_selection' => 'book:' . $newBook->id,
- 'name' => 'My copied test page'
+ 'name' => 'My copied test page',
]);
$pageCopy = Page::where('name', '=', 'My copied test page')->first();
$resp->assertSee('Copy Page');
$movePageResp = $this->post($page->getUrl('/copy'), [
- 'name' => 'My copied test page'
+ 'name' => 'My copied test page',
]);
$pageCopy = Page::where('name', '=', 'My copied test page')->first();
$movePageResp = $this->post($page->getUrl('/copy'), [
'entity_selection' => 'book:' . $newBook->id,
- 'name' => 'My copied test page'
+ 'name' => 'My copied test page',
]);
$movePageResp->assertRedirect();
$this->assertDatabaseHas('pages', [
- 'name' => 'My copied test page',
+ 'name' => 'My copied test page',
'created_by' => $viewer->id,
- 'book_id' => $newBook->id,
+ 'book_id' => $newBook->id,
]);
}
->where('draft', '=', true)->first();
$details = [
- 'name' => 'my page',
+ 'name' => 'my page',
'markdown' => '',
];
$resp = $this->post($book->getUrl("/draft/{$draft->id}"), $details);
$this->assertDatabaseHas('pages', [
'markdown' => $details['markdown'],
- 'id' => $draft->id,
- 'draft' => false
+ 'id' => $draft->id,
+ 'draft' => false,
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Tools\SearchOptions;
use Tests\TestCase;
public function test_to_string_includes_all_items_in_the_correct_format()
{
$expected = 'cat "dog" [tag=good] {is_tree}';
- $options = new SearchOptions;
+ $options = new SearchOptions();
$options->searches = ['cat'];
$options->exacts = ['dog'];
$options->tags = ['tag=good'];
$this->assertEquals([
'is_tree' => '',
- 'name' => 'dan',
- 'cat' => 'happy',
+ 'name' => 'dan',
+ 'cat' => 'happy',
], $opts->filters);
}
}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
$resp->assertSee('Move Page');
$movePageResp = $this->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$page = Page::find($page->id);
$newChapter = $newBook->chapters()->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
- 'entity_selection' => 'chapter:' . $newChapter->id
+ 'entity_selection' => 'chapter:' . $newChapter->id,
]);
$page = Page::find($page->id);
$newBook = Book::where('id', '!=', $oldChapter->book_id)->first();
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$page->refresh();
$this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], $editor->roles->all());
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$this->assertPermissionError($movePageResp);
$this->setEntityRestrictions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles->all());
$movePageResp = $this->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$page = Page::find($page->id);
$this->setEntityRestrictions($page, ['view', 'update', 'create'], $editor->roles->all());
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$this->assertPermissionError($movePageResp);
$pageView = $this->get($page->getUrl());
$this->setEntityRestrictions($page, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$movePageResp = $this->put($page->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$page = Page::find($page->id);
$chapterMoveResp->assertSee('Move Chapter');
$moveChapterResp = $this->put($chapter->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$chapter = Chapter::find($chapter->id);
$this->setEntityRestrictions($chapter, ['view', 'update', 'create'], $editor->roles->all());
$moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$this->assertPermissionError($moveChapterResp);
$pageView = $this->get($chapter->getUrl());
$this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles->all());
$moveChapterResp = $this->put($chapter->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$chapter = Chapter::find($chapter->id);
$pageToCheck->delete();
$this->asEditor()->put($chapter->getUrl('/move'), [
- 'entity_selection' => 'book:' . $newBook->id
+ 'entity_selection' => 'book:' . $newBook->id,
]);
$pageToCheck->refresh();
// Create request data
$reqData = [
[
- 'id' => $chapterToMove->id,
- 'sort' => 0,
+ 'id' => $chapterToMove->id,
+ 'sort' => 0,
'parentChapter' => false,
- 'type' => 'chapter',
- 'book' => $newBook->id
- ]
+ 'type' => 'chapter',
+ 'book' => $newBook->id,
+ ],
];
foreach ($pagesToMove as $index => $page) {
$reqData[] = [
- 'id' => $page->id,
- 'sort' => $index,
+ 'id' => $page->id,
+ 'sort' => $index,
'parentChapter' => $index === count($pagesToMove) - 1 ? $chapterToMove->id : false,
- 'type' => 'page',
- 'book' => $newBook->id
+ 'type' => 'page',
+ 'book' => $newBook->id,
];
}
$sortResp->assertRedirect($newBook->getUrl());
$sortResp->assertStatus(302);
$this->assertDatabaseHas('chapters', [
- 'id' => $chapterToMove->id,
- 'book_id' => $newBook->id,
- 'priority' => 0
+ 'id' => $chapterToMove->id,
+ 'book_id' => $newBook->id,
+ 'priority' => 0,
]);
$this->assertTrue($newBook->chapters()->count() === 1);
$this->assertTrue($newBook->chapters()->first()->pages()->count() === 1);
$checkResp = $this->get(Page::find($checkPage->id)->getUrl());
$checkResp->assertSee($newBook->name);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Entity;
+<?php
+
+namespace Tests\Entity;
use BookStack\Actions\Tag;
use BookStack\Entities\Models\Entity;
class TagTest extends TestCase
{
-
protected $defaultTagCount = 20;
/**
}
$entity->tags()->saveMany($tags);
+
return $entity;
}
];
$page = $this->getEntityWithTags(Page::class, $tags);
- $resp = $this->asEditor()->get("/search?term=[category]");
+ $resp = $this->asEditor()->get('/search?term=[category]');
$resp->assertSee($page->name);
$resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'category');
$resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'buckets');
$resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'color');
$resp->assertElementContains('[href="' . $page->getUrl() . '"]', 'red');
}
-
}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Entities\Models\Book;
use Illuminate\Support\Facades\Log;
class ErrorTest extends TestCase
{
-
public function test_404_page_does_not_show_login()
{
// Due to middleware being handled differently this will not fail
$resp->assertStatus(404);
$resp->assertSeeText('Image Not Found');
}
-}
\ No newline at end of file
+}
class FavouriteTest extends TestCase
{
-
public function test_page_add_favourite_flow()
{
$page = Page::query()->first();
$resp = $this->post('/favourites/add', [
'type' => get_class($page),
- 'id' => $page->id,
+ 'id' => $page->id,
]);
$resp->assertRedirect($page->getUrl());
$resp->assertSessionHas('success', "\"{$page->name}\" has been added to your favourites");
$this->assertDatabaseHas('favourites', [
- 'user_id' => $editor->id,
+ 'user_id' => $editor->id,
'favouritable_type' => $page->getMorphClass(),
- 'favouritable_id' => $page->id,
+ 'favouritable_id' => $page->id,
]);
}
$page = Page::query()->first();
$editor = $this->getEditor();
Favourite::query()->forceCreate([
- 'user_id' => $editor->id,
- 'favouritable_id' => $page->id,
+ 'user_id' => $editor->id,
+ 'favouritable_id' => $page->id,
'favouritable_type' => $page->getMorphClass(),
]);
$resp = $this->post('/favourites/remove', [
'type' => get_class($page),
- 'id' => $page->id,
+ 'id' => $page->id,
]);
$resp->assertRedirect($page->getUrl());
$resp->assertSessionHas('success', "\"{$page->name}\" has been removed from your favourites");
/** @var Page $page */
$page = Page::query()->first();
- $page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id]));
+ $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id]));
$resp = $this->get('/');
$resp->assertElementExists('#top-favourites');
$resp = $this->actingAs($editor)->get('/favourites');
$resp->assertDontSee($page->name);
- $page->favourites()->save((new Favourite)->forceFill(['user_id' => $editor->id]));
+ $page->favourites()->save((new Favourite())->forceFill(['user_id' => $editor->id]));
$resp = $this->get('/favourites');
$resp->assertSee($page->name);
$resp = $this->get('/favourites?page=2');
$resp->assertDontSee($page->name);
}
-
-}
\ No newline at end of file
+}
class FooterLinksTest extends TestCase
{
-
public function test_saving_setting()
{
- $resp = $this->asAdmin()->post("/settings", [
+ $resp = $this->asAdmin()->post('/settings', [
'setting-app-footer-links' => [
['label' => 'My custom link 1', 'url' => 'https://example.com/1'],
['label' => 'My custom link 2', 'url' => 'https://example.com/2'],
$resp->assertElementContains('footer a[href="https://example.com/privacy"]', 'Privacy Policy');
$resp->assertElementContains('footer a[href="https://example.com/terms"]', 'Terms of Service');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Bookshelf;
-use BookStack\Entities\Models\Page;
class HomepageTest extends TestCase
{
-
public function test_default_homepage_visible()
{
$this->asEditor();
$content = str_repeat('This is the body content of my custom homepage.', 20);
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings([
- 'app-homepage' => $customPage->id,
- 'app-homepage-type' => 'page'
+ 'app-homepage' => $customPage->id,
+ 'app-homepage-type' => 'page',
]);
$homeVisit = $this->get('/');
$content = str_repeat('This is the body content of my custom homepage.', 20);
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings([
- 'app-homepage' => $customPage->id,
- 'app-homepage-type' => 'default'
+ 'app-homepage' => $customPage->id,
+ 'app-homepage-type' => 'default',
]);
$pageDeleteReq = $this->delete($customPage->getUrl());
-<?php namespace Tests;
+<?php
+
+namespace Tests;
class LanguageTest extends TestCase
{
-
protected $langs;
/**
foreach ($this->langs as $lang) {
foreach ($files as $file) {
$loadError = false;
+
try {
$translations = trans(str_replace('.php', '', $file), [], $lang);
} catch (\Exception $e) {
public function test_rtl_config_set_if_lang_is_rtl()
{
$this->asEditor();
- $this->assertFalse(config('app.rtl'), "App RTL config should be false by default");
+ $this->assertFalse(config('app.rtl'), 'App RTL config should be false by default');
setting()->putUser($this->getEditor(), 'language', 'ar');
$this->get('/');
- $this->assertTrue(config('app.rtl'), "App RTL config should have been set to true by middleware");
+ $this->assertTrue(config('app.rtl'), 'App RTL config should have been set to true by middleware');
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
return $tags;
}
-
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Permissions;
+<?php
+
+namespace Tests\Permissions;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
-use Illuminate\Support\Str;
use Tests\TestCase;
class EntityOwnerChangeTest extends TestCase
{
-
public function test_changing_page_owner()
{
$page = Page::query()->first();
$this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]);
$this->assertDatabaseHas('bookshelves', ['owned_by' => $user->id, 'id' => $shelf->id]);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Permissions;
+<?php
+namespace Tests\Permissions;
+
+use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
-use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use Illuminate\Support\Str;
use Tests\BrowserKitTest;
class EntityPermissionsTest extends BrowserKitTest
{
-
/**
* @var User
*/
$this->setRestrictionsForTestRoles($chapter, ['view', 'create']);
-
$this->visit($chapterUrl . '/create-page')
->type('test page', 'name')
->type('test content', 'html')
->press('Save Permissions')
->seeInDatabase('bookshelves', ['id' => $shelf->id, 'restricted' => true])
->seeInDatabase('entity_permissions', [
- 'restrictable_id' => $shelf->id,
+ 'restrictable_id' => $shelf->id,
'restrictable_type' => Bookshelf::newModelInstance()->getMorphClass(),
- 'role_id' => '2',
- 'action' => 'view'
+ 'role_id' => '2',
+ 'action' => 'view',
]);
}
->press('Save Permissions')
->seeInDatabase('books', ['id' => $book->id, 'restricted' => true])
->seeInDatabase('entity_permissions', [
- 'restrictable_id' => $book->id,
+ 'restrictable_id' => $book->id,
'restrictable_type' => Book::newModelInstance()->getMorphClass(),
- 'role_id' => '2',
- 'action' => 'view'
+ 'role_id' => '2',
+ 'action' => 'view',
]);
}
->press('Save Permissions')
->seeInDatabase('chapters', ['id' => $chapter->id, 'restricted' => true])
->seeInDatabase('entity_permissions', [
- 'restrictable_id' => $chapter->id,
+ 'restrictable_id' => $chapter->id,
'restrictable_type' => Chapter::newModelInstance()->getMorphClass(),
- 'role_id' => '2',
- 'action' => 'update'
+ 'role_id' => '2',
+ 'action' => 'update',
]);
}
->press('Save Permissions')
->seeInDatabase('pages', ['id' => $page->id, 'restricted' => true])
->seeInDatabase('entity_permissions', [
- 'restrictable_id' => $page->id,
+ 'restrictable_id' => $page->id,
'restrictable_type' => Page::newModelInstance()->getMorphClass(),
- 'role_id' => '2',
- 'action' => 'delete'
+ 'role_id' => '2',
+ 'action' => 'delete',
]);
}
$this->actingAs($this->user)->visit($firstBook->getUrl() . '/sort');
}
- public function test_book_sort_permission() {
+ public function test_book_sort_permission()
+ {
$firstBook = Book::first();
$secondBook = Book::find(2);
// Create request data
$reqData = [
[
- 'id' => $firstBookChapter->id,
- 'sort' => 0,
+ 'id' => $firstBookChapter->id,
+ 'sort' => 0,
'parentChapter' => false,
- 'type' => 'chapter',
- 'book' => $secondBook->id
- ]
+ 'type' => 'chapter',
+ 'book' => $secondBook->id,
+ ],
];
// Move chapter from first book to a second book
$reqData = [
[
- 'id' => $secondBookChapter->id,
- 'sort' => 0,
+ 'id' => $secondBookChapter->id,
+ 'sort' => 0,
'parentChapter' => false,
- 'type' => 'chapter',
- 'book' => $firstBook->id
- ]
+ 'type' => 'chapter',
+ 'book' => $firstBook->id,
+ ],
];
// Move chapter from second book to first book
-<?php namespace Tests\Permissions;
+<?php
+
+namespace Tests\Permissions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
class ExportPermissionsTest extends TestCase
{
-
public function test_page_content_without_view_access_hidden_on_chapter_export()
{
$chapter = Chapter::query()->first();
$resp->assertDontSee($pageContent);
}
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Permissions;
+<?php
+
+namespace Tests\Permissions;
use BookStack\Actions\Comment;
+use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
-use BookStack\Auth\Role;
use BookStack\Uploads\Image;
use Laravel\BrowserKitTesting\HttpException;
use Tests\BrowserKitTest;
$editUrl = '/settings/users/' . $adminUser->id;
$this->actingAs($adminUser)->put($editUrl, [
- 'name' => $adminUser->name,
+ 'name' => $adminUser->name,
'email' => $adminUser->email,
'roles' => [
'viewer' => strval($viewerRole->id),
- ]
+ ],
])->followRedirects();
$this->seePageIs($editUrl);
public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision()
{
- $usersLink = 'href="'.url('/settings/users') . '"';
+ $usersLink = 'href="' . url('/settings/users') . '"';
$this->actingAs($this->user)->visit('/')->dontSee($usersLink);
$this->giveUserPermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->visit('/')->see($usersLink);
->assertResponseOk()
->seeElement('input[name=email][disabled]');
$this->put($userProfileUrl, [
- 'name' => 'my_new_name',
+ 'name' => 'my_new_name',
]);
$this->seeInDatabase('users', [
- 'id' => $this->user->id,
+ 'id' => $this->user->id,
'email' => $originalEmail,
- 'name' => 'my_new_name',
+ 'name' => 'my_new_name',
]);
$this->giveUserPermissions($this->user, ['users-manage']);
->dontSeeElement('input[name=email][disabled]')
->seeElement('input[name=email]');
$this->put($userProfileUrl, [
- 'name' => 'my_new_name_2',
+ 'name' => 'my_new_name_2',
]);
$this->seeInDatabase('users', [
- 'id' => $this->user->id,
+ 'id' => $this->user->id,
- 'name' => 'my_new_name_2',
+ 'name' => 'my_new_name_2',
]);
}
}
/**
- * Check a standard entity access permission
+ * Check a standard entity access permission.
+ *
* @param string $permission
- * @param array $accessUrls Urls that are only accessible after having the permission
- * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission
+ * @param array $accessUrls Urls that are only accessible after having the permission
+ * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission
*/
private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
{
}
foreach ($visibles as $url => $text) {
$this->actingAs($this->user)->visit($url)
- ->dontSeeInElement('.action-buttons',$text);
+ ->dontSeeInElement('.action-buttons', $text);
}
$this->giveUserPermissions($this->user, [$permission]);
public function test_bookshelves_create_all_permissions()
{
$this->checkAccessPermission('bookshelf-create-all', [
- '/create-shelf'
+ '/create-shelf',
], [
- '/shelves' => 'New Shelf'
+ '/shelves' => 'New Shelf',
]);
$this->visit('/create-shelf')
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-update-own', [
- $ownShelf->getUrl('/edit')
+ $ownShelf->getUrl('/edit'),
], [
- $ownShelf->getUrl() => 'Edit'
+ $ownShelf->getUrl() => 'Edit',
]);
$this->visit($otherShelf->getUrl())
{
$otherShelf = Bookshelf::first();
$this->checkAccessPermission('bookshelf-update-all', [
- $otherShelf->getUrl('/edit')
+ $otherShelf->getUrl('/edit'),
], [
- $otherShelf->getUrl() => 'Edit'
+ $otherShelf->getUrl() => 'Edit',
]);
}
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-delete-own', [
- $ownShelf->getUrl('/delete')
+ $ownShelf->getUrl('/delete'),
], [
- $ownShelf->getUrl() => 'Delete'
+ $ownShelf->getUrl() => 'Delete',
]);
$this->visit($otherShelf->getUrl())
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
$otherShelf = Bookshelf::first();
$this->checkAccessPermission('bookshelf-delete-all', [
- $otherShelf->getUrl('/delete')
+ $otherShelf->getUrl('/delete'),
], [
- $otherShelf->getUrl() => 'Delete'
+ $otherShelf->getUrl() => 'Delete',
]);
$this->visit($otherShelf->getUrl())->visit($otherShelf->getUrl('/delete'))
public function test_books_create_all_permissions()
{
$this->checkAccessPermission('book-create-all', [
- '/create-book'
+ '/create-book',
], [
- '/books' => 'Create New Book'
+ '/books' => 'Create New Book',
]);
$this->visit('/create-book')
$otherBook = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('book-update-own', [
- $ownBook->getUrl() . '/edit'
+ $ownBook->getUrl() . '/edit',
], [
- $ownBook->getUrl() => 'Edit'
+ $ownBook->getUrl() => 'Edit',
]);
$this->visit($otherBook->getUrl())
{
$otherBook = Book::take(1)->get()->first();
$this->checkAccessPermission('book-update-all', [
- $otherBook->getUrl() . '/edit'
+ $otherBook->getUrl() . '/edit',
], [
- $otherBook->getUrl() => 'Edit'
+ $otherBook->getUrl() => 'Edit',
]);
}
$otherBook = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('book-delete-own', [
- $ownBook->getUrl() . '/delete'
+ $ownBook->getUrl() . '/delete',
], [
- $ownBook->getUrl() => 'Delete'
+ $ownBook->getUrl() => 'Delete',
]);
$this->visit($otherBook->getUrl())
$this->giveUserPermissions($this->user, ['book-update-all']);
$otherBook = Book::take(1)->get()->first();
$this->checkAccessPermission('book-delete-all', [
- $otherBook->getUrl() . '/delete'
+ $otherBook->getUrl() . '/delete',
], [
- $otherBook->getUrl() => 'Delete'
+ $otherBook->getUrl() => 'Delete',
]);
$this->visit($otherBook->getUrl())->visit($otherBook->getUrl() . '/delete')
$book = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('chapter-create-own', [
- $ownBook->getUrl('/create-chapter')
+ $ownBook->getUrl('/create-chapter'),
], [
- $ownBook->getUrl() => 'New Chapter'
+ $ownBook->getUrl() => 'New Chapter',
]);
$this->visit($ownBook->getUrl('/create-chapter'))
{
$book = Book::take(1)->get()->first();
$this->checkAccessPermission('chapter-create-all', [
- $book->getUrl('/create-chapter')
+ $book->getUrl('/create-chapter'),
], [
- $book->getUrl() => 'New Chapter'
+ $book->getUrl() => 'New Chapter',
]);
$this->visit($book->getUrl('/create-chapter'))
$otherChapter = Chapter::take(1)->get()->first();
$ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
$this->checkAccessPermission('chapter-update-own', [
- $ownChapter->getUrl() . '/edit'
+ $ownChapter->getUrl() . '/edit',
], [
- $ownChapter->getUrl() => 'Edit'
+ $ownChapter->getUrl() => 'Edit',
]);
$this->visit($otherChapter->getUrl())
{
$otherChapter = Chapter::take(1)->get()->first();
$this->checkAccessPermission('chapter-update-all', [
- $otherChapter->getUrl() . '/edit'
+ $otherChapter->getUrl() . '/edit',
], [
- $otherChapter->getUrl() => 'Edit'
+ $otherChapter->getUrl() => 'Edit',
]);
}
$otherChapter = Chapter::take(1)->get()->first();
$ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
$this->checkAccessPermission('chapter-delete-own', [
- $ownChapter->getUrl() . '/delete'
+ $ownChapter->getUrl() . '/delete',
], [
- $ownChapter->getUrl() => 'Delete'
+ $ownChapter->getUrl() => 'Delete',
]);
$bookUrl = $ownChapter->book->getUrl();
$this->giveUserPermissions($this->user, ['chapter-update-all']);
$otherChapter = Chapter::take(1)->get()->first();
$this->checkAccessPermission('chapter-delete-all', [
- $otherChapter->getUrl() . '/delete'
+ $otherChapter->getUrl() . '/delete',
], [
- $otherChapter->getUrl() => 'Delete'
+ $otherChapter->getUrl() => 'Delete',
]);
$bookUrl = $otherChapter->book->getUrl();
}
$this->checkAccessPermission('page-create-own', [], [
- $ownBook->getUrl() => 'New Page',
- $ownChapter->getUrl() => 'New Page'
+ $ownBook->getUrl() => 'New Page',
+ $ownChapter->getUrl() => 'New Page',
]);
$this->giveUserPermissions($this->user, ['page-create-own']);
}
$this->checkAccessPermission('page-create-all', [], [
- $book->getUrl() => 'New Page',
- $chapter->getUrl() => 'New Page'
+ $book->getUrl() => 'New Page',
+ $chapter->getUrl() => 'New Page',
]);
$this->giveUserPermissions($this->user, ['page-create-all']);
$otherPage = Page::take(1)->get()->first();
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->checkAccessPermission('page-update-own', [
- $ownPage->getUrl() . '/edit'
+ $ownPage->getUrl() . '/edit',
], [
- $ownPage->getUrl() => 'Edit'
+ $ownPage->getUrl() => 'Edit',
]);
$this->visit($otherPage->getUrl())
{
$otherPage = Page::take(1)->get()->first();
$this->checkAccessPermission('page-update-all', [
- $otherPage->getUrl() . '/edit'
+ $otherPage->getUrl() . '/edit',
], [
- $otherPage->getUrl() => 'Edit'
+ $otherPage->getUrl() => 'Edit',
]);
}
$otherPage = Page::take(1)->get()->first();
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->checkAccessPermission('page-delete-own', [
- $ownPage->getUrl() . '/delete'
+ $ownPage->getUrl() . '/delete',
], [
- $ownPage->getUrl() => 'Delete'
+ $ownPage->getUrl() => 'Delete',
]);
$parent = $ownPage->chapter ?? $ownPage->book;
$this->giveUserPermissions($this->user, ['page-update-all']);
$otherPage = Page::take(1)->get()->first();
$this->checkAccessPermission('page-delete-all', [
- $otherPage->getUrl() . '/delete'
+ $otherPage->getUrl() . '/delete',
], [
- $otherPage->getUrl() => 'Delete'
+ $otherPage->getUrl() => 'Delete',
]);
$parent = $otherPage->chapter ?? $otherPage->book;
$adminRole = Role::getSystemRole('admin');
$publicRole = Role::getSystemRole('public');
$this->asAdmin()->visit('/settings/users/' . $user->id)
- ->seeElement('[name="roles['.$adminRole->id.']"]')
- ->seeElement('[name="roles['.$publicRole->id.']"]');
+ ->seeElement('[name="roles[' . $adminRole->id . ']"]')
+ ->seeElement('[name="roles[' . $publicRole->id . ']"]');
}
public function test_public_role_visible_in_role_listing()
$this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [
'display_name' => $viewerRole->display_name,
- 'description' => $viewerRole->description,
- 'permission' => []
+ 'description' => $viewerRole->description,
+ 'permission' => [],
])->assertResponseStatus(302);
$this->expectException(HttpException::class);
->dontSee('Sort the current book');
}
- public function test_comment_create_permission () {
+ public function test_comment_create_permission()
+ {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->actingAs($this->user)->addComment($ownPage);
$this->assertResponseStatus(200);
}
-
- public function test_comment_update_own_permission () {
+ public function test_comment_update_own_permission()
+ {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']);
$commentId = $this->actingAs($this->user)->addComment($ownPage);
$this->assertResponseStatus(200);
}
- public function test_comment_update_all_permission () {
+ public function test_comment_update_all_permission()
+ {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$commentId = $this->asAdmin()->addComment($ownPage);
$this->assertResponseStatus(200);
}
- public function test_comment_delete_own_permission () {
+ public function test_comment_delete_own_permission()
+ {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->giveUserPermissions($this->user, ['comment-create-all']);
$commentId = $this->actingAs($this->user)->addComment($ownPage);
$this->assertResponseStatus(200);
}
- public function test_comment_delete_all_permission () {
+ public function test_comment_delete_all_permission()
+ {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$commentId = $this->asAdmin()->addComment($ownPage);
$this->assertResponseStatus(200);
}
- private function addComment($page) {
+ private function addComment($page)
+ {
$comment = factory(Comment::class)->make();
$url = "/comment/$page->id";
$request = [
'text' => $comment->text,
- 'html' => $comment->html
+ 'html' => $comment->html,
];
$this->postJson($url, $request);
$comment = $page->comments()->first();
+
return $comment === null ? null : $comment->id;
}
- private function updateComment($commentId) {
+ private function updateComment($commentId)
+ {
$comment = factory(Comment::class)->make();
$url = "/comment/$commentId";
$request = [
'text' => $comment->text,
- 'html' => $comment->html
+ 'html' => $comment->html,
];
return $this->putJson($url, $request);
}
- private function deleteComment($commentId) {
- $url = '/comment/' . $commentId;
- return $this->json('DELETE', $url);
- }
+ private function deleteComment($commentId)
+ {
+ $url = '/comment/' . $commentId;
+ return $this->json('DELETE', $url);
+ }
}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use Auth;
use BookStack\Auth\Permissions\PermissionService;
class PublicActionTest extends TestCase
{
-
public function test_app_not_public()
{
$this->setSettings(['app-public' => 'false']);
public function test_login_link_visible()
{
$this->setSettings(['app-public' => 'true']);
- $this->get('/')->assertElementExists('a[href="'.url('/login').'"]');
+ $this->get('/')->assertElementExists('a[href="' . url('/login') . '"]');
}
public function test_register_link_visible_when_enabled()
$chapter = Chapter::query()->first();
$resp = $this->get($chapter->getUrl());
$resp->assertSee('New Page');
- $resp->assertElementExists('a[href="'.$chapter->getUrl('/create-page').'"]');
+ $resp->assertElementExists('a[href="' . $chapter->getUrl('/create-page') . '"]');
$resp = $this->get($chapter->getUrl('/create-page'));
$resp->assertSee('Continue');
$resp->assertSee('Page Name');
- $resp->assertElementExists('form[action="'.$chapter->getUrl('/create-guest-page').'"]');
+ $resp->assertElementExists('form[action="' . $chapter->getUrl('/create-guest-page') . '"]');
$resp = $this->post($chapter->getUrl('/create-guest-page'), ['name' => 'My guest page']);
$resp->assertRedirect($chapter->book->getUrl('/page/my-guest-page/edit'));
$user = User::getDefault();
$this->assertDatabaseHas('pages', [
- 'name' => 'My guest page',
+ 'name' => 'My guest page',
'chapter_id' => $chapter->id,
'created_by' => $user->id,
- 'updated_by' => $user->id
+ 'updated_by' => $user->id,
]);
}
$resp = $this->get('/robots.txt');
$resp->assertSee("User-agent: *\nDisallow:");
- $resp->assertDontSee("Disallow: /");
+ $resp->assertDontSee('Disallow: /');
}
public function test_robots_effected_by_setting()
$resp = $this->get('/robots.txt');
$resp->assertSee("User-agent: *\nDisallow:");
- $resp->assertDontSee("Disallow: /");
+ $resp->assertDontSee('Disallow: /');
// Check config overrides app-public setting
config()->set('app.allow_robots', false);
$resp->assertRedirect($book->getUrl());
$this->followRedirects($resp)->assertSee($book->name);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
"DELETE:/settings/recycle-bin/{$deletion->id}",
];
- foreach($routes as $route) {
+ foreach ($routes as $route) {
[$method, $url] = explode(':', $route);
$resp = $this->call($method, $url);
$this->assertPermissionError($resp);
$this->giveUserPermissions($editor, ['restrictions-manage-all']);
- foreach($routes as $route) {
+ foreach ($routes as $route) {
[$method, $url] = explode(':', $route);
$resp = $this->call($method, $url);
$this->assertPermissionError($resp);
$this->giveUserPermissions($editor, ['settings-manage']);
- foreach($routes as $route) {
+ foreach ($routes as $route) {
DB::beginTransaction();
[$method, $url] = explode(':', $route);
$resp = $this->call($method, $url);
$this->assertNotPermissionError($resp);
DB::rollBack();
}
-
}
public function test_recycle_bin_view()
public function test_recycle_bin_empty()
{
$page = Page::query()->first();
- $book = Book::query()->where('id' , '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+ $book = Book::query()->where('id', '!=', $page->book_id)->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
$editor = $this->getEditor();
$this->actingAs($editor)->delete($page->getUrl());
$this->actingAs($editor)->delete($book->getUrl());
$itemCount = 2 + $book->pages->count() + $book->chapters->count();
$redirectReq = $this->get('/settings/recycle-bin');
- $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+ $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin');
}
public function test_entity_restore()
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
$redirectReq = $this->get('/settings/recycle-bin');
- $redirectReq->assertNotificationContains('Restored '.$itemCount.' total items from the recycle bin');
+ $redirectReq->assertNotificationContains('Restored ' . $itemCount . ' total items from the recycle bin');
}
public function test_permanent_delete()
$itemCount = 1 + $book->pages->count() + $book->chapters->count();
$redirectReq = $this->get('/settings/recycle-bin');
- $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+ $redirectReq->assertNotificationContains('Deleted ' . $itemCount . ' total items from the recycle bin');
}
public function test_permanent_delete_for_each_type()
{
/** @var Entity $entity */
- foreach ([new Bookshelf, new Book, new Chapter, new Page] as $entity) {
+ foreach ([new Bookshelf(), new Book(), new Chapter(), new Page()] as $entity) {
$entity = $entity->newQuery()->first();
$this->asEditor()->delete($entity->getUrl());
$deletion = Deletion::query()->orderBy('id', 'desc')->firstOrFail();
$deletion = $page->deletions()->firstOrFail();
$this->assertDatabaseHas('activities', [
- 'type' => 'page_delete',
- 'entity_id' => $page->id,
+ 'type' => 'page_delete',
+ 'entity_id' => $page->id,
'entity_type' => $page->getMorphClass(),
]);
$this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
$this->assertDatabaseMissing('activities', [
- 'type' => 'page_delete',
- 'entity_id' => $page->id,
+ 'type' => 'page_delete',
+ 'entity_id' => $page->id,
'entity_type' => $page->getMorphClass(),
]);
$this->assertDatabaseHas('activities', [
- 'type' => 'page_delete',
- 'entity_id' => null,
+ 'type' => 'page_delete',
+ 'entity_id' => null,
'entity_type' => null,
- 'detail' => $page->name,
+ 'detail' => $page->name,
]);
}
$chapterRestoreView->assertSeeText($chapter->name);
$chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
- $chapterRestore->assertRedirect("/settings/recycle-bin");
- $this->assertDatabaseMissing("deletions", ["id" => $chapterDeletion->id]);
+ $chapterRestore->assertRedirect('/settings/recycle-bin');
+ $this->assertDatabaseMissing('deletions', ['id' => $chapterDeletion->id]);
$chapter->refresh();
$this->assertNotNull($chapter->deleted_at);
$pageRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$pageDeletion->id}/restore");
$pageRestoreView->assertSee('The parent of this item has also been deleted.');
- $pageRestoreView->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id. '/restore"]', 'Restore Parent');
+ $pageRestoreView->assertElementContains('a[href$="/settings/recycle-bin/' . $bookDeletion->id . '/restore"]', 'Restore Parent');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+namespace Tests;
use Illuminate\Support\Str;
class SecurityHeaderTest extends TestCase
{
-
public function test_cookies_samesite_lax_by_default()
{
- $resp = $this->get("/");
+ $resp = $this->get('/');
foreach ($resp->headers->getCookies() as $cookie) {
- $this->assertEquals("lax", $cookie->getSameSite());
+ $this->assertEquals('lax', $cookie->getSameSite());
}
}
public function test_cookies_samesite_none_when_iframe_hosts_set()
{
- $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "http://example.com", function() {
- $resp = $this->get("/");
+ $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'http://example.com', function () {
+ $resp = $this->get('/');
foreach ($resp->headers->getCookies() as $cookie) {
- $this->assertEquals("none", $cookie->getSameSite());
+ $this->assertEquals('none', $cookie->getSameSite());
}
});
}
public function test_secure_cookies_controlled_by_app_url()
{
- $this->runWithEnv("APP_URL", "http://example.com", function() {
- $resp = $this->get("/");
+ $this->runWithEnv('APP_URL', 'http://example.com', function () {
+ $resp = $this->get('/');
foreach ($resp->headers->getCookies() as $cookie) {
$this->assertFalse($cookie->isSecure());
}
});
- $this->runWithEnv("APP_URL", "https://example.com", function() {
- $resp = $this->get("/");
+ $this->runWithEnv('APP_URL', 'https://example.com', function () {
+ $resp = $this->get('/');
foreach ($resp->headers->getCookies() as $cookie) {
$this->assertTrue($cookie->isSecure());
}
public function test_iframe_csp_self_only_by_default()
{
- $resp = $this->get("/");
+ $resp = $this->get('/');
$cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
$frameHeaders = $cspHeaders->filter(function ($val) {
return Str::startsWith($val, 'frame-ancestors');
public function test_iframe_csp_includes_extra_hosts_if_configured()
{
- $this->runWithEnv("ALLOWED_IFRAME_HOSTS", "https://a.example.com https://b.example.com", function() {
- $resp = $this->get("/");
+ $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'https://a.example.com https://b.example.com', function () {
+ $resp = $this->get('/');
$cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
- $frameHeaders = $cspHeaders->filter(function($val) {
+ $frameHeaders = $cspHeaders->filter(function ($val) {
return Str::startsWith($val, 'frame-ancestors');
});
$this->assertTrue($frameHeaders->count() === 1);
$this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first());
});
-
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+namespace Tests;
+
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Auth\Permissions\PermissionsRepo;
+use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Repos\ChapterRepo;
-use BookStack\Auth\Permissions\PermissionsRepo;
-use BookStack\Auth\Role;
-use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
+use Illuminate\Foundation\Testing\Assert as PHPUnit;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Log;
use Mockery;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
-use Illuminate\Foundation\Testing\Assert as PHPUnit;
trait SharedTestHelpers
{
-
protected $admin;
protected $editor;
return $this->actingAs($this->getEditor());
}
-
/**
* Get a editor user.
*/
$editorRole = Role::getRole('editor');
$this->editor = $editorRole->users->first();
}
+
return $this->editor;
}
if (!empty($attributes)) {
$user->forceFill($attributes)->save();
}
+
return $user;
}
}
/**
- * Create and return a new test chapter
+ * Create and return a new test chapter.
*/
public function newChapter(array $input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book): Chapter
{
}
/**
- * Create and return a new test page
+ * Create and return a new test page.
*/
public function newPage(array $input = ['name' => 'test page', 'html' => 'My new test page']): Page
{
$book = Book::query()->first();
$pageRepo = app(PageRepo::class);
$draftPage = $pageRepo->getNewDraftPage($book);
+
return $pageRepo->publishDraft($draftPage, $input);
}
foreach ($roles as $role) {
$permissions[] = [
'role_id' => $role->id,
- 'action' => strtolower($action)
+ 'action' => strtolower($action),
];
}
}
$permissionRepo = app(PermissionsRepo::class);
$roleData = factory(Role::class)->make()->toArray();
$roleData['permissions'] = array_flip($permissions);
+
return $permissionRepo->saveNewRole($roleData);
}
*/
protected function assertPermissionError($response)
{
- PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error.");
+ PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response contains a permission error.');
}
/**
*/
protected function assertNotPermissionError($response)
{
- PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error.");
+ PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), 'Failed asserting the response does not contain a permission error.');
}
/**
return $testHandler;
}
-
-}
\ No newline at end of file
+}
<?php
use Illuminate\Cache\ArrayStore;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Tests\TestCase;
{
public function test_returns_json_with_expected_results()
{
- $resp = $this->get("/status");
+ $resp = $this->get('/status');
$resp->assertStatus(200);
$resp->assertJson([
'database' => true,
- 'cache' => true,
- 'session' => true,
+ 'cache' => true,
+ 'session' => true,
]);
}
{
DB::shouldReceive('table')->andThrow(new Exception());
- $resp = $this->get("/status");
+ $resp = $this->get('/status');
$resp->assertStatus(500);
$resp->assertJson([
'database' => false,
Cache::swap($mockStore);
$mockStore->shouldReceive('get')->andReturn('cat');
- $resp = $this->get("/status");
+ $resp = $this->get('/status');
$resp->assertStatus(500);
$resp->assertJson([
'cache' => false,
Session::swap($mockSession);
$mockSession->shouldReceive('get')->andReturn('cat');
- $resp = $this->get("/status");
+ $resp = $this->get('/status');
$resp->assertStatus(500);
$resp->assertJson([
'session' => false,
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Entities\Models\Entity;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* The base URL to use while testing the application.
+ *
* @var string
*/
protected $baseUrl = 'http://localhost';
/**
* Assert the session contains a specific entry.
+ *
* @param string $key
+ *
* @return $this
*/
protected function assertSessionHas(string $key)
{
$this->assertTrue(session()->has($key), "Session does not contain a [{$key}] entry");
+
return $this;
}
/**
* Override of the get method so we can get visibility of custom TestResponse methods.
- * @param string $uri
- * @param array $headers
+ *
+ * @param string $uri
+ * @param array $headers
+ *
* @return TestResponse
*/
public function get($uri, array $headers = [])
/**
* Create the test response instance from the given response.
*
- * @param \Illuminate\Http\Response $response
+ * @param \Illuminate\Http\Response $response
+ *
* @return TestResponse
*/
protected function createTestResponse($response)
$this->assertDatabaseHas('activities', $detailsToCheck);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Notifications\TestEmail;
use Illuminate\Contracts\Notifications\Dispatcher;
class TestEmailTest extends TestCase
{
-
public function test_a_send_test_button_shows()
{
$pageView = $this->asAdmin()->get('/settings/maintenance');
$sendReq = $this->actingAs($user)->post('/settings/maintenance/send-test-email');
Notification::assertSentTo($user, TestEmail::class);
}
-
-
-}
\ No newline at end of file
+}
-<?php namespace Tests;
+<?php
-use \Illuminate\Foundation\Testing\TestResponse as BaseTestResponse;
-use Symfony\Component\DomCrawler\Crawler;
+namespace Tests;
+
+use Illuminate\Foundation\Testing\TestResponse as BaseTestResponse;
use PHPUnit\Framework\Assert as PHPUnit;
+use Symfony\Component\DomCrawler\Crawler;
/**
* Class TestResponse
* Custom extension of the default Laravel TestResponse class.
- * @package Tests
*/
-class TestResponse extends BaseTestResponse {
-
+class TestResponse extends BaseTestResponse
+{
protected $crawlerInstance;
/**
if (!is_object($this->crawlerInstance)) {
$this->crawlerInstance = new Crawler($this->getContent());
}
+
return $this->crawlerInstance;
}
/**
* Assert the response contains the specified element.
+ *
* @return $this
*/
public function assertElementExists(string $selector)
$elements = $this->crawler()->filter($selector);
PHPUnit::assertTrue(
$elements->count() > 0,
- 'Unable to find element matching the selector: '.PHP_EOL.PHP_EOL.
- "[{$selector}]".PHP_EOL.PHP_EOL.
- 'within'.PHP_EOL.PHP_EOL.
+ 'Unable to find element matching the selector: ' . PHP_EOL . PHP_EOL .
+ "[{$selector}]" . PHP_EOL . PHP_EOL .
+ 'within' . PHP_EOL . PHP_EOL .
"[{$this->getContent()}]."
);
+
return $this;
}
/**
* Assert the response does not contain the specified element.
+ *
* @return $this
*/
public function assertElementNotExists(string $selector)
$elements = $this->crawler()->filter($selector);
PHPUnit::assertTrue(
$elements->count() === 0,
- 'Found elements matching the selector: '.PHP_EOL.PHP_EOL.
- "[{$selector}]".PHP_EOL.PHP_EOL.
- 'within'.PHP_EOL.PHP_EOL.
+ 'Found elements matching the selector: ' . PHP_EOL . PHP_EOL .
+ "[{$selector}]" . PHP_EOL . PHP_EOL .
+ 'within' . PHP_EOL . PHP_EOL .
"[{$this->getContent()}]."
);
+
return $this;
}
* Assert the response includes a specific element containing the given text.
* If an nth match is provided, only that will be checked otherwise all matching
* elements will be checked for the given text.
+ *
* @return $this
*/
public function assertElementContains(string $selector, string $text, ?int $nthMatch = null)
PHPUnit::assertTrue(
$matched,
- 'Unable to find element of selector: '.PHP_EOL.PHP_EOL.
- ($nthMatch ? ("at position {$nthMatch}".PHP_EOL.PHP_EOL) : '') .
- "[{$selector}]".PHP_EOL.PHP_EOL.
- 'containing text'.PHP_EOL.PHP_EOL.
- "[{$text}]".PHP_EOL.PHP_EOL.
- 'within'.PHP_EOL.PHP_EOL.
+ 'Unable to find element of selector: ' . PHP_EOL . PHP_EOL .
+ ($nthMatch ? ("at position {$nthMatch}" . PHP_EOL . PHP_EOL) : '') .
+ "[{$selector}]" . PHP_EOL . PHP_EOL .
+ 'containing text' . PHP_EOL . PHP_EOL .
+ "[{$text}]" . PHP_EOL . PHP_EOL .
+ 'within' . PHP_EOL . PHP_EOL .
"[{$this->getContent()}]."
);
* Assert the response does not include a specific element containing the given text.
* If an nth match is provided, only that will be checked otherwise all matching
* elements will be checked for the given text.
+ *
* @return $this
*/
public function assertElementNotContains(string $selector, string $text, ?int $nthMatch = null)
PHPUnit::assertTrue(
!$matched,
- 'Found element of selector: '.PHP_EOL.PHP_EOL.
- ($nthMatch ? ("at position {$nthMatch}".PHP_EOL.PHP_EOL) : '') .
- "[{$selector}]".PHP_EOL.PHP_EOL.
- 'containing text'.PHP_EOL.PHP_EOL.
- "[{$text}]".PHP_EOL.PHP_EOL.
- 'within'.PHP_EOL.PHP_EOL.
+ 'Found element of selector: ' . PHP_EOL . PHP_EOL .
+ ($nthMatch ? ("at position {$nthMatch}" . PHP_EOL . PHP_EOL) : '') .
+ "[{$selector}]" . PHP_EOL . PHP_EOL .
+ 'containing text' . PHP_EOL . PHP_EOL .
+ "[{$text}]" . PHP_EOL . PHP_EOL .
+ 'within' . PHP_EOL . PHP_EOL .
"[{$this->getContent()}]."
);
/**
* Assert there's a notification within the view containing the given text.
+ *
* @return $this
*/
public function assertNotificationContains(string $text)
/**
* Get the escaped text pattern for the constraint.
+ *
* @return string
*/
protected function getEscapedPattern(string $text)
{
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
+
return $rawPattern == $escapedPattern
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
}
-
}
-<?php namespace Tests;
+<?php
+
+namespace Tests;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
$callback = function ($environment) use (&$callbackCalled) {
$this->assertInstanceOf(ConfigurableEnvironmentInterface::class, $environment);
$callbackCalled = true;
+
return $environment;
};
Theme::listen(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $callback);
public function test_add_social_driver()
{
Theme::addSocialDriver('catnet', [
- 'client_id' => 'abc123',
- 'client_secret' => 'def456'
+ 'client_id' => 'abc123',
+ 'client_secret' => 'def456',
], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting');
$this->assertEquals('catnet', config('services.catnet.name'));
public function test_add_social_driver_uses_name_in_config_if_given()
{
Theme::addSocialDriver('catnet', [
- 'client_id' => 'abc123',
+ 'client_id' => 'abc123',
'client_secret' => 'def456',
- 'name' => 'Super Cat Name',
+ 'name' => 'Super Cat Name',
], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting');
$this->assertEquals('Super Cat Name', config('services.catnet.name'));
$loginResp->assertSee('Super Cat Name');
}
-
public function test_add_social_driver_allows_a_configure_for_redirect_callback_to_be_passed()
{
Theme::addSocialDriver(
'discord',
[
- 'client_id' => 'abc123',
+ 'client_id' => 'abc123',
'client_secret' => 'def456',
- 'name' => 'Super Cat Name',
+ 'name' => 'Super Cat Name',
],
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
function ($driver) {
$this->assertStringContainsString('donkey=donut', $redirect);
}
-
protected function usingThemeFolder(callable $callback)
{
// Create a folder and configure a theme
- $themeFolderName = 'testing_theme_' . rtrim(base64_encode(time()), "=");
+ $themeFolderName = 'testing_theme_' . rtrim(base64_encode(time()), '=');
config()->set('view.theme', $themeFolderName);
$themeFolderPath = theme_path('');
File::makeDirectory($themeFolderPath);
// Cleanup the custom theme folder we created
File::deleteDirectory($themeFolderPath);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Unit;
+<?php
+
+namespace Tests\Unit;
use Illuminate\Support\Facades\Log;
use Tests\TestCase;
* Class ConfigTest
* Many of the tests here are to check on tweaks made
* to maintain backwards compatibility.
- *
- * @package Tests
*/
class ConfigTest extends TestCase
{
-
public function test_filesystem_images_falls_back_to_storage_type_var()
{
- $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+ $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () {
$this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
$this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
});
public function test_filesystem_attachments_falls_back_to_storage_type_var()
{
- $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+ $this->runWithEnv('STORAGE_TYPE', 'local_secure', function () {
$this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
$this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
});
]);
$temp = tempnam(sys_get_temp_dir(), 'bs-test');
- $original = ini_set( 'error_log', $temp);
+ $original = ini_set('error_log', $temp);
Log::channel('errorlog_plain_webserver')->info('Aww, look, a cute puppy');
- ini_set( 'error_log', $original);
+ ini_set('error_log', $original);
$output = file_get_contents($temp);
$this->assertStringContainsString('Aww, look, a cute puppy', $output);
* Set an environment variable of the given name and value
* then check the given config key to see if it matches the given result.
* Providing a null $envVal clears the variable.
+ *
* @param mixed $expectedResult
*/
protected function checkEnvConfigResult(string $envName, ?string $envVal, string $configKey, $expectedResult)
{
- $this->runWithEnv($envName, $envVal, function() use ($configKey, $expectedResult) {
+ $this->runWithEnv($envName, $envVal, function () use ($configKey, $expectedResult) {
$this->assertEquals($expectedResult, config($configKey));
});
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Unit;
+<?php
+
+namespace Tests\Unit;
use Tests\TestCase;
class UrlTest extends TestCase
{
-
public function test_url_helper_takes_custom_url_into_account()
{
- $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function() {
+ $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function () {
$this->assertEquals('http://example.com/bookstack/books', url('/books'));
});
}
public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different()
{
- $this->runWithEnv('APP_URL', 'https://example.com/', function() {
+ $this->runWithEnv('APP_URL', 'https://example.com/', function () {
$this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');
});
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Uploads;
+<?php
-use BookStack\Entities\Tools\TrashCan;
+namespace Tests\Uploads;
+
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\TrashCan;
use BookStack\Uploads\Attachment;
-use BookStack\Entities\Models\Page;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\UploadedFile;
use Tests\TestCase;
class AttachmentTest extends TestCase
{
/**
- * Get a test file that can be uploaded
+ * Get a test file that can be uploaded.
*/
protected function getTestFile(string $fileName): UploadedFile
{
protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Foundation\Testing\TestResponse
{
$file = $this->getTestFile($name);
+
return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**
- * Create a new attachment
+ * Create a new attachment.
*/
protected function createAttachment(Page $page): Attachment
{
$this->post('attachments/link', [
- 'attachment_link_url' => 'https://example.com',
- 'attachment_link_name' => 'Example Attachment Link',
+ 'attachment_link_url' => 'https://example.com',
+ 'attachment_link_name' => 'Example Attachment Link',
'attachment_link_uploaded_to' => $page->id,
]);
$fileName = 'upload_test_file.txt';
$expectedResp = [
- 'name' => $fileName,
+ 'name' => $fileName,
'uploaded_to'=> $page->id,
- 'extension' => 'txt',
- 'order' => 1,
+ 'extension' => 'txt',
+ 'order' => 1,
'created_by' => $admin->id,
'updated_by' => $admin->id,
];
$page = Page::query()->first();
$fileName = 'upload_test_file.txt';
-
$upload = $this->asAdmin()->uploadFile($fileName, $page->id);
$upload->assertStatus(200);
$this->asAdmin();
$linkReq = $this->call('POST', 'attachments/link', [
- 'attachment_link_url' => 'https://example.com',
- 'attachment_link_name' => 'Example Attachment Link',
+ 'attachment_link_url' => 'https://example.com',
+ 'attachment_link_name' => 'Example Attachment Link',
'attachment_link_uploaded_to' => $page->id,
]);
$expectedData = [
- 'path' => 'https://example.com',
- 'name' => 'Example Attachment Link',
+ 'path' => 'https://example.com',
+ 'name' => 'Example Attachment Link',
'uploaded_to' => $page->id,
- 'created_by' => $admin->id,
- 'updated_by' => $admin->id,
- 'external' => true,
- 'order' => 1,
- 'extension' => ''
+ 'created_by' => $admin->id,
+ 'updated_by' => $admin->id,
+ 'external' => true,
+ 'order' => 1,
+ 'extension' => '',
];
$linkReq->assertStatus(200);
$attachment = $this->createAttachment($page);
$update = $this->call('PUT', 'attachments/' . $attachment->id, [
'attachment_edit_name' => 'My new attachment name',
- 'attachment_edit_url' => 'https://test.example.com'
+ 'attachment_edit_url' => 'https://test.example.com',
]);
$expectedData = [
- 'id' => $attachment->id,
- 'path' => 'https://test.example.com',
- 'name' => 'My new attachment name',
- 'uploaded_to' => $page->id
+ 'id' => $attachment->id,
+ 'path' => 'https://test.example.com',
+ 'name' => 'My new attachment name',
+ 'uploaded_to' => $page->id,
];
$update->assertStatus(200);
$this->delete($attachment->getUrl());
$this->assertDatabaseMissing('attachments', [
- 'name' => $fileName
+ 'name' => $fileName,
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
$this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
$this->assertDatabaseHas('attachments', [
- 'name' => $fileName
+ 'name' => $fileName,
]);
app(PageRepo::class)->destroy($page);
app(TrashCan::class)->empty();
$this->assertDatabaseMissing('attachments', [
- 'name' => $fileName
+ 'name' => $fileName,
]);
$this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
$admin = $this->getAdmin();
$viewer = $this->getViewer();
$page = Page::query()->first(); /** @var Page $page */
-
$this->actingAs($admin);
$fileName = 'permission_test.txt';
$this->uploadFile($fileName, $page->id);
$this->actingAs($viewer);
$attachmentGet = $this->get($attachment->getUrl());
$attachmentGet->assertStatus(404);
- $attachmentGet->assertSee("Attachment not found");
+ $attachmentGet->assertSee('Attachment not found');
$this->deleteUploads();
}
' javascript:alert("bunny")',
'JavaScript:alert("bunny")',
"\t\n\t\nJavaScript:alert(\"bunny\")",
- "data:text/html;<a></a>",
- "Data:text/html;<a></a>",
- "Data:text/html;<a></a>",
+ 'data:text/html;<a></a>',
+ 'Data:text/html;<a></a>',
+ 'Data:text/html;<a></a>',
];
foreach ($badLinks as $badLink) {
$linkReq = $this->post('attachments/link', [
- 'attachment_link_url' => $badLink,
- 'attachment_link_name' => 'Example Attachment Link',
+ 'attachment_link_url' => $badLink,
+ 'attachment_link_name' => 'Example Attachment Link',
'attachment_link_uploaded_to' => $page->id,
]);
$linkReq->assertStatus(422);
foreach ($badLinks as $badLink) {
$linkReq = $this->put('attachments/' . $attachment->id, [
- 'attachment_edit_url' => $badLink,
+ 'attachment_edit_url' => $badLink,
'attachment_edit_name' => 'Example Attachment Link',
]);
$linkReq->assertStatus(422);
$attachmentGet = $this->get($attachment->getUrl(true));
// http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
$attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
- $attachmentGet->assertHeader('Content-Disposition', "inline; filename=\"upload_test_file.txt\"");
+ $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
$this->deleteUploads();
}
-<?php namespace Tests\Uploads;
+<?php
+
+namespace Tests\Uploads;
use BookStack\Auth\User;
use BookStack\Exceptions\HttpFetchException;
use BookStack\Uploads\HttpFetcher;
-use Illuminate\Support\Facades\Log;
use Tests\TestCase;
class AvatarTest extends TestCase
{
use UsesImages;
-
protected function createUserRequest($user)
{
$this->asAdmin()->post('/settings/users/create', [
- 'name' => $user->name,
- 'email' => $user->email,
- 'password' => 'testing',
+ 'name' => $user->name,
+ 'email' => $user->email,
+ 'password' => 'testing',
'password-confirm' => 'testing',
]);
+
return User::where('email', '=', $user->email)->first();
}
'services.disable_services' => false,
]);
$user = factory(User::class)->make();
- $this->assertImageFetchFrom('https://www.gravatar.com/avatar/'.md5(strtolower($user->email)).'?s=500&d=identicon');
+ $this->assertImageFetchFrom('https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon');
$user = $this->createUserRequest($user);
$this->assertDatabaseHas('images', [
- 'type' => 'user',
- 'created_by' => $user->id
+ 'type' => 'user',
+ 'created_by' => $user->id,
]);
$this->deleteUserImage($user);
}
-
public function test_custom_url_used_if_set()
{
config()->set([
'services.disable_services' => false,
- 'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}',
+ 'services.avatar_url' => 'https://example.com/${email}/${hash}/${size}',
]);
$user = factory(User::class)->make();
- $url = 'https://example.com/'. urlencode(strtolower($user->email)) .'/'. md5(strtolower($user->email)).'/500';
+ $url = 'https://example.com/' . urlencode(strtolower($user->email)) . '/' . md5(strtolower($user->email)) . '/500';
$this->assertImageFetchFrom($url);
$user = $this->createUserRequest($user);
$this->createUserRequest($user);
$this->assertTrue($logger->hasError('Failed to save user avatar image'));
}
-
}
-<?php namespace Tests\Uploads;
+<?php
+
+namespace Tests\Uploads;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\Image;
$imageGet = $this->getJson("/images/drawio/base64/{$image->id}");
$imageGet->assertJson([
- 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII='
+ 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
]);
}
$upload = $this->postJson('images/drawio', [
'uploaded_to' => $page->id,
- 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII='
+ 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=',
]);
$upload->assertStatus(200);
$upload->assertJson([
- 'type' => 'drawio',
+ 'type' => 'drawio',
'uploaded_to' => $page->id,
- 'created_by' => $editor->id,
- 'updated_by' => $editor->id,
+ 'created_by' => $editor->id,
+ 'updated_by' => $editor->id,
]);
$image = Image::where('type', '=', 'drawio')->first();
- $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: '. public_path($image->path));
+ $this->assertTrue(file_exists(public_path($image->path)), 'Uploaded image not found at path: ' . public_path($image->path));
$testImageData = file_get_contents($this->getTestImageFilePath());
$uploadedImageData = file_get_contents(public_path($image->path));
- $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected");
+ $this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected');
}
public function test_drawio_url_can_be_configured()
$resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
$resp->assertDontSee('drawio-url');
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\Uploads;
+<?php
+namespace Tests\Uploads;
+
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Image;
-use BookStack\Entities\Models\Page;
use BookStack\Uploads\ImageService;
use Illuminate\Support\Str;
use Tests\TestCase;
class ImageTest extends TestCase
{
-
use UsesImages;
public function test_image_upload()
$imgDetails = $this->uploadGalleryImage($page);
$relPath = $imgDetails['path'];
- $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath));
+ $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
$this->deleteImage($relPath);
$this->assertDatabaseHas('images', [
- 'url' => $this->baseUrl . $relPath,
- 'type' => 'gallery',
+ 'url' => $this->baseUrl . $relPath,
+ 'type' => 'gallery',
'uploaded_to' => $page->id,
- 'path' => $relPath,
- 'created_by' => $admin->id,
- 'updated_by' => $admin->id,
- 'name' => $imgDetails['name'],
+ 'path' => $relPath,
+ 'created_by' => $admin->id,
+ 'updated_by' => $admin->id,
+ 'name' => $imgDetails['name'],
]);
}
$imgDetails = $this->uploadGalleryImage($page, 'compressed.png');
$relPath = $imgDetails['path'];
- $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath));
+ $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: ' . public_path($relPath));
$displayImage = $imgDetails['response']->thumbs->display;
$displayImageRelPath = implode('/', array_slice(explode('/', $displayImage), 3));
$this->assertDatabaseHas('images', [
'type' => 'gallery',
- 'name' => $newName
+ 'name' => $newName,
]);
}
$imgDetails = $this->uploadGalleryImage($page);
$image = Image::query()->first();
- $page->html = '<img src="'.$image->url.'">';
+ $page->html = '<img src="' . $image->url . '">';
$page->save();
$usage = $this->get('/images/edit/' . $image->id . '?delete=true');
$this->assertDatabaseMissing('images', [
'type' => 'gallery',
- 'name' => $fileName
+ 'name' => $fileName,
]);
}
{
$this->asEditor();
$badNames = [
- "bad-char-#-image.png",
- "bad-char-?-image.png",
- "?#.png",
- "?.png",
- "#.png",
+ 'bad-char-#-image.png',
+ 'bad-char-?-image.png',
+ '?#.png',
+ '?.png',
+ '#.png',
];
foreach ($badNames as $name) {
$galleryFile = $this->getTestImage($name);
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::query()->first();
- $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png');
+ $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);
- $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath);
+ $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);
if (file_exists($expectedPath)) {
unlink($expectedPath);
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$page = Page::query()->first();
- $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png');
+ $expectedPath = storage_path('uploads/images/gallery/' . date('Y-m') . '/my-secure-test-upload.png');
$upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$imageUrl = json_decode($upload->getContent(), true)['url'];
config()->set('filesystems.images', 'local_secure');
$this->asAdmin();
$galleryFile = $this->getTestImage('my-system-test-upload.png');
- $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png');
+ $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-upload.png');
$upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []);
$upload->assertRedirect('/settings');
- $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath);
+ $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);
if (file_exists($expectedPath)) {
unlink($expectedPath);
$this->uploadImage($imageName, $page->id);
$image = Image::first();
- $delete = $this->delete( '/images/' . $image->id);
+ $delete = $this->delete('/images/' . $image->id);
$delete->assertStatus(200);
$this->assertDatabaseMissing('images', [
- 'url' => $this->baseUrl . $relPath,
- 'type' => 'gallery'
+ 'url' => $this->baseUrl . $relPath,
+ 'type' => 'gallery',
]);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded image has not been deleted as expected');
$folder = public_path(dirname($relPath));
$imageCount = count(glob($folder . '/*'));
- $delete = $this->delete( '/images/' . $image->id);
+ $delete = $this->delete('/images/' . $image->id);
$delete->assertStatus(200);
$newCount = count(glob($folder . '/*'));
$this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
$this->assertDatabaseHas('images', [
- 'type' => 'user',
+ 'type' => 'user',
'uploaded_to' => $editor->id,
- 'created_by' => $admin->id,
+ 'created_by' => $admin->id,
]);
}
$this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
$profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get();
- $this->assertTrue($profileImages->count() === 1, "Found profile images does not match upload count");
+ $this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count');
$imagePath = public_path($profileImages->first()->path);
$this->assertTrue(file_exists($imagePath));
$userDelete->assertStatus(302);
$this->assertDatabaseMissing('images', [
- 'type' => 'user',
- 'created_by' => $editor->id
+ 'type' => 'user',
+ 'created_by' => $editor->id,
]);
$this->assertDatabaseMissing('images', [
- 'type' => 'user',
- 'uploaded_to' => $editor->id
+ 'type' => 'user',
+ 'uploaded_to' => $editor->id,
]);
$this->assertFalse(file_exists($imagePath));
$pageRepo = app(PageRepo::class);
$pageRepo->update($page, [
- 'name' => $page->name,
- 'html' => $page->html . "<img src=\"{$image->url}\">",
- 'summary' => ''
+ 'name' => $page->name,
+ 'html' => $page->html . "<img src=\"{$image->url}\">",
+ 'summary' => '',
]);
// Ensure no images are reported as deletable
// Save a revision of our page without the image;
$pageRepo->update($page, [
- 'name' => $page->name,
- 'html' => "<p>Hello</p>",
- 'summary' => ''
+ 'name' => $page->name,
+ 'html' => '<p>Hello</p>',
+ 'summary' => '',
]);
// Ensure revision images are picked up okay
$this->deleteImage($relPath);
}
-
}
-<?php namespace Tests\Uploads;
+<?php
+
+namespace Tests\Uploads;
use BookStack\Entities\Models\Page;
use Illuminate\Http\UploadedFile;
$data = file_get_contents($base64FilePath);
$decoded = base64_decode($data);
file_put_contents($imagePath, $decoded);
+
return new UploadedFile($imagePath, $imageFileName, 'image/png', null, true);
}
/**
- * Get a test image that can be uploaded
+ * Get a test image that can be uploaded.
*/
protected function getTestImage(string $fileName, ?string $testDataFileName = null): UploadedFile
{
/**
* Get the raw file data for the test image.
+ *
* @return false|string
*/
protected function getTestImageContent()
*/
protected function getTestImagePath(string $type, string $fileName): string
{
- return '/uploads/images/' . $type . '/' . Date('Y-m') . '/' . $fileName;
+ return '/uploads/images/' . $type . '/' . date('Y-m') . '/' . $fileName;
}
/**
* Uploads an image with the given name.
+ *
* @param $name
- * @param int $uploadedTo
+ * @param int $uploadedTo
* @param string $contentType
+ *
* @return \Illuminate\Foundation\Testing\TestResponse
*/
protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png', ?string $testDataFileName = null)
{
$file = $this->getTestImage($name, $testDataFileName);
+
return $this->withHeader('Content-Type', $contentType)
->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
* Upload a new gallery image.
* Returns the image name.
* Can provide a page to relate the image to.
+ *
* @param Page|null $page
+ *
* @return array
*/
protected function uploadGalleryImage(Page $page = null, ?string $testDataFileName = null)
$upload = $this->uploadImage($imageName, $page->id, 'image/png', $testDataFileName);
$upload->assertStatus(200);
+
return [
- 'name' => $imageName,
- 'path' => $relPath,
- 'page' => $page,
+ 'name' => $imageName,
+ 'path' => $relPath,
+ 'page' => $page,
'response' => json_decode($upload->getContent()),
];
}
unlink($path);
}
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\User;
+<?php
+
+namespace Tests\User;
use BookStack\Actions\ActivityType;
use BookStack\Api\ApiToken;
class UserApiTokenTest extends TestCase
{
-
protected $testTokenData = [
- 'name' => 'My test API token',
+ 'name' => 'My test API token',
'expires_at' => '2050-04-01',
];
$token = ApiToken::query()->latest()->first();
$resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id));
$this->assertDatabaseHas('api_tokens', [
- 'user_id' => $editor->id,
- 'name' => $this->testTokenData['name'],
+ 'user_id' => $editor->id,
+ 'name' => $this->testTokenData['name'],
'expires_at' => $this->testTokenData['expires_at'],
]);
$under = Carbon::now()->addYears(99);
$this->assertTrue(
($token->expires_at < $over && $token->expires_at > $under),
- "Token expiry set at 100 years in future"
+ 'Token expiry set at 100 years in future'
);
}
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
$token = ApiToken::query()->latest()->first();
$updateData = [
- 'name' => 'My updated token',
+ 'name' => 'My updated token',
'expires_at' => '2011-01-01',
];
$token = ApiToken::query()->latest()->first();
$resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), [
- 'name' => 'My updated token',
+ 'name' => 'My updated token',
'expires_at' => '',
]);
$token->refresh();
$under = Carbon::now()->addYears(99);
$this->assertTrue(
($token->expires_at < $over && $token->expires_at > $under),
- "Token expiry set at 100 years in future"
+ 'Token expiry set at 100 years in future'
);
}
$resp = $this->get($tokenUrl . '/delete');
$resp->assertSeeText('Delete Token');
$resp->assertSeeText($token->name);
- $resp->assertElementExists('form[action="'.$tokenUrl.'"]');
+ $resp->assertElementExists('form[action="' . $tokenUrl . '"]');
$resp = $this->delete($tokenUrl);
$resp->assertRedirect($editor->getEditUrl('#api_tokens'));
$resp->assertRedirect($viewer->getEditUrl('#api_tokens'));
$this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);
}
-
-}
\ No newline at end of file
+}
-<?php namespace Tests\User;
+<?php
+
+namespace Tests\User;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
class UserManagementTest extends TestCase
{
-
public function test_delete()
{
$editor = $this->getEditor();
$resp = $this->asAdmin()->delete("settings/users/{$editor->id}");
- $resp->assertRedirect("/settings/users");
+ $resp->assertRedirect('/settings/users');
$resp = $this->followRedirects($resp);
- $resp->assertSee("User successfully removed");
+ $resp->assertSee('User successfully removed');
$this->assertActivityExists(ActivityType::USER_DELETE);
$this->assertDatabaseMissing('users', ['id' => $editor->id]);
{
$editor = $this->getEditor();
$resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete");
- $resp->assertSee("Migrate Ownership");
- $resp->assertSee("new_owner_id");
+ $resp->assertSee('Migrate Ownership');
+ $resp->assertSee('new_owner_id');
}
public function test_delete_with_new_owner_id_changes_ownership()
{
$page = Page::query()->first();
$owner = $page->ownedBy;
- $newOwner = User::query()->where('id', '!=' , $owner->id)->first();
+ $newOwner = User::query()->where('id', '!=', $owner->id)->first();
$this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]);
$this->assertDatabaseHas('pages', [
- 'id' => $page->id,
+ 'id' => $page->id,
'owned_by' => $newOwner->id,
]);
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\User;
+<?php
+
+namespace Tests\User;
use Tests\TestCase;
class UserPreferencesTest extends TestCase
{
-
public function test_update_sort_preference()
{
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/books', [
- 'sort' => 'created_at',
- 'order' => 'desc'
+ $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/books', [
+ 'sort' => 'created_at',
+ 'order' => 'desc',
]);
$updateRequest->assertStatus(302);
$this->assertDatabaseHas('settings', [
'setting_key' => 'user:' . $editor->id . ':books_sort',
- 'value' => 'created_at'
+ 'value' => 'created_at',
]);
$this->assertDatabaseHas('settings', [
'setting_key' => 'user:' . $editor->id . ':books_sort_order',
- 'value' => 'desc'
+ 'value' => 'desc',
]);
$this->assertEquals('created_at', setting()->getForCurrentUser('books_sort'));
$this->assertEquals('desc', setting()->getForCurrentUser('books_sort_order'));
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/bookshelves', [
- 'sort' => 'cat',
- 'order' => 'dog'
+ $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/bookshelves', [
+ 'sort' => 'cat',
+ 'order' => 'dog',
]);
$updateRequest->assertStatus(302);
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id.'/change-sort/dogs', [
- 'sort' => 'name',
- 'order' => 'asc'
+ $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/dogs', [
+ 'sort' => 'name',
+ 'order' => 'asc',
]);
$updateRequest->assertStatus(500);
$editor = $this->getEditor();
$this->actingAs($editor);
- $updateRequest = $this->patch('/settings/users/' . $editor->id.'/update-expansion-preference/home-details', ['expand' => 'true']);
+ $updateRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/home-details', ['expand' => 'true']);
$updateRequest->assertStatus(204);
$this->assertDatabaseHas('settings', [
'setting_key' => 'user:' . $editor->id . ':section_expansion#home-details',
- 'value' => 'true'
+ 'value' => 'true',
]);
$this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details'));
- $invalidKeyRequest = $this->patch('/settings/users/' . $editor->id.'/update-expansion-preference/my-home-details', ['expand' => 'true']);
+ $invalidKeyRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/my-home-details', ['expand' => 'true']);
$invalidKeyRequest->assertStatus(500);
}
$home = $this->get('/login');
$home->assertElementExists('.dark-mode');
}
-}
\ No newline at end of file
+}
-<?php namespace Tests\User;
+<?php
+
+namespace Tests\User;
use Activity;
use BookStack\Actions\ActivityType;
->pageNotHasElement('.content-wrap .entity-list-item')
->see('List View');
}
-
}