APP_KEY=SomeRandomString
# Application URL
-# Remove the hash below and set a URL if using BookStack behind
-# a proxy or if using a third-party authentication option.
# This must be the root URL that you want to host BookStack on.
-# All URL's in BookStack will be generated using this value.
-#APP_URL=https://example.com
+# All URLs in BookStack will be generated using this value
+# to ensure URLs generated are consistent and secure.
+# If you change this in the future you may need to run a command
+# to update stored URLs in the database. Command example:
+# php artisan bookstack:update-url https://old.example.com https://new.example.com
+APP_URL=https://example.com
# Database details
DB_HOST=localhost
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
-# Mail sender options
-MAIL_FROM_NAME=BookStack
+# Mail sender details
+MAIL_FROM_NAME="BookStack"
# SMTP mail options
# If set to 'false' a limit will not be enforced.
REVISION_LIMIT=50
+# Recycle Bin Lifetime
+# The number of days that content will remain in the recycle bin before
+# being considered for auto-removal. It is not a guarantee that content will
+# be removed after this time.
+# Set to 0 for no recycle bin functionality.
+# Set to -1 for unlimited recycle bin lifetime.
+RECYCLE_BIN_LIFETIME=30
+
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null
+# A list of hosts that BookStack can be iframed within.
+# Space separated if multiple. BookStack host domain is auto-inferred.
+# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
+# Setting this option will also auto-adjust cookies to be SameSite=None.
+ALLOWED_IFRAME_HOSTS=null
+
# The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
--- /dev/null
+# These are supported funding model platforms
+
+github: [ssddanbrown]
namespace BookStack\Actions;
use BookStack\Auth\User;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
use BookStack\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Support\Str;
/**
- * @property string $key
+ * @property string $type
* @property User $user
* @property Entity $entity
- * @property string $extra
+ * @property string $detail
* @property string $entity_type
* @property int $entity_id
* @property int $user_id
- * @property int $book_id
*/
class Activity extends Model
{
/**
* Get the user this activity relates to.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function user()
+ public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
- * Returns text from the language files, Looks up by using the
- * activity key.
+ * Returns text from the language files, Looks up by using the activity key.
*/
- public function getText()
+ public function getText(): string
{
- return trans('activities.' . $this->key);
+ return trans('activities.' . $this->type);
+ }
+
+ /**
+ * Check if this activity is intended to be for an entity.
+ */
+ public function isForEntity(): bool
+ {
+ return Str::startsWith($this->type, [
+ 'page_', 'chapter_', 'book_', 'bookshelf_'
+ ]);
}
/**
*/
public function isSimilarTo(Activity $activityB): bool
{
- return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
+ return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id];
}
}
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
-use BookStack\Entities\Entity;
-use Illuminate\Support\Collection;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Interfaces\Loggable;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Log;
class ActivityService
{
protected $activity;
- protected $user;
protected $permissionService;
- /**
- * ActivityService constructor.
- */
public function __construct(Activity $activity, PermissionService $permissionService)
{
$this->activity = $activity;
$this->permissionService = $permissionService;
- $this->user = user();
}
/**
- * Add activity data to database.
+ * Add activity data to database for an entity.
*/
- public function add(Entity $entity, string $activityKey, ?int $bookId = null)
+ public function addForEntity(Entity $entity, string $type)
{
- $activity = $this->newActivityForUser($activityKey, $bookId);
+ $activity = $this->newActivityForUser($type);
$entity->activity()->save($activity);
- $this->setNotification($activityKey);
+ $this->setNotification($type);
}
/**
- * Adds a activity history with a message, without binding to a entity.
+ * Add a generic activity event to the database.
+ * @param string|Loggable $detail
*/
- public function addMessage(string $activityKey, string $message, ?int $bookId = null)
+ public function add(string $type, $detail = '')
{
- $this->newActivityForUser($activityKey, $bookId)->forceFill([
- 'extra' => $message
- ])->save();
+ if ($detail instanceof Loggable) {
+ $detail = $detail->logDescriptor();
+ }
- $this->setNotification($activityKey);
+ $activity = $this->newActivityForUser($type);
+ $activity->detail = $detail;
+ $activity->save();
+ $this->setNotification($type);
}
/**
* Get a new activity instance for the current user.
*/
- protected function newActivityForUser(string $key, ?int $bookId = null): Activity
+ protected function newActivityForUser(string $type): Activity
{
return $this->activity->newInstance()->forceFill([
- 'key' => strtolower($key),
- 'user_id' => $this->user->id,
- 'book_id' => $bookId ?? 0,
+ 'type' => strtolower($type),
+ 'user_id' => user()->id,
]);
}
* and instead uses the 'extra' field with the entities name.
* Used when an entity is deleted.
*/
- public function removeEntity(Entity $entity): Collection
+ public function removeEntity(Entity $entity)
{
- $activities = $entity->activity()->get();
$entity->activity()->update([
- 'extra' => $entity->name,
- 'entity_id' => 0,
- 'entity_type' => '',
+ 'detail' => $entity->name,
+ 'entity_id' => null,
+ 'entity_type' => null,
]);
- return $activities;
}
/**
*/
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
{
+ /** @var [string => int[]] $queryIds */
+ $queryIds = [$entity->getMorphClass() => [$entity->id]];
+
if ($entity->isA('book')) {
- $query = $this->activity->newQuery()->where('book_id', '=', $entity->id);
- } else {
- $query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
- ->where('entity_id', '=', $entity->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');
}
- $activity = $this->permissionService
- ->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
- ->orderBy('created_at', 'desc')
- ->with(['entity', 'user.avatar'])
+ $query = $this->activity->newQuery();
+ $query->where(function (Builder $query) use ($queryIds) {
+ foreach ($queryIds as $morphClass => $idArr) {
+ $query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
+ $innerQuery->where('entity_type', '=', $morphClass)
+ ->whereIn('entity_id', $idArr);
+ });
+ }
+ });
+
+ $activity = $query->orderBy('created_at', 'desc')
+ ->with(['entity' => function (Relation $query) {
+ $query->withTrashed();
+ }, 'user.avatar'])
->skip($count * ($page - 1))
->take($count)
->get();
/**
* Flashes a notification message to the session if an appropriate message is available.
*/
- protected function setNotification(string $activityKey)
+ protected function setNotification(string $type)
{
- $notificationTextKey = 'activities.' . $activityKey . '_notification';
+ $notificationTextKey = 'activities.' . $type . '_notification';
if (trans()->has($notificationTextKey)) {
$message = trans($notificationTextKey);
session()->flash('success', $message);
--- /dev/null
+<?php namespace BookStack\Actions;
+
+class ActivityType
+{
+ const PAGE_CREATE = 'page_create';
+ const PAGE_UPDATE = 'page_update';
+ const PAGE_DELETE = 'page_delete';
+ const PAGE_RESTORE = 'page_restore';
+ const PAGE_MOVE = 'page_move';
+
+ const CHAPTER_CREATE = 'chapter_create';
+ const CHAPTER_UPDATE = 'chapter_update';
+ const CHAPTER_DELETE = 'chapter_delete';
+ const CHAPTER_MOVE = 'chapter_move';
+
+ const BOOK_CREATE = 'book_create';
+ const BOOK_UPDATE = 'book_update';
+ const BOOK_DELETE = 'book_delete';
+ const BOOK_SORT = 'book_sort';
+
+ const BOOKSHELF_CREATE = 'bookshelf_create';
+ const BOOKSHELF_UPDATE = 'bookshelf_update';
+ const BOOKSHELF_DELETE = 'bookshelf_delete';
+
+ const COMMENTED_ON = 'commented_on';
+ const PERMISSIONS_UPDATE = 'permissions_update';
+
+ const SETTINGS_UPDATE = 'settings_update';
+ const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
+
+ const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
+ const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
+ const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
+
+ const USER_CREATE = 'user_create';
+ const USER_UPDATE = 'user_update';
+ const USER_DELETE = 'user_delete';
+
+ const API_TOKEN_CREATE = 'api_token_create';
+ const API_TOKEN_UPDATE = 'api_token_update';
+ const API_TOKEN_DELETE = 'api_token_delete';
+
+ const ROLE_CREATE = 'role_create';
+ const ROLE_UPDATE = 'role_update';
+ const ROLE_DELETE = 'role_delete';
+
+ const AUTH_PASSWORD_RESET = 'auth_password_reset_request';
+ const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
+ const AUTH_LOGIN = 'auth_login';
+ const AUTH_REGISTER = 'auth_register';
+}
\ No newline at end of file
<?php namespace BookStack\Actions;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property string text
* @property int|null parent_id
* @property int local_id
*/
-class Comment extends Ownable
+class Comment extends Model
{
+ use HasCreatorAndUpdater;
+
protected $fillable = ['text', 'parent_id'];
protected $appends = ['created', 'updated'];
/**
* Get the entity that this comment belongs to
- * @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
- public function entity()
+ public function entity(): MorphTo
{
return $this->morphTo('entity');
}
/**
* Check if a comment has been updated since creation.
- * @return bool
*/
- public function isUpdated()
+ public function isUpdated(): bool
{
return $this->updated_at->timestamp > $this->created_at->timestamp;
}
<?php namespace BookStack\Actions;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
use League\CommonMark\CommonMarkConverter;
+use BookStack\Facades\Activity as ActivityService;
/**
* Class CommentRepo
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
+ ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
return $comment;
}
use BookStack\Model;
-/**
- * Class Attribute
- * @package BookStack
- */
class Tag extends Model
{
protected $fillable = ['name', 'value', 'order'];
- protected $hidden = ['id', 'entity_id', 'entity_type'];
+ protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
/**
* Get the entity that this tag belongs to
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
use DB;
use Illuminate\Support\Collection;
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Book;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use DB;
use Illuminate\Support\Collection;
/**
* Add a view to the given entity.
- * @param \BookStack\Entities\Entity $entity
+ * @param \BookStack\Entities\Models\Entity $entity
* @return int
*/
public function add(Entity $entity)
/**
* Get all recently viewed entities for the current user.
- * @param int $count
- * @param int $page
- * @param Entity|bool $filterModel
- * @return mixed
*/
- public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
+ public function getUserRecentlyViewed(int $count = 10, int $page = 1)
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
- $query = $this->permissionService
- ->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
-
- if ($filterModel) {
- $query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
+ $all = collect();
+ /** @var Entity $instance */
+ foreach ($this->entityProvider->all() as $name => $instance) {
+ $items = $instance::visible()->withLastView()
+ ->orderBy('last_viewed_at', 'desc')
+ ->skip($count * ($page - 1))
+ ->take($count)
+ ->get();
+ $all = $all->concat($items);
}
- $query = $query->where('user_id', '=', $user->id);
- $viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
- ->skip($count * $page)->take($count)->get()->pluck('viewable');
- return $viewables;
+ return $all->sortByDesc('last_viewed_at')->slice(0, $count);
}
/**
<?php namespace BookStack\Api;
use BookStack\Http\Controllers\Api\ApiController;
+use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use ReflectionClass;
protected $reflectionClasses = [];
protected $controllerClasses = [];
+ /**
+ * Load the docs form the cache if existing
+ * otherwise generate and store in the cache.
+ */
+ public static function generateConsideringCache(): Collection
+ {
+ $appVersion = trim(file_get_contents(base_path('version')));
+ $cacheKey = 'api-docs::' . $appVersion;
+ if (Cache::has($cacheKey) && config('app.env') === 'production') {
+ $docs = Cache::get($cacheKey);
+ } else {
+ $docs = (new static())->generate();
+ Cache::put($cacheKey, $docs, 60 * 24);
+ }
+ return $docs;
+ }
+
/**
* Generate API documentation.
*/
- public function generate(): Collection
+ protected function generate(): Collection
{
$apiRoutes = $this->getFlatApiRoutes();
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
/**
* Load body params and their rules by inspecting the given class and method name.
- * @throws \Illuminate\Contracts\Container\BindingResolutionException
+ * @throws BindingResolutionException
*/
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
{
<?php namespace BookStack\Api;
use BookStack\Auth\User;
+use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
-class ApiToken extends Model
+/**
+ * Class ApiToken
+ * @property int $id
+ * @property string $token_id
+ * @property string $secret
+ * @property string $name
+ * @property Carbon $expires_at
+ * @property User $user
+ */
+class ApiToken extends Model implements Loggable
{
protected $fillable = ['name', 'expires_at'];
protected $casts = [
{
return Carbon::now()->addYears(100)->format('Y-m-d');
}
+
+ /**
+ * @inheritdoc
+ */
+ public function logDescriptor(): string
+ {
+ return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
+ }
}
* guard with 'remember' functionality removed. Basic auth and event emission
* has also been removed to keep this simple. Designed to be extended by external
* Auth Guards.
- *
- * @package Illuminate\Auth
*/
class ExternalBaseSessionGuard implements StatefulGuard
{
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
- *
- * @package BookStack\Auth\Access\Guards
*/
class Saml2SessionGuard extends ExternalBaseSessionGuard
{
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
- * @package BookStack\Services
*/
class Ldap
{
<?php namespace BookStack\Auth\Access;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
use Exception;
class RegistrationService
$newUser->socialAccounts()->save($socialAccount);
}
+ Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
+
// Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
$newUser->save();
<?php namespace BookStack\Auth\Access;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\SamlException;
use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
use Exception;
use Illuminate\Support\Str;
use OneLogin\Saml2\Auth;
}
auth()->login($user);
+ Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
return $user;
}
}
<?php namespace BookStack\Auth\Access;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Activity;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider;
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
auth()->login($socialAccount->user);
+ Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
return redirect()->intended('/');
}
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Page;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use BookStack\Traits\HasOwner;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
/**
* PermissionService constructor.
- * @param JointPermission $jointPermission
- * @param EntityPermission $entityPermission
- * @param Role $role
- * @param Connection $db
- * @param EntityProvider $entityProvider
*/
public function __construct(
JointPermission $jointPermission,
/**
* Prepare the local entity cache and ensure it's empty
- * @param \BookStack\Entities\Entity[] $entities
+ * @param \BookStack\Entities\Models\Entity[] $entities
*/
protected function readyEntityCache($entities = [])
{
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
- * @return \BookStack\Entities\Book
+ * @return \BookStack\Entities\Models\Book
*/
protected function getChapter($chapterId)
{
});
// Chunk through all bookshelves
- $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
+ $this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
*/
protected function bookFetchQuery()
{
- return $this->entityProvider->book->newQuery()
- ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
- $query->select(['id', 'restricted', 'created_by', 'book_id']);
+ return $this->entityProvider->book->withTrashed()->newQuery()
+ ->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
+ $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
}, 'pages' => function ($query) {
- $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
+ $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
}]);
}
/**
* Rebuild the entity jointPermissions for a particular entity.
- * @param \BookStack\Entities\Entity $entity
+ * @param \BookStack\Entities\Models\Entity $entity
* @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
});
// Chunk through all bookshelves
- $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
+ $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
/**
* Delete all of the entity jointPermissions for a list of entities.
- * @param \BookStack\Entities\Entity[] $entities
+ * @param \BookStack\Entities\Models\Entity[] $entities
* @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
/**
* Get the actions related to an entity.
- * @param \BookStack\Entities\Entity $entity
+ * @param \BookStack\Entities\Models\Entity $entity
* @return array
*/
protected function getActions(Entity $entity)
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
- * @param \BookStack\Entities\Entity $entity
+ * @param \BookStack\Entities\Models\Entity $entity
* @param Role $role
* @param $action
* @param $permissionAll
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
- 'created_by' => $entity->getRawAttribute('created_by')
+ 'owned_by' => $entity->getRawAttribute('owned_by')
];
}
/**
* Checks if an entity has a restriction set upon it.
- * @param Ownable $ownable
- * @param $permission
- * @return bool
+ * @param HasCreatorAndUpdater|HasOwner $ownable
*/
- public function checkOwnableUserAccess(Ownable $ownable, $permission)
+ public function checkOwnableUserAccess(Model $ownable, string $permission): bool
{
$explodedPermission = explode('-', $permission);
- $baseQuery = $ownable->where('id', '=', $ownable->id);
+ $baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
- ->where('created_by', '=', $userId);
+ ->where('owned_by', '=', $userId);
});
});
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
- * @param \BookStack\Entities\Entity $entity
+ * @param \BookStack\Entities\Models\Entity $entity
* @param $action
* @return bool|mixed
*/
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
});
$query->where('has_permission', '=', true)
->orWhere(function (Builder $query) {
$query->where('has_permission_own', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
});
$query->where('draft', '=', false)
->orWhere(function (Builder $query) {
$query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
}
/**
* Add restrictions for a generic entity
* @param string $entityType
- * @param Builder|\BookStack\Entities\Entity $query
+ * @param Builder|\BookStack\Entities\Models\Entity $query
* @param string $action
* @return Builder
*/
$query->where('draft', '=', false)
->orWhere(function ($query) {
$query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
}
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
});
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
});
<?php namespace BookStack\Auth\Permissions;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
+use BookStack\Facades\Activity;
use Exception;
use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Support\Str;
class PermissionsRepo
{
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionService->buildJointPermissionForRole($role);
+ Activity::add(ActivityType::ROLE_CREATE, $role);
return $role;
}
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);
+ Activity::add(ActivityType::ROLE_UPDATE, $role);
}
/**
* Assign an list of permission names to an role.
*/
- public function assignRolePermissions(Role $role, array $permissionNameArray = [])
+ protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
}
$this->permissionService->deleteJointPermissionsForRole($role);
+ Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}
}
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
+use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property string $external_auth_id
* @property string $system_name
*/
-class Role extends Model
+class Role extends Model implements Loggable
{
protected $fillable = ['display_name', 'description', 'external_auth_id'];
/**
* The roles that belong to the role.
*/
- public function users()
+ public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
* The RolePermissions that belong to the role.
*/
- public function permissions()
+ public function permissions(): BelongsToMany
{
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
{
return static::query()->where('system_name', '!=', 'admin')->get();
}
+
+ /**
+ * @inheritdoc
+ */
+ public function logDescriptor(): string
+ {
+ return "({$this->id}) {$this->display_name}";
+ }
}
<?php namespace BookStack\Auth;
+use BookStack\Interfaces\Loggable;
use BookStack\Model;
-class SocialAccount extends Model
+/**
+ * Class SocialAccount
+ * @property string $driver
+ * @property User $user
+ */
+class SocialAccount extends Model implements Loggable
{
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
{
return $this->belongsTo(User::class);
}
+
+ /**
+ * @inheritDoc
+ */
+ public function logDescriptor(): string
+ {
+ return "{$this->driver}; {$this->user->logDescriptor()}";
+ }
}
<?php namespace BookStack\Auth;
+use BookStack\Actions\Activity;
use BookStack\Api\ApiToken;
+use BookStack\Interfaces\Loggable;
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Notifications\Notifiable;
+use Illuminate\Support\Collection;
/**
* Class User
- * @package BookStack\Auth
* @property string $id
* @property string $name
* @property string $email
* @property string $external_auth_id
* @property string $system_name
*/
-class User extends Model implements AuthenticatableContract, CanResetPasswordContract
+class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
{
use Authenticatable, CanResetPassword, Notifiable;
/**
* This holds the user's permissions when loaded.
- * @var array
+ * @var ?Collection
*/
protected $permissions;
}
}
+ /**
+ * Check if the user has a particular permission.
+ */
+ public function can(string $permissionName): bool
+ {
+ if ($this->email === 'guest') {
+ return false;
+ }
+
+ return $this->permissions()->contains($permissionName);
+ }
+
/**
* Get all permissions belonging to a the current user.
- * @param bool $cache
- * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
- public function permissions($cache = true)
+ protected function permissions(): Collection
{
- if (isset($this->permissions) && $cache) {
+ if (isset($this->permissions)) {
return $this->permissions;
}
- $this->load('roles.permissions');
- $permissions = $this->roles->map(function ($role) {
- return $role->permissions;
- })->flatten()->unique();
- $this->permissions = $permissions;
- return $permissions;
+
+ $this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
+ ->select('role_permissions.name as name')->distinct()
+ ->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
+ ->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
+ ->where('ru.user_id', '=', $this->id)
+ ->get()
+ ->pluck('name');
+
+ return $this->permissions;
}
/**
- * Check if the user has a particular permission.
- * @param $permissionName
- * @return bool
+ * Clear any cached permissions on this instance.
*/
- public function can($permissionName)
+ public function clearPermissionCache()
{
- if ($this->email === 'guest') {
- return false;
- }
- return $this->permissions()->pluck('name')->contains($permissionName);
+ $this->permissions = null;
}
/**
return $this->hasMany(ApiToken::class);
}
+ /**
+ * Get the latest activity instance for this user.
+ */
+ public function latestActivity(): HasOne
+ {
+ return $this->hasOne(Activity::class)->latest();
+ }
+
/**
* Get the url for editing this user.
*/
{
$this->notify(new ResetPassword($token));
}
+
+ /**
+ * @inheritdoc
+ */
+ public function logDescriptor(): string
+ {
+ return "({$this->id}) {$this->name}";
+ }
}
<?php namespace BookStack\Auth;
use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
+use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
{
-
- protected $user;
- protected $role;
+ protected $userAvatar;
/**
* UserRepo constructor.
*/
- public function __construct(User $user, Role $role)
+ public function __construct(UserAvatars $userAvatar)
{
- $this->user = $user;
- $this->role = $role;
+ $this->userAvatar = $userAvatar;
}
/**
*/
public function getByEmail(string $email): ?User
{
- return $this->user->where('email', '=', $email)->first();
+ return User::query()->where('email', '=', $email)->first();
}
/**
- * @param int $id
- * @return User
+ * Get a user by their ID.
*/
- public function getById($id)
+ public function getById(int $id): User
{
- return $this->user->newQuery()->findOrFail($id);
+ return User::query()->findOrFail($id);
}
/**
* Get all the users with their permissions.
- * @return Builder|static
*/
- public function getAllUsers()
+ public function getAllUsers(): Collection
{
- return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
+ return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
* Get all the users with their permissions in a paginated format.
- * @param int $count
- * @param $sortData
- * @return Builder|static
*/
- public function getAllUsersPaginatedAndSorted($count, $sortData)
+ public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
{
- $query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
+ $sort = $sortData['sort'];
+ if ($sort === 'latest_activity') {
+ $sort = \BookStack\Actions\Activity::query()->select('created_at')
+ ->whereColumn('activities.user_id', 'users.id')
+ ->latest()
+ ->take(1);
+ }
+
+ $query = User::query()->with(['roles', 'avatar', 'latestActivity'])
+ ->orderBy($sort, $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
/**
* Assign a user to a system-level role.
- * @param User $user
- * @param $systemRoleName
* @throws NotFoundException
*/
- public function attachSystemRole(User $user, $systemRoleName)
+ public function attachSystemRole(User $user, string $systemRoleName)
{
- $role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
- if ($role === null) {
+ $role = Role::getSystemRole($systemRoleName);
+ if (is_null($role)) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
/**
* Checks if the give user is the only admin.
- * @param User $user
- * @return bool
*/
- public function isOnlyAdmin(User $user)
+ public function isOnlyAdmin(User $user): bool
{
if (!$user->hasSystemRole('admin')) {
return false;
}
- $adminRole = $this->role->getSystemRole('admin');
- if ($adminRole->users->count() > 1) {
+ $adminRole = Role::getSystemRole('admin');
+ if ($adminRole->users()->count() > 1) {
return false;
}
+
return true;
}
/**
* Set the assigned user roles via an array of role IDs.
- * @param User $user
- * @param array $roles
* @throws UserUpdateException
*/
public function setUserRoles(User $user, array $roles)
/**
* Check if the given user is the last admin and their new roles no longer
* contains the admin role.
- * @param User $user
- * @param array $newRoles
- * @return bool
*/
protected function demotingLastAdmin(User $user, array $newRoles) : bool
{
if ($this->isOnlyAdmin($user)) {
- $adminRole = $this->role->getSystemRole('admin');
+ $adminRole = Role::getSystemRole('admin');
if (!in_array(strval($adminRole->id), $newRoles)) {
return true;
}
*/
public function create(array $data, bool $emailConfirmed = false): User
{
- return $this->user->forceCreate([
+ $details = [
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => $emailConfirmed,
'external_auth_id' => $data['external_auth_id'] ?? '',
- ]);
+ ];
+ return User::query()->forceCreate($details);
}
/**
* Remove the given user from storage, Delete all related content.
- * @param User $user
* @throws Exception
*/
- public function destroy(User $user)
+ public function destroy(User $user, ?int $newOwnerId = null)
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
$user->delete();
// Delete user profile images
- $profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
+ $profileImages = Image::query()->where('type', '=', 'user')
+ ->where('uploaded_to', '=', $user->id)
+ ->get();
+
foreach ($profileImages as $image) {
Images::destroy($image);
}
+
+ if (!empty($newOwnerId)) {
+ $newOwner = User::query()->find($newOwnerId);
+ if (!is_null($newOwner)) {
+ $this->migrateOwnership($user, $newOwner);
+ }
+ }
+ }
+
+ /**
+ * Migrate ownership of items in the system from one user to another.
+ */
+ protected function migrateOwnership(User $fromUser, User $toUser)
+ {
+ $entities = (new EntityProvider)->all();
+ foreach ($entities as $instance) {
+ $instance->newQuery()->where('owned_by', '=', $fromUser->id)
+ ->update(['owned_by' => $toUser->id]);
+ }
}
/**
* Get the latest activity for a user.
- * @param User $user
- * @param int $count
- * @param int $page
- * @return array
*/
- public function getActivity(User $user, $count = 20, $page = 0)
+ public function getActivity(User $user, int $count = 20, int $page = 0): array
{
return Activity::userActivity($user, $count, $page);
}
/**
* Get the roles in the system that are assignable to a user.
- * @return mixed
*/
- public function getAllRoles()
+ public function getAllRoles(): Collection
{
- return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
+ return Role::query()->orderBy('display_name', 'asc')->get();
}
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
- * @param User $user
- * @return bool
*/
- public function downloadAndAssignUserAvatar(User $user)
+ public function downloadAndAssignUserAvatar(User $user): void
{
- if (!Images::avatarFetchEnabled()) {
- return false;
- }
-
try {
- $avatar = Images::saveUserAvatar($user);
- $user->avatar()->associate($avatar);
- $user->save();
- return true;
+ $this->userAvatar->fetchAndAssignToUser($user);
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
- return false;
}
}
}
// If set to false then a limit will not be enforced.
'revision_limit' => env('REVISION_LIMIT', 50),
+ // The number of days that content will remain in the recycle bin before
+ // being considered for auto-removal. It is not a guarantee that content will
+ // be removed after this time.
+ // Set to 0 for no recycle bin functionality.
+ // Set to -1 for unlimited recycle bin lifetime.
+ 'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
+
// Allow <script> tags to entered within page content.
// <script> tags are escaped by default.
// Even when overridden the WYSIWYG editor may still escape script content.
// and used by BookStack in URL generation.
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
+ // A list of hosts that BookStack can be iframed within.
+ // Space separated if multiple. BookStack host domain is auto-inferred.
+ 'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
+
// Application timezone for back-end date functions.
'timezone' => env('APP_TIMEZONE', 'UTC'),
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class,
+ BookStack\Providers\CustomValidationServiceProvider::class,
],
/*
'root' => storage_path(),
],
- 'ftp' => [
- 'driver' => 'ftp',
- 'host' => 'ftp.example.com',
- 'username' => 'your-username',
- 'password' => 'your-password',
- ],
-
's3' => [
'driver' => 's3',
'key' => env('STORAGE_S3_KEY', 'your-key'),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
- 'rackspace' => [
- 'driver' => 'rackspace',
- 'username' => 'your-username',
- 'key' => 'your-key',
- 'container' => 'your-container',
- 'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
- 'region' => 'IAD',
- 'url_type' => 'publicURL',
- ],
-
],
];
<?php
+use \Illuminate\Support\Str;
+
/**
* Session configuration options.
*
// By setting this option to true, session cookies will only be sent back
// to the server if the browser has a HTTPS connection. This will keep
// the cookie from being sent to you if it can not be done securely.
- 'secure' => env('SESSION_SECURE_COOKIE', false),
+ 'secure' => env('SESSION_SECURE_COOKIE', null)
+ ?? Str::startsWith(env('APP_URL'), 'https:'),
// HTTP Access Only
// Setting this value to true will prevent JavaScript from accessing the
// This option determines how your cookies behave when cross-site requests
// take place, and can be used to mitigate CSRF attacks. By default, we
// do not enable this as other CSRF protection services are in place.
- // Options: lax, strict
- 'same_site' => null,
+ // Options: lax, strict, none
+ 'same_site' => 'lax',
];
* @var string
*/
protected $signature = 'bookstack:cleanup-images
- {--a|all : Include images that are used in page revisions}
- {--f|force : Actually run the deletions}
+ {--a|all : Also delete images that are only used in old revisions}
+ {--f|force : Actually run the deletions, Defaults to a dry-run}
';
/**
namespace BookStack\Console\Commands;
-use BookStack\Entities\PageRevision;
+use BookStack\Entities\Models\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command
namespace BookStack\Console\Commands;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Console\Command;
/**
* Create a new command instance.
- *
- * @param UserRepo $userRepo
*/
public function __construct(UserRepo $userRepo)
{
namespace BookStack\Console\Commands;
-use BookStack\Entities\SearchService;
+use BookStack\Entities\Tools\SearchIndex;
use DB;
use Illuminate\Console\Command;
*/
protected $description = 'Re-index all content for searching';
- protected $searchService;
+ protected $searchIndex;
/**
* Create a new command instance.
- *
- * @param SearchService $searchService
*/
- public function __construct(SearchService $searchService)
+ public function __construct(SearchIndex $searchIndex)
{
parent::__construct();
- $this->searchService = $searchService;
+ $this->searchIndex = $searchIndex;
}
/**
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
- $this->searchService->setConnection(DB::connection($this->option('database')));
}
- $this->searchService->indexAllEntities();
+ $this->searchIndex->indexAllEntities();
DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}
<?php namespace BookStack\Entities;
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\ShelfContext;
use Illuminate\View\View;
class BreadcrumbsViewComposer
/**
* BreadcrumbsViewComposer constructor.
- * @param EntityContext $entityContextManager
+ * @param ShelfContext $entityContextManager
*/
- public function __construct(EntityContext $entityContextManager)
+ public function __construct(ShelfContext $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
+++ /dev/null
-<?php namespace BookStack\Entities;
-
-use Illuminate\Support\Collection;
-
-/**
- * Class Chapter
- * @property Collection<Page> $pages
- * @package BookStack\Entities
- */
-class Chapter extends BookChild
-{
- public $searchFactor = 1.3;
-
- protected $fillable = ['name', 'description', 'priority', 'book_id'];
- protected $hidden = ['restricted', 'pivot'];
-
- /**
- * Get the pages that this chapter contains.
- * @param string $dir
- * @return mixed
- */
- public function pages($dir = 'ASC')
- {
- return $this->hasMany(Page::class)->orderBy('priority', $dir);
- }
-
- /**
- * Get the url of this chapter.
- * @param string|bool $path
- * @return string
- */
- public function getUrl($path = false)
- {
- $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
- $fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
-
- if ($path !== false) {
- $fullPath .= '/' . trim($path, '/');
- }
-
- return url($fullPath);
- }
-
- /**
- * Get an excerpt of this chapter's description to the specified length or less.
- * @param int $length
- * @return string
- */
- public function getExcerpt(int $length = 100)
- {
- $description = $this->text ?? $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
- }
-
- /**
- * Check if this chapter has any child pages.
- * @return bool
- */
- public function hasChildren()
- {
- return count($this->pages) > 0;
- }
-
- /**
- * Get the visible pages in this chapter.
- */
- public function getVisiblePages(): Collection
- {
- return $this->pages()->visible()
- ->orderBy('draft', 'desc')
- ->orderBy('priority', 'asc')
- ->get();
- }
-}
<?php namespace BookStack\Entities;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
+
/**
* Class EntityProvider
*
* Provides access to the core entity models.
* Wrapped up in this provider since they are often used together
* so this is a neater alternative to injecting all in individually.
- *
- * @package BookStack\Entities
*/
class EntityProvider
{
*/
public $pageRevision;
- /**
- * EntityProvider constructor.
- */
- public function __construct(
- Bookshelf $bookshelf,
- Book $book,
- Chapter $chapter,
- Page $page,
- PageRevision $pageRevision
- ) {
- $this->bookshelf = $bookshelf;
- $this->book = $book;
- $this->chapter = $chapter;
- $this->page = $page;
- $this->pageRevision = $pageRevision;
+
+ public function __construct()
+ {
+ $this->bookshelf = new Bookshelf();
+ $this->book = new Book();
+ $this->chapter = new Chapter();
+ $this->page = new Page();
+ $this->pageRevision = new PageRevision();
}
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
+ * @return array<Entity>
*/
public function all(): array
{
+++ /dev/null
-<?php namespace BookStack\Entities\Managers;
-
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\HasCoverImage;
-use BookStack\Entities\Page;
-use BookStack\Exceptions\NotifyException;
-use BookStack\Facades\Activity;
-use BookStack\Uploads\AttachmentService;
-use BookStack\Uploads\ImageService;
-use Exception;
-use Illuminate\Contracts\Container\BindingResolutionException;
-
-class TrashCan
-{
-
- /**
- * Remove a bookshelf from the system.
- * @throws Exception
- */
- public function destroyShelf(Bookshelf $shelf)
- {
- $this->destroyCommonRelations($shelf);
- $shelf->delete();
- }
-
- /**
- * Remove a book from the system.
- * @throws NotifyException
- * @throws BindingResolutionException
- */
- public function destroyBook(Book $book)
- {
- foreach ($book->pages as $page) {
- $this->destroyPage($page);
- }
-
- foreach ($book->chapters as $chapter) {
- $this->destroyChapter($chapter);
- }
-
- $this->destroyCommonRelations($book);
- $book->delete();
- }
-
- /**
- * Remove a page from the system.
- * @throws NotifyException
- */
- public function destroyPage(Page $page)
- {
- // Check if set as custom homepage & remove setting if not used or throw error if active
- $customHome = setting('app-homepage', '0:');
- if (intval($page->id) === intval(explode(':', $customHome)[0])) {
- if (setting('app-homepage-type') === 'page') {
- throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
- }
- setting()->remove('app-homepage');
- }
-
- $this->destroyCommonRelations($page);
-
- // Delete Attached Files
- $attachmentService = app(AttachmentService::class);
- foreach ($page->attachments as $attachment) {
- $attachmentService->deleteFile($attachment);
- }
-
- $page->delete();
- }
-
- /**
- * Remove a chapter from the system.
- * @throws Exception
- */
- public function destroyChapter(Chapter $chapter)
- {
- if (count($chapter->pages) > 0) {
- foreach ($chapter->pages as $page) {
- $page->chapter_id = 0;
- $page->save();
- }
- }
-
- $this->destroyCommonRelations($chapter);
- $chapter->delete();
- }
-
- /**
- * Update entity relations to remove or update outstanding connections.
- */
- protected function destroyCommonRelations(Entity $entity)
- {
- Activity::removeEntity($entity);
- $entity->views()->delete();
- $entity->permissions()->delete();
- $entity->tags()->delete();
- $entity->comments()->delete();
- $entity->jointPermissions()->delete();
- $entity->searchTerms()->delete();
-
- if ($entity instanceof HasCoverImage && $entity->cover) {
- $imageService = app()->make(ImageService::class);
- $imageService->destroy($entity->cover);
- }
- }
-}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Exception;
* @property string $description
* @property int $image_id
* @property Image|null $cover
- * @package BookStack\Entities
*/
class Book extends Entity implements HasCoverImage
{
public $searchFactor = 2;
protected $fillable = ['name', 'description'];
- protected $hidden = ['restricted', 'pivot', 'image_id'];
+ protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
/**
* Get the url for this book.
- * @param string|bool $path
- * @return string
*/
- public function getUrl($path = false)
+ public function getUrl(string $path = ''): string
{
- if ($path !== false) {
- return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
- }
- return url('/books/' . urlencode($this->slug));
+ return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
}
/**
$chapters = $this->chapters()->visible()->get();
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
-
- /**
- * Get an excerpt of this book's description to the specified length or less.
- * @param int $length
- * @return string
- */
- public function getExcerpt(int $length = 100)
- {
- $description = $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
- }
}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Book;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property Book $book
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
*/
-class BookChild extends Entity
+abstract class BookChild extends Entity
{
/**
$this->save();
$this->refresh();
- // Update related activity
- $this->activity()->update(['book_id' => $newBookId]);
-
// Update all child pages if a chapter
if ($this instanceof Chapter) {
foreach ($this->pages as $page) {
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
protected $fillable = ['name', 'description', 'image_id'];
- protected $hidden = ['restricted', 'image_id'];
+ protected $hidden = ['restricted', 'image_id', 'deleted_at'];
/**
* Get the books in this shelf.
/**
* Get the url for this bookshelf.
- * @param string|bool $path
- * @return string
*/
- public function getUrl($path = false)
+ public function getUrl(string $path = ''): string
{
- if ($path !== false) {
- return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
- }
- return url('/shelves/' . urlencode($this->slug));
+ return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
}
/**
return 'cover_shelf';
}
- /**
- * Get an excerpt of this book's description to the specified length or less.
- * @param int $length
- * @return string
- */
- public function getExcerpt(int $length = 100)
- {
- $description = $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
- }
-
/**
* Check if this shelf contains the given book.
* @param Book $book
--- /dev/null
+<?php namespace BookStack\Entities\Models;
+
+use Illuminate\Support\Collection;
+
+/**
+ * Class Chapter
+ * @property Collection<Page> $pages
+ */
+class Chapter extends BookChild
+{
+ public $searchFactor = 1.3;
+
+ protected $fillable = ['name', 'description', 'priority', 'book_id'];
+ protected $hidden = ['restricted', 'pivot', 'deleted_at'];
+
+ /**
+ * Get the pages that this chapter contains.
+ * @param string $dir
+ * @return mixed
+ */
+ public function pages($dir = 'ASC')
+ {
+ return $this->hasMany(Page::class)->orderBy('priority', $dir);
+ }
+
+ /**
+ * Get the url of this chapter.
+ */
+ public function getUrl($path = ''): string
+ {
+ $parts = [
+ 'books',
+ urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+ 'chapter',
+ urlencode($this->slug),
+ trim($path, '/'),
+ ];
+
+ return url('/' . implode('/', $parts));
+ }
+
+ /**
+ * Get the visible pages in this chapter.
+ */
+ public function getVisiblePages(): Collection
+ {
+ return $this->pages()->visible()
+ ->orderBy('draft', 'desc')
+ ->orderBy('priority', 'asc')
+ ->get();
+ }
+}
--- /dev/null
+<?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;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
+
+class Deletion extends Model implements Loggable
+{
+
+ /**
+ * Get the related deletable record.
+ */
+ public function deletable(): MorphTo
+ {
+ return $this->morphTo('deletable')->withTrashed();
+ }
+
+ /**
+ * The the user that performed the deletion.
+ */
+ public function deleter(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'deleted_by');
+ }
+
+ /**
+ * Create a new deletion record for the provided entity.
+ */
+ public static function createForEntity(Entity $entity): Deletion
+ {
+ $record = (new self())->forceFill([
+ 'deleted_by' => user()->id,
+ 'deletable_type' => $entity->getMorphClass(),
+ '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;
+<?php namespace BookStack\Entities\Models;
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Entities\Tools\SearchIndex;
+use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use BookStack\Traits\HasOwner;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Entity
* @method static Entity|Builder hasPermission(string $permission)
* @method static Builder withLastView()
* @method static Builder withViewCount()
- *
- * @package BookStack\Entities
*/
-class Entity extends Ownable
+abstract class Entity extends Model
{
+ use SoftDeletes;
+ use HasCreatorAndUpdater;
+ use HasOwner;
/**
* @var string - Name of property where the main text content is found
/**
* Get the entities that are visible to the current user.
*/
- public function scopeVisible(Builder $query)
+ public function scopeVisible(Builder $query): Builder
{
return $this->scopeHasPermission($query, 'view');
}
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
- * @param $entity
- * @return bool
*/
- public function matches($entity)
+ public function matches(Entity $entity): bool
{
return [get_class($this), $this->id] === [get_class($entity), $entity->id];
}
/**
- * Checks if an entity matches or contains another given entity.
- * @param Entity $entity
- * @return bool
+ * Checks if the current entity matches or contains the given.
*/
- public function matchesOrContains(Entity $entity)
+ public function matchesOrContains(Entity $entity): bool
{
- $matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
-
- if ($matches) {
+ if ($this->matches($entity)) {
return true;
}
/**
* Gets the activity objects for this entity.
- * @return MorphMany
*/
- public function activity()
+ public function activity(): MorphMany
{
return $this->morphMany(Activity::class, 'entity')
->orderBy('created_at', 'desc');
/**
* Get View objects for this entity.
*/
- public function views()
+ public function views(): MorphMany
{
return $this->morphMany(View::class, 'viewable');
}
/**
* Get the Tag models that have been user assigned to this entity.
- * @return MorphMany
*/
- public function tags()
+ public function tags(): MorphMany
{
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the comments for an entity
- * @param bool $orderByCreated
- * @return MorphMany
*/
- public function comments($orderByCreated = true)
+ public function comments(bool $orderByCreated = true): MorphMany
{
$query = $this->morphMany(Comment::class, 'entity');
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
/**
* Get the related search terms.
- * @return MorphMany
*/
- public function searchTerms()
+ public function searchTerms(): MorphMany
{
return $this->morphMany(SearchTerm::class, 'entity');
}
/**
* Get this entities restrictions.
*/
- public function permissions()
+ public function permissions(): MorphMany
{
return $this->morphMany(EntityPermission::class, 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
- * @param $role_id
- * @param $action
- * @return bool
*/
- public function hasRestriction($role_id, $action)
+ public function hasRestriction(int $role_id, string $action): bool
{
return $this->permissions()->where('role_id', '=', $role_id)
->where('action', '=', $action)->count() > 0;
/**
* Get the entity jointPermissions this is connected to.
- * @return MorphMany
*/
- public function jointPermissions()
+ public function jointPermissions(): MorphMany
{
return $this->morphMany(JointPermission::class, 'entity');
}
/**
- * Check if this instance or class is a certain type of entity.
- * Examples of $type are 'page', 'book', 'chapter'
+ * Get the related delete records for this entity.
*/
- public static function isA(string $type): bool
+ public function deletions(): MorphMany
{
- return static::getType() === strtolower($type);
+ return $this->morphMany(Deletion::class, 'deletable');
}
/**
- * Get entity type.
- * @return mixed
+ * Check if this instance or class is a certain type of entity.
+ * Examples of $type are 'page', 'book', 'chapter'
*/
- public static function getType()
+ public static function isA(string $type): bool
{
- return strtolower(static::getClassName());
+ return static::getType() === strtolower($type);
}
/**
- * Get an instance of an entity of the given type.
- * @param $type
- * @return Entity
+ * Get the entity type as a simple lowercase word.
*/
- public static function getEntityInstance($type)
+ public static function getType(): string
{
- $types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
- $className = str_replace([' ', '-', '_'], '', ucwords($type));
- if (!in_array($className, $types)) {
- return null;
- }
-
- return app('BookStack\\Entities\\' . $className);
+ $className = array_slice(explode('\\', static::class), -1, 1)[0];
+ return strtolower($className);
}
/**
/**
* Get the body text of this entity.
- * @return mixed
*/
- public function getText()
+ public function getText(): string
{
- return $this->{$this->textField};
+ return $this->{$this->textField} ?? '';
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
- * @param int $length
- * @return mixed
*/
- public function getExcerpt(int $length = 100)
+ public function getExcerpt(int $length = 100): string
{
$text = $this->getText();
+
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
+
return trim($text);
}
/**
* Get the url of this entity
- * @param $path
- * @return string
*/
- public function getUrl($path = '/')
+ abstract public function getUrl(string $path = '/'): string;
+
+ /**
+ * Get the parent entity if existing.
+ * This is the "static" parent and does not include dynamic
+ * relations such as shelves to books.
+ */
+ public function getParent(): ?Entity
{
- return $path;
+ if ($this->isA('page')) {
+ return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
+ }
+ if ($this->isA('chapter')) {
+ return $this->book()->withTrashed()->first();
+ }
+ return null;
}
/**
*/
public function indexForSearch()
{
- $searchService = app()->make(SearchService::class);
- $searchService->indexEntity(clone $this);
+ app(SearchIndex::class)->indexEntity(clone $this);
}
/**
*/
public function refreshSlug(): string
{
- $generator = new SlugGenerator($this);
- $this->slug = $generator->generate();
+ $this->slug = (new SlugGenerator)->generate($this);
return $this->slug;
}
}
<?php
-namespace BookStack\Entities;
+namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
+use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\Attachment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
public $textField = 'text';
- protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
+ protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
+
+ protected $casts = [
+ 'draft' => 'boolean',
+ 'template' => 'boolean',
+ ];
/**
* Get the entities that are visible to the current user.
*/
- public function scopeVisible(Builder $query)
+ public function scopeVisible(Builder $query): Builder
{
$query = Permissions::enforceDraftVisiblityOnQuery($query);
return parent::scopeVisible($query);
return $array;
}
- /**
- * Get the parent item
- */
- public function parent(): Entity
- {
- return $this->chapter_id ? $this->chapter : $this->book;
- }
-
/**
* Get the chapter that this page is in, If applicable.
* @return BelongsTo
}
/**
- * Get the url for this page.
- * @param string|bool $path
- * @return string
+ * Get the url of this page.
*/
- public function getUrl($path = false)
+ public function getUrl($path = ''): string
{
- $bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
- $midText = $this->draft ? '/draft/' : '/page/';
- $idComponent = $this->draft ? $this->id : urlencode($this->slug);
-
- $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
- if ($path !== false) {
- $url .= '/' . trim($path, '/');
- }
-
- return url($url);
+ $parts = [
+ 'books',
+ urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
+ $this->draft ? 'draft' : 'page',
+ $this->draft ? $this->id : urlencode($this->slug),
+ trim($path, '/'),
+ ];
+
+ return url('/' . implode('/', $parts));
}
/**
{
return $this->revisions()->first();
}
+
+ /**
+ * Get this page for JSON display.
+ */
+ public function forJsonDisplay(): Page
+ {
+ $refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy']);
+ $refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
+ $refreshed->html = (new PageContent($refreshed))->render();
+ return $refreshed;
+ }
}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
use BookStack\Model;
use Carbon\Carbon;
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Models;
use BookStack\Model;
namespace BookStack\Entities\Repos;
+use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\Entity;
-use BookStack\Entities\HasCoverImage;
+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;
protected $imageRepo;
- /**
- * BaseRepo constructor.
- * @param $tagRepo
- */
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
{
$this->tagRepo = $tagRepo;
$entity->forceFill([
'created_by' => user()->id,
'updated_by' => user()->id,
+ 'owned_by' => user()->id,
]);
$entity->refreshSlug();
$entity->save();
$entity->save();
}
}
-
- /**
- * Update the permissions of an entity.
- */
- public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
- {
- $entity->restricted = $restricted;
- $entity->permissions()->delete();
-
- if (!is_null($permissions)) {
- $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
- return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
- return [
- 'role_id' => $roleId,
- 'action' => strtolower($action),
- ] ;
- });
- });
-
- $entity->permissions()->createMany($entityPermissionData);
- }
-
- $entity->save();
- $entity->rebuildPermissions();
- }
}
<?php namespace BookStack\Entities\Repos;
+use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
use BookStack\Uploads\ImageRepo;
use Exception;
-use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
/**
* BookRepo constructor.
- * @param $tagRepo
*/
public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
{
{
$book = new Book();
$this->baseRepo->create($book, $input);
+ Activity::addForEntity($book, ActivityType::BOOK_CREATE);
return $book;
}
public function update(Book $book, array $input): Book
{
$this->baseRepo->update($book, $input);
+ Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
return $book;
}
$this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
}
- /**
- * Update the permissions of a book.
- */
- public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($book, $restricted, $permissions);
- }
-
/**
* Remove a book from the system.
- * @throws NotifyException
- * @throws BindingResolutionException
+ * @throws Exception
*/
public function destroy(Book $book)
{
$trashCan = new TrashCan();
- $trashCan->destroyBook($book);
+ $trashCan->softDestroyBook($book);
+ Activity::addForEntity($book, ActivityType::BOOK_DELETE);
+
+ $trashCan->autoClearOld();
}
}
<?php namespace BookStack\Entities\Repos;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
use Exception;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
/**
* BookshelfRepo constructor.
- * @param $baseRepo
*/
public function __construct(BaseRepo $baseRepo)
{
$shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input);
$this->updateBooks($shelf, $bookIds);
+ Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
return $shelf;
}
/**
- * Create a new shelf in the system.
+ * Update an existing shelf in the system using the given input.
*/
public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf
{
$this->updateBooks($shelf, $bookIds);
}
+ Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
return $shelf;
}
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
}
- /**
- * Update the permissions of a bookshelf.
- */
- public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
- }
-
/**
* Copy down the permissions of the given shelf to all child books.
*/
public function destroy(Bookshelf $shelf)
{
$trashCan = new TrashCan();
- $trashCan->destroyShelf($shelf);
+ $trashCan->softDestroyShelf($shelf);
+ Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
+ $trashCan->autoClearOld();
}
}
<?php namespace BookStack\Entities\Repos;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\TrashCan;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
+use BookStack\Facades\Activity;
use Exception;
-use Illuminate\Contracts\Container\BindingResolutionException;
-use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class ChapterRepo
/**
* ChapterRepo constructor.
- * @param $baseRepo
*/
public function __construct(BaseRepo $baseRepo)
{
$chapter->book_id = $parentBook->id;
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
+ Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
return $chapter;
}
public function update(Chapter $chapter, array $input): Chapter
{
$this->baseRepo->update($chapter, $input);
+ Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
return $chapter;
}
- /**
- * Update the permissions of a chapter.
- */
- public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
- }
-
/**
* Remove a chapter from the system.
* @throws Exception
public function destroy(Chapter $chapter)
{
$trashCan = new TrashCan();
- $trashCan->destroyChapter($chapter);
+ $trashCan->softDestroyChapter($chapter);
+ Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
+ $trashCan->autoClearOld();
}
/**
throw new MoveOperationException('Chapters can only be moved into books');
}
+ /** @var Book $parent */
$parent = Book::visible()->where('id', '=', $entityId)->first();
if ($parent === null) {
throw new MoveOperationException('Book to move chapter into not found');
$chapter->changeBook($parent->id);
$chapter->rebuildPermissions();
+ Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
+
return $parent;
}
}
<?php namespace BookStack\Entities\Repos;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Managers\TrashCan;
-use BookStack\Entities\Page;
-use BookStack\Entities\PageRevision;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+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\NotifyException;
use BookStack\Exceptions\PermissionsException;
+use BookStack\Facades\Activity;
+use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
* Get a page by ID.
* @throws NotFoundException
*/
- public function getById(int $id): Page
+ public function getById(int $id, array $relations = ['book']): Page
{
- $page = Page::visible()->with(['book'])->find($id);
+ $page = Page::visible()->with($relations)->find($id);
if (!$page) {
throw new NotFoundException(trans('errors.page_not_found'));
$page = (new Page())->forceFill([
'name' => trans('entities.pages_initial_name'),
'created_by' => user()->id,
+ 'owned_by' => user()->id,
'updated_by' => user()->id,
'draft' => true,
]);
public function publishDraft(Page $draft, array $input): Page
{
$this->baseRepo->update($draft, $input);
- if (isset($input['template']) && userCan('templates-manage')) {
- $draft->template = ($input['template'] === 'true');
- }
+ $this->updateTemplateStatusAndContentFromInput($draft, $input);
- $pageContent = new PageContent($draft);
- $pageContent->setNewHTML($input['html']);
$draft->draft = false;
$draft->revision_count = 1;
$draft->priority = $this->getNewPriority($draft);
$this->savePageRevision($draft, trans('entities.pages_initial_revision'));
$draft->indexForSearch();
- return $draft->refresh();
+ $draft->refresh();
+
+ Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
+ return $draft;
}
/**
$oldHtml = $page->html;
$oldName = $page->name;
- if (isset($input['template']) && userCan('templates-manage')) {
- $page->template = ($input['template'] === 'true');
- }
-
- $pageContent = new PageContent($page);
- $pageContent->setNewHTML($input['html']);
+ $this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
// Update with new details
$this->savePageRevision($page, $summary);
}
+ Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
return $page;
}
+ protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
+ {
+ if (isset($input['template']) && userCan('templates-manage')) {
+ $page->template = ($input['template'] === 'true');
+ }
+
+ $pageContent = new PageContent($page);
+ if (isset($input['html'])) {
+ $pageContent->setNewHTML($input['html']);
+ } else {
+ $pageContent->setNewMarkdown($input['markdown']);
+ }
+ }
+
/**
* Saves a page revision into the system.
*/
{
// If the page itself is a draft simply update that
if ($page->draft) {
- $page->fill($input);
if (isset($input['html'])) {
- $content = new PageContent($page);
- $content->setNewHTML($input['html']);
+ (new PageContent($page))->setNewHTML($input['html']);
}
+ $page->fill($input);
$page->save();
return $page;
}
/**
* Destroy a page from the system.
- * @throws NotifyException
+ * @throws Exception
*/
public function destroy(Page $page)
{
$trashCan = new TrashCan();
- $trashCan->destroyPage($page);
+ $trashCan->softDestroyPage($page);
+ Activity::addForEntity($page, ActivityType::PAGE_DELETE);
+ $trashCan->autoClearOld();
}
/**
$page->save();
$page->indexForSearch();
+ Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
return $page;
}
* @throws MoveOperationException
* @throws PermissionsException
*/
- public function move(Page $page, string $parentIdentifier): Book
+ public function move(Page $page, string $parentIdentifier): Entity
{
$parent = $this->findParentByIdentifier($parentIdentifier);
if ($parent === null) {
$page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
$page->rebuildPermissions();
- return ($parent instanceof Book ? $parent : $parent->book);
+ Activity::addForEntity($page, ActivityType::PAGE_MOVE);
+ return $parent;
}
/**
*/
public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
{
- $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+ $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
if ($parent === null) {
throw new MoveOperationException('Book or chapter to move page into not found');
}
return $parentClass::visible()->where('id', '=', $entityId)->first();
}
- /**
- * Update the permissions of a page.
- */
- public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($page, $restricted, $permissions);
- }
-
/**
* Change the page's parent to the given entity.
*/
*/
protected function getNewPriority(Page $page): int
{
- if ($page->parent() instanceof Chapter) {
- $lastPage = $page->parent()->pages('desc')->first();
+ $parent = $page->getParent();
+ if ($parent instanceof Chapter) {
+ $lastPage = $parent->pages('desc')->first();
return $lastPage ? $lastPage->priority + 1 : 0;
}
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
-use BookStack\Entities\Book;
-use BookStack\Entities\BookChild;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
use BookStack\Exceptions\SortOperationException;
use Illuminate\Support\Collection;
/**
* BookContents constructor.
- * @param $book
*/
public function __construct(Book $book)
{
$pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
$chapter = $chapterMap->get($chapter_id);
if ($chapter) {
- $chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
+ $chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
} else {
$lonePages = $lonePages->concat($pages);
}
});
+ $chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
+ $chapter->setAttribute('visible_pages', collect([]));
+ });
+
$all->each(function (Entity $entity) use ($renderPages) {
$entity->setRelation('book', $this->book);
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Uploads\ImageService;
use DomPDF;
use Exception;
use SnappyPDF;
use Throwable;
-class ExportService
+class ExportFormatter
{
protected $imageService;
protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
- preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
+ preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
- foreach ($chapter->pages as $page) {
+ foreach ($chapter->getVisiblePages() as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
*/
public function bookToPlainText(Book $book): string
{
- $bookTree = (new BookContents($book))->getTree(false, true);
+ $bookTree = (new BookContents($book))->getTree(false, false);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
+use League\CommonMark\CommonMarkConverter;
class PageContent
{
{
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
+ $this->page->markdown = '';
+ }
+
+ /**
+ * Update the content of the page with new provided Markdown content.
+ */
+ public function setNewMarkdown(string $markdown)
+ {
+ $this->page->markdown = $markdown;
+ $html = $this->markdownToHtml($markdown);
+ $this->page->html = $this->formatHtml($html);
+ $this->page->text = $this->toPlainText();
+ }
+
+ /**
+ * Convert the given Markdown content to a HTML string.
+ */
+ protected function markdownToHtml(string $markdown): string
+ {
+ $converter = new CommonMarkConverter();
+ return $converter->convertToHtml($markdown);
}
/**
$scriptElem->parentNode->removeChild($scriptElem);
}
+ // Remove clickable links to JavaScript URI
+ $badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
+ foreach ($badLinks as $badLink) {
+ $badLink->parentNode->removeChild($badLink);
+ }
+
+ // Remove forms with calls to JavaScript URI
+ $badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
+ foreach ($badForms as $badForm) {
+ $badForm->parentNode->removeChild($badForm);
+ }
+
+ // Remove meta tag to prevent external redirects
+ $metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
+ foreach ($metaTags as $metaTag) {
+ $metaTag->parentNode->removeChild($metaTag);
+ }
+
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
foreach ($badIframes as $badIframe) {
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
-use BookStack\Entities\Page;
-use BookStack\Entities\PageRevision;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Models\PageRevision;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
--- /dev/null
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Entity;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+
+class PermissionsUpdater
+{
+
+ /**
+ * Update an entities permissions from a permission form submit request.
+ */
+ public function updateFromPermissionsForm(Entity $entity, Request $request)
+ {
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->get('restrictions', null);
+ $ownerId = $request->get('owned_by', null);
+
+ $entity->restricted = $restricted;
+ $entity->permissions()->delete();
+
+ if (!is_null($permissions)) {
+ $entityPermissionData = $this->formatPermissionsFromRequestToEntityPermissions($permissions);
+ $entity->permissions()->createMany($entityPermissionData);
+ }
+
+ if (!is_null($ownerId)) {
+ $this->updateOwnerFromId($entity, intval($ownerId));
+ }
+
+ $entity->save();
+ $entity->rebuildPermissions();
+
+ Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
+ }
+
+ /**
+ * Update the owner of the given entity.
+ * Checks the user exists in the system first.
+ * Does not save the model, just updates it.
+ */
+ protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
+ {
+ $newOwner = User::query()->find($newOwnerId);
+ if (!is_null($newOwner)) {
+ $entity->owned_by = $newOwner->id;
+ }
+ }
+
+ /**
+ * Format permissions provided from a permission form to be
+ * EntityPermission data.
+ */
+ protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): Collection
+ {
+ return collect($permissions)->flatMap(function ($restrictions, $roleId) {
+ return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+ return [
+ 'role_id' => $roleId,
+ 'action' => strtolower($action),
+ ] ;
+ });
+ });
+ }
+}
--- /dev/null
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\SearchTerm;
+use Illuminate\Support\Collection;
+
+class SearchIndex
+{
+ /**
+ * @var SearchTerm
+ */
+ protected $searchTerm;
+
+ /**
+ * @var EntityProvider
+ */
+ protected $entityProvider;
+
+
+ public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider)
+ {
+ $this->searchTerm = $searchTerm;
+ $this->entityProvider = $entityProvider;
+ }
+
+
+ /**
+ * Index the given entity.
+ */
+ public function indexEntity(Entity $entity)
+ {
+ $this->deleteEntityTerms($entity);
+ $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
+ $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
+ $terms = array_merge($nameTerms, $bodyTerms);
+ foreach ($terms as $index => $term) {
+ $terms[$index]['entity_type'] = $entity->getMorphClass();
+ $terms[$index]['entity_id'] = $entity->id;
+ }
+ $this->searchTerm->newQuery()->insert($terms);
+ }
+
+ /**
+ * Index multiple Entities at once
+ * @param Entity[] $entities
+ */
+ protected function indexEntities(array $entities)
+ {
+ $terms = [];
+ foreach ($entities as $entity) {
+ $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
+ $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
+ foreach (array_merge($nameTerms, $bodyTerms) as $term) {
+ $term['entity_id'] = $entity->id;
+ $term['entity_type'] = $entity->getMorphClass();
+ $terms[] = $term;
+ }
+ }
+
+ $chunkedTerms = array_chunk($terms, 500);
+ foreach ($chunkedTerms as $termChunk) {
+ $this->searchTerm->newQuery()->insert($termChunk);
+ }
+ }
+
+ /**
+ * Delete and re-index the terms for all entities in the system.
+ */
+ public function indexAllEntities()
+ {
+ $this->searchTerm->newQuery()->truncate();
+
+ foreach ($this->entityProvider->all() as $entityModel) {
+ $selectFields = ['id', 'name', $entityModel->textField];
+ $entityModel->newQuery()
+ ->withTrashed()
+ ->select($selectFields)
+ ->chunk(1000, function (Collection $entities) {
+ $this->indexEntities($entities->all());
+ });
+ }
+ }
+
+ /**
+ * Delete related Entity search terms.
+ */
+ public function deleteEntityTerms(Entity $entity)
+ {
+ $entity->searchTerms()->delete();
+ }
+
+ /**
+ * Create a scored term array from the given text.
+ */
+ protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
+ {
+ $tokenMap = []; // {TextToken => OccurrenceCount}
+ $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
+ $token = strtok($text, $splitChars);
+
+ while ($token !== false) {
+ if (!isset($tokenMap[$token])) {
+ $tokenMap[$token] = 0;
+ }
+ $tokenMap[$token]++;
+ $token = strtok($splitChars);
+ }
+
+ $terms = [];
+ foreach ($tokenMap as $token => $count) {
+ $terms[] = [
+ 'term' => $token,
+ 'score' => $count * $scoreAdjustment
+ ];
+ }
+
+ return $terms;
+ }
+}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
use Illuminate\Http\Request;
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
-class SearchService
+class SearchRunner
{
- /**
- * @var SearchTerm
- */
- protected $searchTerm;
/**
* @var EntityProvider
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
- /**
- * SearchService constructor.
- */
- public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
+
+ public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
{
- $this->searchTerm = $searchTerm;
$this->entityProvider = $entityProvider;
$this->db = $db;
$this->permissionService = $permissionService;
}
- /**
- * Set the database connection
- */
- public function setConnection(Connection $connection)
- {
- $this->db = $connection;
- }
-
/**
* Search all entities in the system.
* The provided count is for each entity to search,
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
+
return $results->sortByDesc('score')->take(20);
}
/**
- * Search a book for entities
+ * Search a chapter for entities
*/
public function searchChapter(int $chapterId, string $searchString): Collection
{
* matching instead of the items themselves.
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
- public function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
+ protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
{
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
if ($getCount) {
// Handle normal search terms
if (count($searchOpts->searches) > 0) {
- $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
+ $rawScoreSum = $this->db->raw('SUM(score) as score');
+ $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum);
$subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($searchOpts) {
foreach ($searchOpts->searches as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
- $entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
+ $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');
$entitySelect->mergeBindings($subQuery);
}
// Handle exact term matching
- if (count($searchOpts->exacts) > 0) {
- $entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) {
- foreach ($searchOpts->exacts as $inputTerm) {
- $query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
- $query->where('name', 'like', '%'.$inputTerm .'%')
- ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
- });
- }
+ foreach ($searchOpts->exacts as $inputTerm) {
+ $entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
+ $query->where('name', 'like', '%'.$inputTerm .'%')
+ ->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
});
}
return $query;
}
- /**
- * Index the given entity.
- */
- public function indexEntity(Entity $entity)
- {
- $this->deleteEntityTerms($entity);
- $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
- $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
- $terms = array_merge($nameTerms, $bodyTerms);
- foreach ($terms as $index => $term) {
- $terms[$index]['entity_type'] = $entity->getMorphClass();
- $terms[$index]['entity_id'] = $entity->id;
- }
- $this->searchTerm->newQuery()->insert($terms);
- }
-
- /**
- * Index multiple Entities at once
- * @param \BookStack\Entities\Entity[] $entities
- */
- protected function indexEntities($entities)
- {
- $terms = [];
- foreach ($entities as $entity) {
- $nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
- $bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
- foreach (array_merge($nameTerms, $bodyTerms) as $term) {
- $term['entity_id'] = $entity->id;
- $term['entity_type'] = $entity->getMorphClass();
- $terms[] = $term;
- }
- }
-
- $chunkedTerms = array_chunk($terms, 500);
- foreach ($chunkedTerms as $termChunk) {
- $this->searchTerm->newQuery()->insert($termChunk);
- }
- }
-
- /**
- * Delete and re-index the terms for all entities in the system.
- */
- public function indexAllEntities()
- {
- $this->searchTerm->truncate();
-
- foreach ($this->entityProvider->all() as $entityModel) {
- $selectFields = ['id', 'name', $entityModel->textField];
- $entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
- $this->indexEntities($entities);
- });
- }
- }
-
- /**
- * Delete related Entity search terms.
- * @param Entity $entity
- */
- public function deleteEntityTerms(Entity $entity)
- {
- $entity->searchTerms()->delete();
- }
-
- /**
- * Create a scored term array from the given text.
- * @param $text
- * @param float|int $scoreAdjustment
- * @return array
- */
- protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
- {
- $tokenMap = []; // {TextToken => OccurrenceCount}
- $splitChars = " \n\t.,!?:;()[]{}<>`'\"";
- $token = strtok($text, $splitChars);
-
- while ($token !== false) {
- if (!isset($tokenMap[$token])) {
- $tokenMap[$token] = 0;
- }
- $tokenMap[$token]++;
- $token = strtok($splitChars);
- }
-
- $terms = [];
- foreach ($tokenMap as $token => $count) {
- $terms[] = [
- 'term' => $token,
- 'score' => $count * $scoreAdjustment
- ];
- }
- return $terms;
- }
-
-
-
-
/**
* Custom entity search filters
*/
-<?php namespace BookStack\Entities\Managers;
+<?php namespace BookStack\Entities\Tools;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use Illuminate\Session\Store;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
-class EntityContext
+class ShelfContext
{
- protected $session;
-
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
- /**
- * EntityContextManager constructor.
- */
- public function __construct(Store $session)
- {
- $this->session = $session;
- }
-
/**
* Get the current bookshelf context for the given book.
*/
public function getContextualShelfForBook(Book $book): ?Bookshelf
{
- $contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
+ $contextBookshelfId = session()->get($this->KEY_SHELF_CONTEXT_ID, null);
if (!is_int($contextBookshelfId)) {
return null;
/**
* Store the current contextual shelf ID.
- * @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
- $this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
+ session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
*/
public function clearShelfContext()
{
- $this->session->forget($this->KEY_SHELF_CONTEXT_ID);
+ session()->forget($this->KEY_SHELF_CONTEXT_ID);
}
}
--- /dev/null
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use Illuminate\Support\Collection;
+
+class SiblingFetcher
+{
+
+ /**
+ * Search among the siblings of the entity of given type and id.
+ */
+ public function fetch(string $entityType, int $entityId): Collection
+ {
+ $entity = (new EntityProvider)->get($entityType)->visible()->findOrFail($entityId);
+ $entities = [];
+
+ // Page in chapter
+ if ($entity->isA('page') && $entity->chapter) {
+ $entities = $entity->chapter->getVisiblePages();
+ }
+
+ // Page in book or chapter
+ if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
+ $entities = $entity->book->getDirectChildren();
+ }
+
+ // Book
+ // Gets just the books in a shelf if shelf is in context
+ if ($entity->isA('book')) {
+ $contextShelf = (new ShelfContext)->getContextualShelfForBook($entity);
+ if ($contextShelf) {
+ $entities = $contextShelf->visibleBooks()->get();
+ } else {
+ $entities = Book::visible()->get();
+ }
+ }
+
+ // Shelve
+ if ($entity->isA('bookshelf')) {
+ $entities = Bookshelf::visible()->get();
+ }
+
+ return $entities;
+ }
+}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Tools;
+use BookStack\Entities\Models\Entity;
use Illuminate\Support\Str;
class SlugGenerator
{
- protected $entity;
-
- /**
- * SlugGenerator constructor.
- * @param $entity
- */
- public function __construct(Entity $entity)
- {
- $this->entity = $entity;
- }
-
/**
* Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item.
*/
- public function generate(): string
+ public function generate(Entity $entity): string
{
- $slug = $this->formatNameAsSlug($this->entity->name);
- while ($this->slugInUse($slug)) {
+ $slug = $this->formatNameAsSlug($entity->name);
+ while ($this->slugInUse($slug, $entity)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
* Check if a slug is already in-use for this
* type of model within the same parent.
*/
- protected function slugInUse(string $slug): bool
+ protected function slugInUse(string $slug, Entity $entity): bool
{
- $query = $this->entity->newQuery()->where('slug', '=', $slug);
+ $query = $entity->newQuery()->where('slug', '=', $slug);
- if ($this->entity instanceof BookChild) {
- $query->where('book_id', '=', $this->entity->book_id);
+ if ($entity instanceof BookChild) {
+ $query->where('book_id', '=', $entity->book_id);
}
- if ($this->entity->id) {
- $query->where('id', '!=', $this->entity->id);
+ if ($entity->id) {
+ $query->where('id', '!=', $entity->id);
}
return $query->count() > 0;
--- /dev/null
+<?php namespace BookStack\Entities\Tools;
+
+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;
+use BookStack\Facades\Activity;
+use BookStack\Uploads\AttachmentService;
+use BookStack\Uploads\ImageService;
+use Exception;
+use Illuminate\Support\Carbon;
+
+class TrashCan
+{
+
+ /**
+ * Send a shelf to the recycle bin.
+ */
+ public function softDestroyShelf(Bookshelf $shelf)
+ {
+ Deletion::createForEntity($shelf);
+ $shelf->delete();
+ }
+
+ /**
+ * Send a book to the recycle bin.
+ * @throws Exception
+ */
+ public function softDestroyBook(Book $book)
+ {
+ Deletion::createForEntity($book);
+
+ foreach ($book->pages as $page) {
+ $this->softDestroyPage($page, false);
+ }
+
+ foreach ($book->chapters as $chapter) {
+ $this->softDestroyChapter($chapter, false);
+ }
+
+ $book->delete();
+ }
+
+ /**
+ * Send a chapter to the recycle bin.
+ * @throws Exception
+ */
+ public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
+ {
+ if ($recordDelete) {
+ Deletion::createForEntity($chapter);
+ }
+
+ if (count($chapter->pages) > 0) {
+ foreach ($chapter->pages as $page) {
+ $this->softDestroyPage($page, false);
+ }
+ }
+
+ $chapter->delete();
+ }
+
+ /**
+ * Send a page to the recycle bin.
+ * @throws Exception
+ */
+ public function softDestroyPage(Page $page, bool $recordDelete = true)
+ {
+ if ($recordDelete) {
+ Deletion::createForEntity($page);
+ }
+
+ // Check if set as custom homepage & remove setting if not used or throw error if active
+ $customHome = setting('app-homepage', '0:');
+ if (intval($page->id) === intval(explode(':', $customHome)[0])) {
+ if (setting('app-homepage-type') === 'page') {
+ throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+ }
+ setting()->remove('app-homepage');
+ }
+
+ $page->delete();
+ }
+
+ /**
+ * 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
+ {
+ $count = 0;
+ $pages = $book->pages()->withTrashed()->get();
+ foreach ($pages as $page) {
+ $this->destroyPage($page);
+ $count++;
+ }
+
+ $chapters = $book->chapters()->withTrashed()->get();
+ foreach ($chapters as $chapter) {
+ $this->destroyChapter($chapter);
+ $count++;
+ }
+
+ $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
+ {
+ $count = 0;
+ $pages = $chapter->pages()->withTrashed()->get();
+ if (count($pages)) {
+ foreach ($pages as $page) {
+ $this->destroyPage($page);
+ $count++;
+ }
+ }
+
+ $this->destroyCommonRelations($chapter);
+ $chapter->forceDelete();
+ return $count + 1;
+ }
+
+ /**
+ * Remove a page from the system.
+ * @throws Exception
+ */
+ protected function destroyPage(Page $page): int
+ {
+ $this->destroyCommonRelations($page);
+
+ // Delete Attached Files
+ $attachmentService = app(AttachmentService::class);
+ foreach ($page->attachments as $attachment) {
+ $attachmentService->deleteFile($attachment);
+ }
+
+ $page->forceDelete();
+ return 1;
+ }
+
+ /**
+ * Get the total counts of those that have been trashed
+ * but not yet fully deleted (In recycle bin).
+ */
+ public function getTrashedCounts(): array
+ {
+ $counts = [];
+
+ /** @var Entity $instance */
+ foreach ((new EntityProvider)->all() as $key => $instance) {
+ $counts[$key] = $instance->newQuery()->onlyTrashed()->count();
+ }
+
+ return $counts;
+ }
+
+ /**
+ * Destroy all items that have pending deletions.
+ * @throws Exception
+ */
+ public function empty(): int
+ {
+ $deletions = Deletion::all();
+ $deleteCount = 0;
+ 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
+ {
+ // We directly load the deletable element here just to ensure it still
+ // exists in the event it has already been destroyed during this request.
+ $entity = $deletion->deletable()->first();
+ $count = 0;
+ if ($entity) {
+ $count = $this->destroyEntity($deletion->deletable);
+ }
+ $deletion->delete();
+ return $count;
+ }
+
+ /**
+ * Restore the content within the given deletion.
+ * @throws Exception
+ */
+ public function restoreFromDeletion(Deletion $deletion): int
+ {
+ $shouldRestore = true;
+ $restoreCount = 0;
+ $parent = $deletion->deletable->getParent();
+
+ if ($parent && $parent->trashed()) {
+ $shouldRestore = false;
+ }
+
+ if ($shouldRestore) {
+ $restoreCount = $this->restoreEntity($deletion->deletable);
+ }
+
+ $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
+ {
+ $lifetime = intval(config('app.recycle_bin_lifetime'));
+ if ($lifetime < 0) {
+ return 0;
+ }
+
+ $clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
+ $deleteCount = 0;
+
+ $deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
+ foreach ($deletionsToRemove as $deletion) {
+ $deleteCount += $this->destroyFromDeletion($deletion);
+ }
+
+ return $deleteCount;
+ }
+
+ /**
+ * Restore an entity so it is essentially un-deleted.
+ * Deletions on restored child elements will be removed during this restoration.
+ */
+ protected function restoreEntity(Entity $entity): int
+ {
+ $count = 1;
+ $entity->restore();
+
+ $restoreAction = function ($entity) use (&$count) {
+ if ($entity->deletions_count > 0) {
+ $entity->deletions()->delete();
+ }
+
+ $entity->restore();
+ $count++;
+ };
+
+ if ($entity->isA('chapter') || $entity->isA('book')) {
+ $entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
+ }
+
+ if ($entity->isA('book')) {
+ $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
+ }
+
+ return $count;
+ }
+
+ /**
+ * Destroy the given entity.
+ */
+ protected function destroyEntity(Entity $entity): int
+ {
+ if ($entity->isA('page')) {
+ return $this->destroyPage($entity);
+ }
+ if ($entity->isA('chapter')) {
+ return $this->destroyChapter($entity);
+ }
+ if ($entity->isA('book')) {
+ return $this->destroyBook($entity);
+ }
+ if ($entity->isA('shelf')) {
+ return $this->destroyShelf($entity);
+ }
+ }
+
+ /**
+ * Update entity relations to remove or update outstanding connections.
+ */
+ protected function destroyCommonRelations(Entity $entity)
+ {
+ Activity::removeEntity($entity);
+ $entity->views()->delete();
+ $entity->permissions()->delete();
+ $entity->tags()->delete();
+ $entity->comments()->delete();
+ $entity->jointPermissions()->delete();
+ $entity->searchTerms()->delete();
+ $entity->deletions()->delete();
+
+ if ($entity instanceof HasCoverImage && $entity->cover) {
+ $imageService = app()->make(ImageService::class);
+ $imageService->destroy($entity->cover);
+ }
+ }
+}
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
-class ApiController extends Controller
+abstract class ApiController extends Controller
{
protected $rules = [];
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Api\ApiDocsGenerator;
-use Cache;
-use Illuminate\Support\Collection;
class ApiDocsController extends ApiController
{
*/
public function display()
{
- $docs = $this->getDocs();
+ $docs = ApiDocsGenerator::generateConsideringCache();
+ $this->setPageTitle(trans('settings.users_api_tokens_docs'));
return view('api-docs.index', [
'docs' => $docs,
]);
/**
* Show a JSON view of the API docs data.
*/
- public function json() {
- $docs = $this->getDocs();
- return response()->json($docs);
- }
-
- /**
- * Get the base docs data.
- * Checks and uses the system cache for quick re-fetching.
- */
- protected function getDocs(): Collection
+ public function json()
{
- $appVersion = trim(file_get_contents(base_path('version')));
- $cacheKey = 'api-docs::' . $appVersion;
- if (Cache::has($cacheKey) && config('app.env') === 'production') {
- $docs = Cache::get($cacheKey);
- } else {
- $docs = (new ApiDocsGenerator())->generate();
- Cache::put($cacheKey, $docs, 60*24);
- }
-
- return $docs;
+ $docs = ApiDocsGenerator::generateConsideringCache();
+ return response()->json($docs);
}
}
<?php namespace BookStack\Http\Controllers\Api;
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\NotifyException;
-use BookStack\Facades\Activity;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
],
];
- /**
- * BooksApiController constructor.
- */
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
$requestData = $this->validate($request, $this->rules['create']);
$book = $this->bookRepo->create($requestData);
- Activity::add($book, 'book_create', $book->id);
-
return response()->json($book);
}
$requestData = $this->validate($request, $this->rules['update']);
$book = $this->bookRepo->update($book, $requestData);
- Activity::add($book, 'book_update', $book->id);
return response()->json($book);
}
/**
- * Delete a single book from the system.
- * @throws NotifyException
- * @throws BindingResolutionException
+ * 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);
- Activity::addMessage('book_delete', $book->name);
-
return response('', 204);
}
}
\ No newline at end of file
<?php namespace BookStack\Http\Controllers\Api;
-use BookStack\Entities\Book;
-use BookStack\Entities\ExportService;
-use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\ExportFormatter;
use Throwable;
class BookExportApiController extends ApiController
{
- protected $bookRepo;
- protected $exportService;
+ protected $exportFormatter;
- /**
- * BookExportController constructor.
- */
- public function __construct(BookRepo $bookRepo, ExportService $exportService)
+ public function __construct(ExportFormatter $exportFormatter)
{
- $this->bookRepo = $bookRepo;
- $this->exportService = $exportService;
- parent::__construct();
+ $this->exportFormatter = $exportFormatter;
}
/**
public function exportPdf(int $id)
{
$book = Book::visible()->findOrFail($id);
- $pdfContent = $this->exportService->bookToPdf($book);
+ $pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
}
public function exportHtml(int $id)
{
$book = Book::visible()->findOrFail($id);
- $htmlContent = $this->exportService->bookToContainedHtml($book);
+ $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->downloadResponse($htmlContent, $book->slug . '.html');
}
public function exportPlainText(int $id)
{
$book = Book::visible()->findOrFail($id);
- $textContent = $this->exportService->bookToPlainText($book);
+ $textContent = $this->exportFormatter->bookToPlainText($book);
return $this->downloadResponse($textContent, $book->slug . '.txt');
}
}
<?php namespace BookStack\Http\Controllers\Api;
-use BookStack\Facades\Activity;
use BookStack\Entities\Repos\BookshelfRepo;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
/**
* BookshelfApiController constructor.
- * @param BookshelfRepo $bookshelfRepo
*/
public function __construct(BookshelfRepo $bookshelfRepo)
{
$bookIds = $request->get('books', []);
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
- Activity::add($shelf, 'bookshelf_create', $shelf->id);
return response()->json($shelf);
}
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules['update']);
-
$bookIds = $request->get('books', null);
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
- Activity::add($shelf, 'bookshelf_update', $shelf->id);
-
return response()->json($shelf);
}
/**
- * Delete a single shelf from the system.
+ * 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);
- Activity::addMessage('bookshelf_delete', $shelf->name);
-
return response('', 204);
}
}
\ No newline at end of file
<?php namespace BookStack\Http\Controllers\Api;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+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;
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
- Activity::add($chapter, 'chapter_create', $book->id);
-
return response()->json($chapter->load(['tags']));
}
$this->checkOwnablePermission('chapter-update', $chapter);
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
- Activity::add($chapter, 'chapter_update', $chapter->book->id);
-
return response()->json($updatedChapter->load(['tags']));
}
/**
- * Delete a chapter from the system.
+ * Delete a chapter.
+ * This will typically send the chapter to the recycle bin.
*/
public function delete(string $id)
{
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
- Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
-
return response('', 204);
}
}
<?php namespace BookStack\Http\Controllers\Api;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
class ChapterExportApiController extends ApiController
{
- protected $chapterRepo;
- protected $exportService;
+ protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
- public function __construct(BookRepo $chapterRepo, ExportService $exportService)
+ public function __construct(ExportFormatter $exportFormatter)
{
- $this->chapterRepo = $chapterRepo;
- $this->exportService = $exportService;
- parent::__construct();
+ $this->exportFormatter = $exportFormatter;
}
/**
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
- $pdfContent = $this->exportService->chapterToPdf($chapter);
+ $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
}
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
- $htmlContent = $this->exportService->chapterToContainedHtml($chapter);
+ $htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
}
public function exportPlainText(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
- $textContent = $this->exportService->chapterToPlainText($chapter);
+ $textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
}
}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\PermissionsException;
+use Exception;
+use Illuminate\Http\Request;
+
+class PageApiController extends ApiController
+{
+ protected $pageRepo;
+
+ protected $rules = [
+ 'create' => [
+ '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',
+ ],
+ 'update' => [
+ 'book_id' => 'required|integer',
+ 'chapter_id' => 'required|integer',
+ 'name' => 'string|min:1|max:255',
+ 'html' => 'string',
+ 'markdown' => 'string',
+ 'tags' => 'array',
+ ],
+ ];
+
+ public function __construct(PageRepo $pageRepo)
+ {
+ $this->pageRepo = $pageRepo;
+ }
+
+ /**
+ * Get a listing of pages visible to the user.
+ */
+ public function list()
+ {
+ $pages = Page::visible();
+ return $this->apiListingResponse($pages, [
+ 'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
+ 'draft', 'template',
+ 'created_at', 'updated_at', 'created_by', 'updated_by',
+ ]);
+ }
+
+ /**
+ * Create a new page in the system.
+ *
+ * The ID of a parent book or chapter is required to indicate
+ * where this page should be located.
+ *
+ * Any HTML content provided should be kept to a single-block depth of plain HTML
+ * elements to remain compatible with the BookStack front-end and editors.
+ */
+ public function create(Request $request)
+ {
+ $this->validate($request, $this->rules['create']);
+
+ if ($request->has('chapter_id')) {
+ $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+ } else {
+ $parent = Book::visible()->findOrFail($request->get('book_id'));
+ }
+ $this->checkOwnablePermission('page-create', $parent);
+
+ $draft = $this->pageRepo->getNewDraftPage($parent);
+ $this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
+
+ return response()->json($draft->forJsonDisplay());
+ }
+
+ /**
+ * View the details of a single page.
+ *
+ * Pages will always have HTML content. They may have markdown content
+ * if the markdown editor was used to last update the page.
+ */
+ public function read(string $id)
+ {
+ $page = $this->pageRepo->getById($id, []);
+ return response()->json($page->forJsonDisplay());
+ }
+
+ /**
+ * Update the details of a single page.
+ *
+ * See the 'create' action for details on the provided HTML/Markdown.
+ * Providing a 'book_id' or 'chapter_id' property will essentially move
+ * the page into that parent element if you have permissions to do so.
+ */
+ public function update(Request $request, string $id)
+ {
+ $page = $this->pageRepo->getById($id, []);
+ $this->checkOwnablePermission('page-update', $page);
+
+ $parent = null;
+ if ($request->has('chapter_id')) {
+ $parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
+ } else if ($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) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
+
+ return $this->jsonError(trans('errors.selected_book_chapter_not_found'));
+ }
+ }
+
+ $updatedPage = $this->pageRepo->update($page, $request->all());
+ return response()->json($updatedPage->forJsonDisplay());
+ }
+
+ /**
+ * Delete a page.
+ * This will typically send the page to the recycle bin.
+ */
+ public function delete(string $id)
+ {
+ $page = $this->pageRepo->getById($id, []);
+ $this->checkOwnablePermission('page-delete', $page);
+
+ $this->pageRepo->destroy($page);
+ return response('', 204);
+ }
+}
--- /dev/null
+<?php namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\ExportFormatter;
+use Throwable;
+
+class PageExportApiController extends ApiController
+{
+ protected $exportFormatter;
+
+ public function __construct(ExportFormatter $exportFormatter)
+ {
+ $this->exportFormatter = $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');
+ }
+
+ /**
+ * Export a page as a plain text file.
+ */
+ public function exportPlainText(int $id)
+ {
+ $page = Page::visible()->findOrFail($id);
+ $textContent = $this->exportFormatter->pageToPlainText($page);
+ return $this->downloadResponse($textContent, $page->slug . '.txt');
+ }
+}
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo;
- parent::__construct();
}
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
- 'attachment_edit_url' => 'string|min:1|max:255'
+ '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']), [
$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'
+ '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']), [
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
- $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
+ $attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
return view('attachments.manager-link-form', [
'pageId' => $pageId,
];
$query = Activity::query()
- ->with(['entity', 'user'])
+ ->with([
+ 'entity' => function ($query) {
+ $query->withTrashed();
+ },
+ 'user'
+ ])
->orderBy($listDetails['sort'], $listDetails['order']);
if ($listDetails['event']) {
- $query->where('key', '=', $listDetails['event']);
+ $query->where('type', '=', $listDetails['event']);
}
if ($listDetails['date_from']) {
$activities = $query->paginate(100);
$activities->appends($listDetails);
- $keys = DB::table('activities')->select('key')->distinct()->pluck('key');
+ $types = DB::table('activities')->select('type')->distinct()->pluck('type');
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
'listDetails' => $listDetails,
- 'activityKeys' => $keys,
+ 'activityTypes' => $types,
]);
}
}
/**
* Create a new controller instance.
- *
- * @param EmailConfirmationService $emailConfirmationService
- * @param UserRepo $userRepo
*/
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
- parent::__construct();
}
namespace BookStack\Http\Controllers\Auth;
+use BookStack\Actions\ActivityType;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
{
$this->middleware('guest');
$this->middleware('guard:standard');
- parent::__construct();
}
$request->only('email')
);
+ if ($response === Password::RESET_LINK_SENT) {
+ $this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
+ }
+
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
$this->showSuccessNotification($message);
namespace BookStack\Http\Controllers\Auth;
use Activity;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
-use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
$this->socialAuthService = $socialAuthService;
$this->redirectPath = url('/');
$this->redirectAfterLogout = url('/login');
- parent::__construct();
}
public function username()
}
}
+ $this->logActivity(ActivityType::AUTH_LOGIN, $user);
return redirect()->intended($this->redirectPath());
}
$this->redirectTo = url('/');
$this->redirectPath = url('/');
- parent::__construct();
}
/**
namespace BookStack\Http\Controllers\Auth;
+use BookStack\Actions\ActivityType;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
{
$this->middleware('guest');
$this->middleware('guard:standard');
- parent::__construct();
}
/**
{
$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));
}
*/
public function __construct(Saml2Service $samlService)
{
- parent::__construct();
$this->samlService = $samlService;
$this->middleware('guard:saml2');
}
$this->inviteService = $inviteService;
$this->userRepo = $userRepo;
-
- parent::__construct();
}
/**
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Tools\BookContents;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Tools\PermissionsUpdater;
+use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\ImageUploadException;
-use BookStack\Exceptions\NotifyException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
protected $bookRepo;
protected $entityContextManager;
- /**
- * BookController constructor.
- */
- public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+ public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
$this->entityContextManager = $entityContextManager;
- parent::__construct();
}
/**
$book = $this->bookRepo->create($request->all());
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
- Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
$bookshelf->appendBook($book);
- Activity::add($bookshelf, 'bookshelf_update');
+ Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
}
return redirect($book->getUrl());
$resetCover = $request->has('image_reset');
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
- Activity::add($book, 'book_update', $book->id);
-
return redirect($book->getUrl());
}
/**
* Remove the specified book from the system.
* @throws Throwable
- * @throws NotifyException
*/
public function destroy(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- Activity::addMessage('book_delete', $book->name);
$this->bookRepo->destroy($book);
return redirect('/books');
* Set the restrictions for this book.
* @throws Throwable
*/
- public function permissions(Request $request, string $bookSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->bookRepo->updatePermissions($book, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($book, $request);
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
namespace BookStack\Http\Controllers;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
{
protected $bookRepo;
- protected $exportService;
+ protected $exportFormatter;
/**
* BookExportController constructor.
*/
- public function __construct(BookRepo $bookRepo, ExportService $exportService)
+ public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
{
$this->bookRepo = $bookRepo;
- $this->exportService = $exportService;
- parent::__construct();
+ $this->exportFormatter = $exportFormatter;
}
/**
public function pdf(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
- $pdfContent = $this->exportService->bookToPdf($book);
+ $pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
}
public function html(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
- $htmlContent = $this->exportService->bookToContainedHtml($book);
+ $htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
}
public function plainText(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
- $textContent = $this->exportService->bookToPlainText($book);
+ $textContent = $this->exportFormatter->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
}
namespace BookStack\Http\Controllers;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\BookContents;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\SortOperationException;
use BookStack\Facades\Activity;
protected $bookRepo;
- /**
- * BookSortController constructor.
- * @param $bookRepo
- */
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
- parent::__construct();
}
/**
// Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) {
- Activity::add($book, 'book_sort', $book->id);
+ Activity::addForEntity($book, ActivityType::BOOK_SORT);
});
return redirect($book->getUrl());
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\PermissionsUpdater;
+use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
protected $entityContextManager;
protected $imageRepo;
- /**
- * BookController constructor.
- */
- public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
+ public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
{
$this->bookshelfRepo = $bookshelfRepo;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
- Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
$resetCover = $request->has('image_reset');
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
- Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
- Activity::addMessage('bookshelf_delete', $shelf->name);
$this->bookshelfRepo->destroy($shelf);
return redirect('/shelves');
/**
* Set the permissions for this bookshelf.
*/
- public function permissions(Request $request, string $slug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
{
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($shelf, $request);
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());
<?php namespace BookStack\Http\Controllers;
-use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
public function __construct(ChapterRepo $chapterRepo)
{
$this->chapterRepo = $chapterRepo;
- parent::__construct();
}
/**
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
- Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl());
}
$this->checkOwnablePermission('chapter-update', $chapter);
$this->chapterRepo->update($chapter, $request->all());
- Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
- Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
$this->chapterRepo->destroy($chapter);
return redirect($chapter->book->getUrl());
return redirect()->back();
}
- Activity::add($chapter, 'chapter_move', $newBook->id);
-
$this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
return redirect($chapter->getUrl());
}
* Set the restrictions for this chapter.
* @throws NotFoundException
*/
- public function permissions(Request $request, string $bookSlug, string $chapterSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($chapter, $request);
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
<?php namespace BookStack\Http\Controllers;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\NotFoundException;
use Throwable;
{
protected $chapterRepo;
- protected $exportService;
+ protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
- public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
+ public function __construct(ChapterRepo $chapterRepo, ExportFormatter $exportFormatter)
{
$this->chapterRepo = $chapterRepo;
- $this->exportService = $exportService;
- parent::__construct();
+ $this->exportFormatter = $exportFormatter;
}
/**
public function pdf(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
- $pdfContent = $this->exportService->chapterToPdf($chapter);
+ $pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
}
public function html(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
- $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+ $containedHtml = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
}
public function plainText(string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
- $chapterText = $this->exportService->chapterToPlainText($chapter);
+ $chapterText = $this->exportFormatter->chapterToPlainText($chapter);
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
}
}
<?php namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Actions\ActivityType;
use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
- parent::__construct();
}
/**
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
- Activity::add($page, 'commented_on', $page->book->id);
return view('comments.comment', ['comment' => $comment]);
}
namespace BookStack\Http\Controllers;
-use BookStack\Ownable;
+use BookStack\Facades\Activity;
+use BookStack\Interfaces\Loggable;
+use BookStack\HasCreatorAndUpdater;
+use BookStack\Model;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException;
-use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Response;
use Illuminate\Routing\Controller as BaseController;
-use Illuminate\Validation\ValidationException;
abstract class Controller extends BaseController
{
use DispatchesJobs, ValidatesRequests;
- /**
- * Controller constructor.
- */
- public function __construct()
- {
- //
- }
-
/**
* Check if the current user is signed in.
*/
/**
* Adds the page title into the view.
- * @param $title
*/
- public function setPageTitle($title)
+ public function setPageTitle(string $title)
{
view()->share('pageTitle', $title);
}
}
/**
- * Checks for a permission.
- * @param string $permissionName
- * @return bool|\Illuminate\Http\RedirectResponse
+ * Checks that the current user has the given permission otherwise throw an exception.
*/
- protected function checkPermission($permissionName)
+ protected function checkPermission(string $permission): void
{
- if (!user() || !user()->can($permissionName)) {
+ if (!user() || !user()->can($permission)) {
$this->showPermissionError();
}
- return true;
}
/**
- * Check the current user's permissions against an ownable item.
- * @param $permission
- * @param Ownable $ownable
- * @return bool
+ * Check the current user's permissions against an ownable item otherwise throw an exception.
*/
- protected function checkOwnablePermission($permission, Ownable $ownable)
+ protected function checkOwnablePermission(string $permission, Model $ownable): void
{
- if (userCan($permission, $ownable)) {
- return true;
+ if (!userCan($permission, $ownable)) {
+ $this->showPermissionError();
}
- return $this->showPermissionError();
}
/**
- * Check if a user has a permission or bypass if the callback is true.
- * @param $permissionName
- * @param $callback
- * @return bool
+ * Check if a user has a permission or bypass the permission
+ * check if the given callback resolves true.
*/
- protected function checkPermissionOr($permissionName, $callback)
+ protected function checkPermissionOr(string $permission, callable $callback): void
{
- $callbackResult = $callback();
- if ($callbackResult === false) {
- $this->checkPermission($permissionName);
+ if ($callback() !== true) {
+ $this->checkPermission($permission);
}
- return true;
}
/**
* Check if the current user has a permission or bypass if the provided user
* id matches the current user.
- * @param string $permissionName
- * @param int $userId
- * @return bool
*/
- protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
+ protected function checkPermissionOrCurrentUser(string $permission, int $userId): void
{
- return $this->checkPermissionOr($permissionName, function () use ($userId) {
+ $this->checkPermissionOr($permission, function () use ($userId) {
return $userId === user()->id;
});
}
/**
* Send back a json error message.
- * @param string $messageText
- * @param int $statusCode
- * @return mixed
*/
- protected function jsonError($messageText = "", $statusCode = 500)
+ protected function jsonError(string $messageText = "", int $statusCode = 500): JsonResponse
{
return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
* Create a response that forces a download in the browser.
- * @param string $content
- * @param string $fileName
- * @return \Illuminate\Http\Response
*/
- protected function downloadResponse(string $content, string $fileName)
+ protected function downloadResponse(string $content, string $fileName): Response
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
/**
* Show a positive, successful notification to the user on next view load.
- * @param string $message
*/
- protected function showSuccessNotification(string $message)
+ protected function showSuccessNotification(string $message): void
{
session()->flash('success', $message);
}
/**
* Show a warning notification to the user on next view load.
- * @param string $message
*/
- protected function showWarningNotification(string $message)
+ protected function showWarningNotification(string $message): void
{
session()->flash('warning', $message);
}
/**
* Show an error notification to the user on next view load.
- * @param string $message
*/
- protected function showErrorNotification(string $message)
+ protected function showErrorNotification(string $message): void
{
session()->flash('error', $message);
}
+ /**
+ * Log an activity in the system.
+ * @param string|Loggable
+ */
+ protected function logActivity(string $type, $detail = ''): void
+ {
+ Activity::add($type, $detail);
+ }
+
/**
* Get the validation rules for image files.
*/
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Http\Response;
/**
* Display the homepage.
- * @return Response
*/
public function index()
{
$draftPages = [];
if ($this->isSignedIn()) {
- $draftPages = Page::visible()->where('draft', '=', true)
+ $draftPages = Page::visible()
+ ->where('draft', '=', true)
->where('created_by', '=', user()->id)
- ->orderBy('updated_at', 'desc')->take(6)->get();
+ ->orderBy('updated_at', 'desc')
+ ->with('book')
+ ->take(6)
+ ->get();
}
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
- Views::getUserRecentlyViewed(12*$recentFactor, 0)
+ Views::getUserRecentlyViewed(12*$recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
- $recentlyUpdatedPages = Page::visible()->where('draft', false)
- ->orderBy('updated_at', 'desc')->take(12)->get();
+ $recentlyUpdatedPages = Page::visible()->with('book')
+ ->where('draft', false)
+ ->orderBy('updated_at', 'desc')
+ ->take(12)
+ ->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$homepageOption = setting('app-homepage-type', 'default');
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
<?php namespace BookStack\Http\Controllers\Images;
-use BookStack\Entities\Page;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
-use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Filesystem\Filesystem as File;
-use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Tools\TrashCan;
use BookStack\Notifications\TestEmail;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
// Get application version
$version = trim(file_get_contents(base_path('version')));
- return view('settings.maintenance', ['version' => $version]);
+ // Recycle bin details
+ $recycleStats = (new TrashCan())->getTrashedCounts();
+
+ return view('settings.maintenance', [
+ 'version' => $version,
+ 'recycleStats' => $recycleStats,
+ ]);
}
/**
public function cleanupImages(Request $request, ImageService $imageService)
{
$this->checkPermission('settings-manage');
+ $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images');
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
$dryRun = !($request->has('confirm'));
public function sendTestEmail()
{
$this->checkPermission('settings-manage');
+ $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'send-test-email');
try {
user()->notify(new TestEmail());
<?php namespace BookStack\Http\Controllers;
-use Activity;
-use BookStack\Entities\Managers\BookContents;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Managers\PageEditActivity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\BookContents;
+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\NotifyException;
use BookStack\Exceptions\PermissionsException;
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
- parent::__construct();
}
/**
public function editDraft(string $bookSlug, int $pageId)
{
$draft = $this->pageRepo->getById($pageId);
- $this->checkOwnablePermission('page-create', $draft->parent());
+ $this->checkOwnablePermission('page-create', $draft->getParent());
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->isSignedIn();
'name' => 'required|string|max:255'
]);
$draftPage = $this->pageRepo->getById($pageId);
- $this->checkOwnablePermission('page-create', $draftPage->parent());
+ $this->checkOwnablePermission('page-create', $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
- Activity::add($page, 'page_create', $draftPage->book->id);
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->update($page, $request->all());
- Activity::add($page, 'page_update', $page->book->id);
return redirect($page->getUrl());
}
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
+ $parent = $page->getParent();
- $book = $page->book;
- $parent = $page->chapter ?? $book;
$this->pageRepo->destroy($page);
- Activity::addMessage('page_delete', $page->name, $book->id);
- $this->showSuccessNotification(trans('entities.pages_delete_success'));
return redirect($parent->getUrl());
}
return redirect()->back();
}
- Activity::add($page, 'page_move', $page->book->id);
$this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
return redirect()->back();
}
- Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
-
$this->showSuccessNotification(trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
* @throws NotFoundException
* @throws Throwable
*/
- public function permissions(Request $request, string $bookSlug, string $pageSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($page, $request);
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
namespace BookStack\Http\Controllers;
-use BookStack\Entities\ExportService;
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Tools\ExportFormatter;
+use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use Throwable;
{
protected $pageRepo;
- protected $exportService;
+ protected $exportFormatter;
/**
* PageExportController constructor.
- * @param PageRepo $pageRepo
- * @param ExportService $exportService
*/
- public function __construct(PageRepo $pageRepo, ExportService $exportService)
+ public function __construct(PageRepo $pageRepo, ExportFormatter $exportFormatter)
{
$this->pageRepo = $pageRepo;
- $this->exportService = $exportService;
- parent::__construct();
+ $this->exportFormatter = $exportFormatter;
}
/**
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
- $pdfContent = $this->exportService->pageToPdf($page);
+ $pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$page->html = (new PageContent($page))->render();
- $containedHtml = $this->exportService->pageToContainedHtml($page);
+ $containedHtml = $this->exportFormatter->pageToContainedHtml($page);
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
}
public function plainText(string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
- $pageText = $this->exportService->pageToPlainText($page);
+ $pageText = $this->exportFormatter->pageToPlainText($page);
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
}
<?php namespace BookStack\Http\Controllers;
-use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
-use BookStack\Facades\Activity;
-use GatherContent\Htmldiff\Htmldiff;
+use Ssddanbrown\HtmlDiff\Diff;
class PageRevisionController extends Controller
{
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
- parent::__construct();
}
/**
$prev = $revision->getPrevious();
$prevContent = $prev->html ?? '';
- $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+ $diff = Diff::excecute($prevContent, $revision->html);
$page->fill($revision->toArray());
// TODO - Refactor PageContent so we don't need to juggle this
$page = $this->pageRepo->restoreRevision($page, $revisionId);
- Activity::add($page, 'page_restore', $page->book->id);
return redirect($page->getUrl());
}
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
- parent::__construct();
}
/**
--- /dev/null
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Tools\TrashCan;
+
+class RecycleBinController extends Controller
+{
+
+ protected $recycleBinBaseUrl = '/settings/recycle-bin';
+
+ /**
+ * On each request to a method of this controller check permissions
+ * using a middleware closure.
+ */
+ public function __construct()
+ {
+ $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.
+ */
+ public function index()
+ {
+ $deletions = Deletion::query()->with(['deletable', 'deleter'])->paginate(10);
+
+ $this->setPageTitle(trans('settings.recycle_bin'));
+ return view('settings.recycle-bin.index', [
+ 'deletions' => $deletions,
+ ]);
+ }
+
+ /**
+ * Show the page to confirm a restore of the deletion of the given id.
+ */
+ public function showRestore(string $id)
+ {
+ /** @var Deletion $deletion */
+ $deletion = Deletion::query()->findOrFail($id);
+
+ return view('settings.recycle-bin.restore', [
+ 'deletion' => $deletion,
+ ]);
+ }
+
+ /**
+ * Restore the element attached to the given deletion.
+ * @throws \Exception
+ */
+ public function restore(string $id)
+ {
+ /** @var Deletion $deletion */
+ $deletion = Deletion::query()->findOrFail($id);
+ $this->logActivity(ActivityType::RECYCLE_BIN_RESTORE, $deletion);
+ $restoreCount = (new TrashCan())->restoreFromDeletion($deletion);
+
+ $this->showSuccessNotification(trans('settings.recycle_bin_restore_notification', ['count' => $restoreCount]));
+ return redirect($this->recycleBinBaseUrl);
+ }
+
+ /**
+ * Show the page to confirm a Permanent deletion of the element attached to the deletion of the given id.
+ */
+ public function showDestroy(string $id)
+ {
+ /** @var Deletion $deletion */
+ $deletion = Deletion::query()->findOrFail($id);
+
+ return view('settings.recycle-bin.destroy', [
+ 'deletion' => $deletion,
+ ]);
+ }
+
+ /**
+ * Permanently delete the content associated with the given deletion.
+ * @throws \Exception
+ */
+ public function destroy(string $id)
+ {
+ /** @var Deletion $deletion */
+ $deletion = Deletion::query()->findOrFail($id);
+ $this->logActivity(ActivityType::RECYCLE_BIN_DESTROY, $deletion);
+ $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()
+ {
+ $deleteCount = (new TrashCan())->empty();
+
+ $this->logActivity(ActivityType::RECYCLE_BIN_EMPTY);
+ $this->showSuccessNotification(trans('settings.recycle_bin_destroy_notification', ['count' => $deleteCount]));
+ return redirect($this->recycleBinBaseUrl);
+ }
+}
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
-class PermissionController extends Controller
+class RoleController extends Controller
{
protected $permissionsRepo;
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
- parent::__construct();
}
/**
* Show a listing of the roles in the system.
*/
- public function listRoles()
+ public function list()
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
/**
* Show the form to create a new role
*/
- public function createRole()
+ public function create()
{
$this->checkPermission('user-roles-manage');
return view('settings.roles.create');
/**
* Store a new role in the system.
*/
- public function storeRole(Request $request)
+ public function store(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
* Show the form for editing a user role.
* @throws PermissionsException
*/
- public function editRole(string $id)
+ public function edit(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
* Updates a user role.
* @throws ValidationException
*/
- public function updateRole(Request $request, string $id)
+ public function update(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
* Show the view to delete a role.
* Offers the chance to migrate users.
*/
- public function showDeleteRole(string $id)
+ public function showDelete(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
* Migrate from a previous role if set.
* @throws Exception
*/
- public function deleteRole(Request $request, string $id)
+ public function delete(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Managers\EntityContext;
-use BookStack\Entities\SearchService;
-use BookStack\Entities\SearchOptions;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\SearchRunner;
+use BookStack\Entities\Tools\ShelfContext;
+use BookStack\Entities\Tools\SearchOptions;
+use BookStack\Entities\Tools\SiblingFetcher;
use Illuminate\Http\Request;
class SearchController extends Controller
{
protected $viewService;
- protected $searchService;
+ protected $searchRunner;
protected $entityContextManager;
- /**
- * SearchController constructor.
- */
public function __construct(
ViewService $viewService,
- SearchService $searchService,
- EntityContext $entityContextManager
+ SearchRunner $searchRunner,
+ ShelfContext $entityContextManager
) {
$this->viewService = $viewService;
- $this->searchService = $searchService;
+ $this->searchRunner = $searchRunner;
$this->entityContextManager = $entityContextManager;
- parent::__construct();
}
/**
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1));
- $results = $this->searchService->searchEntities($searchOpts, 'all', $page, 20);
+ $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, 20);
return view('search.all', [
'entities' => $results['results'],
]);
}
-
/**
* Searches all entities within a book.
*/
public function searchBook(Request $request, int $bookId)
{
$term = $request->get('term', '');
- $results = $this->searchService->searchBook($bookId, $term);
+ $results = $this->searchRunner->searchBook($bookId, $term);
return view('partials.entity-list', ['entities' => $results]);
}
public function searchChapter(Request $request, int $chapterId)
{
$term = $request->get('term', '');
- $results = $this->searchService->searchChapter($chapterId, $term);
+ $results = $this->searchRunner->searchChapter($chapterId, $term);
return view('partials.entity-list', ['entities' => $results]);
}
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
- $entities = $this->searchService->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
+ $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
}
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
- $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
- if (!$entity) {
- return $this->jsonError(trans('errors.entity_not_found'), 404);
- }
-
- $entities = [];
-
- // Page in chapter
- if ($entity->isA('page') && $entity->chapter) {
- $entities = $entity->chapter->getVisiblePages();
- }
-
- // Page in book or chapter
- if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
- $entities = $entity->book->getDirectChildren();
- }
-
- // Book
- // Gets just the books in a shelf if shelf is in context
- if ($entity->isA('book')) {
- $contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
- if ($contextShelf) {
- $entities = $contextShelf->visibleBooks()->get();
- } else {
- $entities = Book::visible()->get();
- }
- }
-
- // Shelve
- if ($entity->isA('bookshelf')) {
- $entities = Bookshelf::visible()->get();
- }
-
+ $entities = (new SiblingFetcher)->fetch($type, $id);
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}
<?php namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
+ $key = str_replace('setting-', '', trim($name));
if (strpos($name, 'setting-') !== 0) {
continue;
}
- $key = str_replace('setting-', '', trim($name));
setting()->put($key, $value);
}
setting()->remove('app-logo');
}
+ $section = $request->get('section', '');
+ $this->logActivity(ActivityType::SETTINGS_UPDATE, $section);
$this->showSuccessNotification(trans('settings.settings_save_success'));
- $redirectLocation = '/settings#' . $request->get('section', '');
+ $redirectLocation = '/settings#' . $section;
return redirect(rtrim($redirectLocation, '#'));
}
}
public function __construct(TagRepo $tagRepo)
{
$this->tagRepo = $tagRepo;
- parent::__construct();
}
/**
<?php namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Api\ApiToken;
use BookStack\Auth\User;
use Illuminate\Http\Request;
-use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
session()->flash('api-token-secret:' . $token->id, $secret);
$this->showSuccessNotification(trans('settings.user_api_token_create_success'));
+ $this->logActivity(ActivityType::API_TOKEN_CREATE, $token);
+
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
}
])->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));
}
$token->delete();
$this->showSuccessNotification(trans('settings.user_api_token_delete_success'));
+ $this->logActivity(ActivityType::API_TOKEN_DELETE, $token);
+
return redirect($user->getEditUrl('#api_tokens'));
}
<?php namespace BookStack\Http\Controllers;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\User;
$this->userRepo = $userRepo;
$this->inviteService = $inviteService;
$this->imageRepo = $imageRepo;
- parent::__construct();
}
/**
$this->userRepo->downloadAndAssignUserAvatar($user);
+ $this->logActivity(ActivityType::USER_CREATE, $user);
return redirect('/settings/users');
}
$user->image_id = $image->id;
}
- // Delete the profile image if set to
+ // Delete the profile image if reset option is in request
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
}
$user->save();
$this->showSuccessNotification(trans('settings.users_edit_success'));
+ $this->logActivity(ActivityType::USER_UPDATE, $user);
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
* Remove the specified user from storage.
* @throws \Exception
*/
- public function destroy(int $id)
+ public function destroy(Request $request, int $id)
{
$this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
+ $newOwnerId = $request->get('new_owner_id', null);
if ($this->userRepo->isOnlyAdmin($user)) {
$this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
- $this->userRepo->destroy($user);
+ $this->userRepo->destroy($user, $newOwnerId);
$this->showSuccessNotification(trans('settings.users_delete_success'));
+ $this->logActivity(ActivityType::USER_DELETE, $user);
return redirect('/settings/users');
}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\Request;
+
+class UserSearchController extends Controller
+{
+ /**
+ * Search users in the system, with the response formatted
+ * for use in a select-style list.
+ */
+ public function forSelect(Request $request)
+ {
+ $search = $request->get('search', '');
+ $query = User::query()->orderBy('name', 'desc')
+ ->take(20);
+
+ if (!empty($search)) {
+ $query->where(function (Builder $query) use ($search) {
+ $query->where('email', 'like', '%' . $search . '%')
+ ->orWhere('name', 'like', '%' . $search . '%');
+ });
+ }
+
+ $users = $query->get();
+ return view('components.user-select-list', compact('users'));
+ }
+}
*/
protected $middlewareGroups = [
'web' => [
+ \BookStack\Http\Middleware\ControlIframeSecurity::class,
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
--- /dev/null
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Closure;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Sets CSP headers to restrict the hosts that BookStack can be
+ * iframed within. Also adjusts the cookie samesite options
+ * so that cookies will operate in the third-party context.
+ */
+class ControlIframeSecurity
+{
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ $iframeHosts = collect(explode(' ', config('app.iframe_hosts', '')))->filter();
+ if ($iframeHosts->count() > 0) {
+ config()->set('session.same_site', 'none');
+ }
+
+ $iframeHosts->prepend("'self'");
+
+ $response = $next($request);
+ $cspValue = 'frame-ancestors ' . $iframeHosts->join(' ');
+ $response->headers->set('Content-Security-Policy', $cspValue);
+ return $response;
+ }
+}
--- /dev/null
+<?php
+
+namespace BookStack\Interfaces;
+
+interface Loggable
+{
+ /**
+ * Get the string descriptor for this item.
+ */
+ public function logDescriptor(): string;
+}
\ No newline at end of file
+++ /dev/null
-<?php namespace BookStack;
-
-use BookStack\Auth\User;
-
-/**
- * @property int created_by
- * @property int updated_by
- */
-abstract class Ownable extends Model
-{
- /**
- * Relation for the user that created this entity.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function createdBy()
- {
- return $this->belongsTo(User::class, 'created_by');
- }
-
- /**
- * Relation for the user that updated this entity.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function updatedBy()
- {
- return $this->belongsTo(User::class, 'updated_by');
- }
-
- /**
- * Gets the class name.
- * @return string
- */
- public static function getClassName()
- {
- return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
- }
-}
<?php namespace BookStack\Providers;
use Blade;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;
use Schema;
use URL;
-use Validator;
class AppServiceProvider extends ServiceProvider
{
URL::forceScheme($isHttps ? 'https' : 'http');
}
- // Custom validation methods
- Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
- $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
- return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
- });
-
- Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
- $uploadName = $value->getClientOriginalName();
- return substr_count($uploadName, '.') < 2;
- });
-
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";
});
- Blade::directive('exposeTranslations', function ($expression) {
- return "<?php \$__env->startPush('translations'); ?>" .
- "<?php foreach({$expression} as \$key): ?>" .
- '<meta name="translation" key="<?php echo e($key); ?>" value="<?php echo e(trans($key)); ?>">' . "\n" .
- "<?php endforeach; ?>" .
- '<?php $__env->stopPush(); ?>';
- });
-
// Allow longer string lengths after upgrade to utf8mb4
Schema::defaultStringLength(191);
--- /dev/null
+<?php
+
+namespace BookStack\Providers;
+
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\ServiceProvider;
+
+class CustomValidationServiceProvider extends ServiceProvider
+{
+
+ /**
+ * Register our custom validation rules when the application boots.
+ */
+ public function boot(): void
+ {
+ Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
+ $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
+ return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
+ });
+
+ Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
+ $uploadName = $value->getClientOriginalName();
+ return substr_count($uploadName, '.') < 2;
+ });
+
+ Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
+ $cleanLinkName = strtolower(trim($value));
+ $isJs = strpos($cleanLinkName, 'javascript:') === 0;
+ $isData = strpos($cleanLinkName, 'data:') === 0;
+ return !$isJs && !$isData;
+ });
+ }
+}
--- /dev/null
+<?php namespace BookStack\Traits;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * @property int created_by
+ * @property int updated_by
+ */
+trait HasCreatorAndUpdater
+{
+ /**
+ * Relation for the user that created this entity.
+ */
+ public function createdBy(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'created_by');
+ }
+
+ /**
+ * Relation for the user that updated this entity.
+ */
+ public function updatedBy(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'updated_by');
+ }
+
+}
--- /dev/null
+<?php namespace BookStack\Traits;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * @property int owned_by
+ */
+trait HasOwner
+{
+ /**
+ * Relation for the user that owns this entity.
+ */
+ public function ownedBy(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'owned_by');
+ }
+
+}
<?php namespace BookStack\Uploads;
-use BookStack\Entities\Page;
-use BookStack\Ownable;
+use BookStack\Entities\Models\Page;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
/**
* @property int id
* @property string extension
* @property bool external
*/
-class Attachment extends Ownable
+class Attachment extends Model
{
+ use HasCreatorAndUpdater;
+
protected $fillable = ['name', 'order'];
/**
use BookStack\Exceptions\FileUploadException;
use Exception;
+use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class AttachmentService extends UploadService
+class AttachmentService
{
+ protected $fileSystem;
+
+ /**
+ * AttachmentService constructor.
+ */
+ public function __construct(FileSystem $fileSystem)
+ {
+ $this->fileSystem = $fileSystem;
+ }
+
+
/**
* Get the storage that will be used for storing files.
- * @return \Illuminate\Contracts\Filesystem\Filesystem
*/
- protected function getStorage()
+ protected function getStorage(): FileSystemInstance
{
$storageType = config('filesystems.attachments');
/**
* Save a new File attachment from a given link and name.
- * @param string $name
- * @param string $link
- * @param int $page_id
- * @return Attachment
*/
- public function saveNewFromLink($name, $link, $page_id)
+ public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
return Attachment::forceCreate([
/**
* Update the details of a file.
- * @param Attachment $attachment
- * @param $requestData
- * @return Attachment
*/
- public function updateFile(Attachment $attachment, $requestData)
+ public function updateFile(Attachment $attachment, array $requestData): Attachment
{
$attachment->name = $requestData['name'];
+
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
$attachment->path = $requestData['link'];
if (!$attachment->external) {
$attachment->external = true;
}
}
+
$attachment->save();
return $attachment;
}
<?php namespace BookStack\Uploads;
-use BookStack\Entities\Page;
-use BookStack\Ownable;
+use BookStack\Entities\Models\Page;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
use Images;
-class Image extends Ownable
+class Image extends Model
{
+ use HasCreatorAndUpdater;
protected $fillable = ['name'];
protected $hidden = [];
<?php namespace BookStack\Uploads;
use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Exceptions\ImageUploadException;
use Exception;
use Illuminate\Database\Eloquent\Builder;
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
- $validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
+ $validPageIds = $contextPage->book->pages()->visible()->get(['id'])->pluck('id')->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};
<?php namespace BookStack\Uploads;
-use BookStack\Auth\User;
-use BookStack\Exceptions\HttpFetchException;
use BookStack\Exceptions\ImageUploadException;
use DB;
+use ErrorException;
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\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
-use phpDocumentor\Reflection\Types\Integer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
-class ImageService extends UploadService
+class ImageService
{
-
protected $imageTool;
protected $cache;
protected $storageUrl;
protected $image;
- protected $http;
+ protected $fileSystem;
/**
* ImageService constructor.
- * @param Image $image
- * @param ImageManager $imageTool
- * @param FileSystem $fileSystem
- * @param Cache $cache
- * @param HttpFetcher $http
*/
- public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
+ public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->image = $image;
$this->imageTool = $imageTool;
+ $this->fileSystem = $fileSystem;
$this->cache = $cache;
- $this->http = $http;
- parent::__construct($fileSystem);
}
/**
* Get the storage that will be used for storing images.
- * @param string $type
- * @return \Illuminate\Contracts\Filesystem\Filesystem
*/
- protected function getStorage($type = '')
+ protected function getStorage(string $type = ''): FileSystemInstance
{
$storageType = config('filesystems.images');
/**
* Saves a new image from an upload.
- * @param UploadedFile $uploadedFile
- * @param string $type
- * @param int $uploadedTo
- * @param int|null $resizeWidth
- * @param int|null $resizeHeight
- * @param bool $keepRatio
* @return mixed
* @throws ImageUploadException
*/
/**
* Save a new image from a uri-encoded base64 string of data.
- * @param string $base64Uri
- * @param string $name
- * @param string $type
- * @param int $uploadedTo
- * @return Image
* @throws ImageUploadException
*/
- public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
+ public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, int $uploadedTo = 0): Image
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
return $this->saveNew($name, $data, $type, $uploadedTo);
}
- /**
- * Gets an image from url and saves it to the database.
- * @param $url
- * @param string $type
- * @param bool|string $imageName
- * @return mixed
- * @throws \Exception
- */
- private function saveNewFromUrl($url, $type, $imageName = false)
- {
- $imageName = $imageName ? $imageName : basename($url);
- try {
- $imageData = $this->http->fetch($url);
- } catch (HttpFetchException $exception) {
- throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
- }
- return $this->saveNew($imageName, $imageData, $type);
- }
-
/**
* Save a new image into storage.
* @throws ImageUploadException
*/
- private function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
+ public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
{
$storage = $this->getStorage($type);
$secureUploads = setting('app-secure-images');
}
$imageDetails = [
- 'name' => $imageName,
- 'path' => $fullPath,
- 'url' => $this->getPublicUrl($fullPath),
- 'type' => $type,
+ 'name' => $imageName,
+ 'path' => $fullPath,
+ 'url' => $this->getPublicUrl($fullPath),
+ 'type' => $type,
'uploaded_to' => $uploadedTo
];
$name = Str::random(10);
}
- return $name . '.' . $extension;
+ return $name . '.' . $extension;
}
/**
* Checks if the image is a gif. Returns true if it is, else false.
- * @param Image $image
- * @return boolean
*/
- protected function isGif(Image $image)
+ protected function isGif(Image $image): bool
{
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}
try {
$thumb = $this->imageTool->make($imageData);
} catch (Exception $e) {
- if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
+ if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
}
throw $e;
/**
* Get the raw data content from an image.
- * @param Image $image
- * @return string
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ * @throws FileNotFoundException
*/
- public function getImageData(Image $image)
+ public function getImageData(Image $image): string
{
$imagePath = $image->path;
$storage = $this->getStorage();
/**
* Destroy an image along with its revisions, thumbnails and remaining folders.
- * @param Image $image
* @throws Exception
*/
public function destroy(Image $image)
// Cleanup of empty folders
$foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
foreach ($foldersInvolved as $directory) {
- if ($this->isFolderEmpty($directory)) {
+ if ($this->isFolderEmpty($storage, $directory)) {
$storage->deleteDirectory($directory);
}
}
}
/**
- * Save an avatar image from an external service.
- * @param \BookStack\Auth\User $user
- * @param int $size
- * @return Image
- * @throws Exception
- */
- public function saveUserAvatar(User $user, $size = 500)
- {
- $avatarUrl = $this->getAvatarUrl();
- $email = strtolower(trim($user->email));
-
- $replacements = [
- '${hash}' => md5($email),
- '${size}' => $size,
- '${email}' => urlencode($email),
- ];
-
- $userAvatarUrl = strtr($avatarUrl, $replacements);
- $imageName = str_replace(' ', '-', $user->name . '-avatar.png');
- $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
- $image->created_by = $user->id;
- $image->updated_by = $user->id;
- $image->uploaded_to = $user->id;
- $image->save();
-
- return $image;
- }
-
- /**
- * Check if fetching external avatars is enabled.
- * @return bool
- */
- public function avatarFetchEnabled()
- {
- $fetchUrl = $this->getAvatarUrl();
- return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
- }
-
- /**
- * Get the URL to fetch avatars from.
- * @return string|mixed
+ * Check whether or not a folder is empty.
*/
- protected function getAvatarUrl()
+ protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
{
- $url = trim(config('services.avatar_url'));
-
- if (empty($url) && !config('services.disable_services')) {
- $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
- }
-
- return $url;
+ $files = $storage->files($path);
+ $folders = $storage->directories($path);
+ return (count($files) === 0 && count($folders) === 0);
}
/**
* Could be much improved to be more specific but kept it generic for now to be safe.
*
* Returns the path of the images that would be/have been deleted.
- * @param bool $checkRevisions
- * @param bool $dryRun
- * @param array $types
- * @return array
*/
- public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
+ public function deleteUnusedImages(bool $checkRevisions = true, bool $dryRun = true)
{
- $types = array_intersect($types, ['gallery', 'drawio']);
+ $types = ['gallery', 'drawio'];
$deletedPaths = [];
$this->image->newQuery()->whereIn('type', $types)
- ->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
+ ->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages')
- ->where('html', 'like', $searchQuery)->count() > 0;
+ ->where('html', 'like', $searchQuery)->count() > 0;
+
$inRevision = false;
if ($checkRevisions) {
- $inRevision = DB::table('page_revisions')
- ->where('html', 'like', $searchQuery)->count() > 0;
+ $inRevision = DB::table('page_revisions')
+ ->where('html', 'like', $searchQuery)->count() > 0;
}
if (!$inPage && !$inRevision) {
/**
* Convert a image URI to a Base64 encoded string.
- * Attempts to find locally via set storage method first.
- * @param string $uri
- * @return null|string
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ * 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)
+ public function imageUriToBase64(string $uri): ?string
{
- $isLocal = strpos(trim($uri), 'http') !== 0;
-
- // Attempt to find local files even if url not absolute
- $base = url('/');
- if (!$isLocal && strpos($uri, $base) === 0) {
- $isLocal = true;
- $uri = str_replace($base, '', $uri);
+ $storagePath = $this->imageUrlToStoragePath($uri);
+ if (empty($uri) || is_null($storagePath)) {
+ return null;
}
+ $storage = $this->getStorage();
$imageData = null;
-
- if ($isLocal) {
- $uri = trim($uri, '/');
- $storage = $this->getStorage();
- if ($storage->exists($uri)) {
- $imageData = $storage->get($uri);
- }
- } else {
- try {
- $imageData = $this->http->fetch($uri);
- } catch (\Exception $e) {
- }
+ if ($storage->exists($storagePath)) {
+ $imageData = $storage->get($storagePath);
}
- if ($imageData === null) {
+ if (is_null($imageData)) {
return null;
}
return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);
}
+ /**
+ * Get a storage path for the given image URL.
+ * Ensures the path will start with "uploads/images".
+ * Returns null if the url cannot be resolved to a local URL.
+ */
+ private function imageUrlToStoragePath(string $url): ?string
+ {
+ $url = ltrim(trim($url), '/');
+
+ // Handle potential relative paths
+ $isRelative = strpos($url, 'http') !== 0;
+ if ($isRelative) {
+ if (strpos(strtolower($url), 'uploads/images') === 0) {
+ return trim($url, '/');
+ }
+ return null;
+ }
+
+ // Handle local images based on paths on the same domain
+ $potentialHostPaths = [
+ url('uploads/images/'),
+ $this->getPublicUrl('/uploads/images/'),
+ ];
+
+ foreach ($potentialHostPaths as $potentialBasePath) {
+ $potentialBasePath = strtolower($potentialBasePath);
+ if (strpos(strtolower($url), $potentialBasePath) === 0) {
+ return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');
+ }
+ }
+
+ return null;
+ }
+
/**
* Gets a public facing url for an image by checking relevant environment variables.
- * @param string $filePath
- * @return string
+ * If s3-style store is in use it will default to guessing a public bucket URL.
*/
- private function getPublicUrl($filePath)
+ private function getPublicUrl(string $filePath): string
{
if ($this->storageUrl === null) {
$storageUrl = config('filesystems.url');
+++ /dev/null
-<?php namespace BookStack\Uploads;
-
-use Illuminate\Contracts\Filesystem\Factory as FileSystem;
-use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
-
-abstract class UploadService
-{
-
- /**
- * @var FileSystem
- */
- protected $fileSystem;
-
-
- /**
- * FileService constructor.
- * @param $fileSystem
- */
- public function __construct(FileSystem $fileSystem)
- {
- $this->fileSystem = $fileSystem;
- }
-
- /**
- * Get the storage that will be used for storing images.
- * @return FileSystemInstance
- */
- protected function getStorage()
- {
- $storageType = config('filesystems.default');
- return $this->fileSystem->disk($storageType);
- }
-
- /**
- * Check whether or not a folder is empty.
- * @param $path
- * @return bool
- */
- protected function isFolderEmpty($path)
- {
- $files = $this->getStorage()->files($path);
- $folders = $this->getStorage()->directories($path);
- return (count($files) === 0 && count($folders) === 0);
- }
-}
--- /dev/null
+<?php namespace BookStack\Uploads;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\HttpFetchException;
+use Exception;
+
+class UserAvatars
+{
+ protected $imageService;
+ protected $http;
+
+ public function __construct(ImageService $imageService, HttpFetcher $http)
+ {
+ $this->imageService = $imageService;
+ $this->http = $http;
+ }
+
+ /**
+ * Fetch and assign an avatar image to the given user.
+ */
+ public function fetchAndAssignToUser(User $user): void
+ {
+ if (!$this->avatarFetchEnabled()) {
+ return;
+ }
+
+ try {
+ $avatar = $this->saveAvatarImage($user);
+ $user->avatar()->associate($avatar);
+ $user->save();
+ } catch (Exception $e) {
+ Log::error('Failed to save user avatar image');
+ }
+ }
+
+ /**
+ * Save an avatar image from an external service.
+ * @throws Exception
+ */
+ protected function saveAvatarImage(User $user, int $size = 500): Image
+ {
+ $avatarUrl = $this->getAvatarUrl();
+ $email = strtolower(trim($user->email));
+
+ $replacements = [
+ '${hash}' => md5($email),
+ '${size}' => $size,
+ '${email}' => urlencode($email),
+ ];
+
+ $userAvatarUrl = strtr($avatarUrl, $replacements);
+ $imageName = str_replace(' ', '-', $user->id . '-avatar.png');
+ $imageData = $this->getAvatarImageData($userAvatarUrl);
+
+ $image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
+ $image->created_by = $user->id;
+ $image->updated_by = $user->id;
+ $image->save();
+
+ return $image;
+ }
+
+ /**
+ * Gets an image from url and returns it as a string of image data.
+ * @throws Exception
+ */
+ protected function getAvatarImageData(string $url): string
+ {
+ try {
+ $imageData = $this->http->fetch($url);
+ } catch (HttpFetchException $exception) {
+ throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
+ }
+ return $imageData;
+ }
+
+ /**
+ * Check if fetching external avatars is enabled.
+ */
+ protected function avatarFetchEnabled(): bool
+ {
+ $fetchUrl = $this->getAvatarUrl();
+ return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
+ }
+
+ /**
+ * Get the URL to fetch avatars from.
+ */
+ protected function getAvatarUrl(): string
+ {
+ $url = trim(config('services.avatar_url'));
+
+ if (empty($url) && !config('services.disable_services')) {
+ $url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
+ }
+
+ return $url;
+ }
+
+}
\ No newline at end of file
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
-use BookStack\Ownable;
+use BookStack\Model;
use BookStack\Settings\SettingService;
/**
* Get the path to a versioned file.
- *
- * @param string $file
- * @return string
* @throws Exception
*/
function versioned_asset(string $file = ''): string
/**
* Helper method to get the current User.
* Defaults to public 'Guest' user if not logged in.
- * @return User
*/
function user(): User
{
}
/**
- * Check if the current user has a permission.
- * If an ownable element is passed in the jointPermissions are checked against
- * that particular item.
+ * Check if the current user has a permission. If an ownable element
+ * is passed in the jointPermissions are checked against that particular item.
*/
-function userCan(string $permission, Ownable $ownable = null): bool
+function userCan(string $permission, Model $ownable = null): bool
{
if ($ownable === null) {
return user() && user()->can($permission);
/**
* Check if the current user has the given permission
* on any item in the system.
- * @param string $permission
- * @param string|null $entityClass
- * @return bool
*/
function userCanOnAny(string $permission, string $entityClass = null): bool
{
/**
* Helper to access system settings.
- * @param string $key
- * @param $default
* @return bool|string|SettingService
*/
function setting(string $key = null, $default = false)
{
$settingService = resolve(SettingService::class);
+
if (is_null($key)) {
return $settingService;
}
+
return $settingService->get($key, $default);
}
/**
* Get a path to a theme resource.
- * @param string $path
- * @return string
*/
function theme_path(string $path = ''): string
{
$theme = config('view.theme');
+
if (!$theme) {
return '';
}
* to the 'resources/assets/icons' folder.
*
* Returns an empty string if icon file not found.
- * @param $name
- * @param array $attrs
- * @return mixed
*/
function icon(string $name, array $attrs = []): string
{
$iconPath = resource_path('icons/' . $name . '.svg');
$themeIconPath = theme_path('icons/' . $name . '.svg');
+
if ($themeIconPath && file_exists($themeIconPath)) {
$iconPath = $themeIconPath;
} else if (!file_exists($iconPath)) {
/*
|--------------------------------------------------------------------------
-| Initialize The App
+| Register The Auto Loader
|--------------------------------------------------------------------------
|
-| We need to get things going before we start up the app.
-| The init file loads everything in, in the correct order.
+| Composer provides a convenient, automatically generated class loader
+| for our application. We just need to utilize it! We'll require it
+| into the script here so that we do not have to worry about the
+| loading of any our classes "manually". Feels great to relax.
|
*/
-require __DIR__.'/bootstrap/init.php';
+require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
+++ /dev/null
-<?php
-
-/*
-|--------------------------------------------------------------------------
-| Load Our Own Helpers
-|--------------------------------------------------------------------------
-|
-| This custom function loads any helpers, before the Laravel Framework
-| is built so we can override any helpers as we please.
-|
-*/
-require __DIR__.'/../app/helpers.php';
-
-/*
-|--------------------------------------------------------------------------
-| Register The Composer Auto Loader
-|--------------------------------------------------------------------------
-|
-| Composer provides a convenient, automatically generated class loader
-| for our application. We just need to utilize it! We'll require it
-| into the script here so that we do not have to worry about the
-| loading of any our classes "manually". Feels great to relax.
-|
-*/
-require __DIR__.'/../vendor/autoload.php';
\ No newline at end of file
"license": "MIT",
"type": "project",
"require": {
- "php": "^7.2",
+ "php": "^7.2.5",
"ext-curl": "*",
"ext-dom": "*",
"ext-gd": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "ext-tidy": "*",
"ext-xml": "*",
- "barryvdh/laravel-dompdf": "^0.8.6",
- "barryvdh/laravel-snappy": "^0.4.7",
+ "barryvdh/laravel-dompdf": "^0.8.7",
+ "barryvdh/laravel-snappy": "^0.4.8",
"doctrine/dbal": "^2.9",
- "facade/ignition": "^1.4",
- "fideloper/proxy": "^4.0",
- "gathercontent/htmldiff": "^0.2.1",
- "intervention/image": "^2.5",
- "laravel/framework": "^6.18",
- "laravel/socialite": "^4.3.2",
- "league/commonmark": "^1.4",
- "league/flysystem-aws-s3-v3": "^1.0",
- "nunomaduro/collision": "^3.0",
+ "facade/ignition": "^1.16.4",
+ "fideloper/proxy": "^4.4.1",
+ "intervention/image": "^2.5.1",
+ "laravel/framework": "^6.20",
+ "laravel/socialite": "^5.1",
+ "league/commonmark": "^1.5",
+ "league/flysystem-aws-s3-v3": "^1.0.29",
+ "nunomaduro/collision": "^3.1",
"onelogin/php-saml": "^3.3",
- "predis/predis": "^1.1",
- "socialiteproviders/discord": "^2.0",
- "socialiteproviders/gitlab": "^3.0",
- "socialiteproviders/microsoft-azure": "^3.0",
- "socialiteproviders/okta": "^1.0",
- "socialiteproviders/slack": "^3.0",
- "socialiteproviders/twitch": "^5.0"
+ "predis/predis": "^1.1.6",
+ "socialiteproviders/discord": "^4.1",
+ "socialiteproviders/gitlab": "^4.1",
+ "socialiteproviders/microsoft-azure": "^4.1",
+ "socialiteproviders/okta": "^4.1",
+ "socialiteproviders/slack": "^4.1",
+ "socialiteproviders/twitch": "^5.3",
+ "ssddanbrown/htmldiff": "^1.0"
},
"require-dev": {
- "barryvdh/laravel-debugbar": "^3.2.8",
- "barryvdh/laravel-ide-helper": "^2.6.4",
- "fzaninotto/faker": "^1.4",
- "laravel/browser-kit-testing": "^5.1",
- "mockery/mockery": "^1.0",
+ "barryvdh/laravel-debugbar": "^3.5.1",
+ "barryvdh/laravel-ide-helper": "^2.8.2",
+ "fakerphp/faker": "^1.9.1",
+ "laravel/browser-kit-testing": "^5.2",
+ "mockery/mockery": "^1.3.3",
"phpunit/phpunit": "^8.0",
- "squizlabs/php_codesniffer": "^3.4",
- "wnx/laravel-stats": "^2.0"
+ "squizlabs/php_codesniffer": "^3.5.8"
},
"autoload": {
"classmap": [
],
"psr-4": {
"BookStack\\": "app/"
- }
+ },
+ "files": [
+ "app/helpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
],
- "pre-update-cmd": [
- "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
- "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
- ],
"pre-install-cmd": [
- "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
- "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
+ "@php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\""
],
"post-install-cmd": [
"@php artisan cache:clear",
"preferred-install": "dist",
"sort-packages": true,
"platform": {
- "php": "7.2.0"
+ "php": "7.2.5"
}
},
"extra": {
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "34390536dd685e0bc49b179babaa06ec",
+ "content-hash": "e89dcb5443300c86da774d0abd956d71",
"packages": [
{
"name": "aws/aws-sdk-php",
- "version": "3.154.6",
+ "version": "3.171.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "83a1382930359e4d4f4c9187239f059d5b282520"
+ "reference": "742663a85ec84647f74dea454d2dc45bba180f9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/83a1382930359e4d4f4c9187239f059d5b282520",
- "reference": "83a1382930359e4d4f4c9187239f059d5b282520",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/742663a85ec84647f74dea454d2dc45bba180f9d",
+ "reference": "742663a85ec84647f74dea454d2dc45bba180f9d",
"shasum": ""
},
"require": {
"s3",
"sdk"
],
- "time": "2020-09-18T18:16:42+00:00"
+ "support": {
+ "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
+ "issues": "https://github.com/aws/aws-sdk-php/issues",
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.171.2"
+ },
+ "time": "2020-12-18T19:12:13+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
"laravel",
"pdf"
],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-dompdf/issues",
+ "source": "https://github.com/barryvdh/laravel-dompdf/tree/master"
+ },
"funding": [
{
"url": "https://github.com/barryvdh",
"wkhtmltoimage",
"wkhtmltopdf"
],
- "time": "2020-09-07T12:33:10+00:00"
- },
- {
- "name": "cogpowered/finediff",
- "version": "0.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/cogpowered/FineDiff.git",
- "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/cogpowered/FineDiff/zipball/339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
- "reference": "339ddc8c3afb656efed4f2f0a80e5c3d026f8ea8",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "require-dev": {
- "mockery/mockery": "*",
- "phpunit/phpunit": "*"
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-snappy/issues",
+ "source": "https://github.com/barryvdh/laravel-snappy/tree/master"
},
- "type": "library",
- "autoload": {
- "psr-0": {
- "cogpowered\\FineDiff": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Rob Crowe",
- },
- {
- "name": "Raymond Hill"
- }
- ],
- "description": "PHP implementation of a Fine granularity Diff engine",
- "homepage": "https://github.com/cogpowered/FineDiff",
- "keywords": [
- "diff",
- "finediff",
- "opcode",
- "string",
- "text"
- ],
- "time": "2014-05-19T10:25:02+00:00"
+ "time": "2020-09-07T12:33:10+00:00"
},
{
"name": "doctrine/cache",
"redis",
"xcache"
],
+ "support": {
+ "issues": "https://github.com/doctrine/cache/issues",
+ "source": "https://github.com/doctrine/cache/tree/1.10.x"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"sqlserver",
"sqlsrv"
],
+ "support": {
+ "issues": "https://github.com/doctrine/dbal/issues",
+ "source": "https://github.com/doctrine/dbal/tree/2.10.4"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"event system",
"events"
],
+ "support": {
+ "issues": "https://github.com/doctrine/event-manager/issues",
+ "source": "https://github.com/doctrine/event-manager/tree/1.1.x"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"uppercase",
"words"
],
+ "support": {
+ "issues": "https://github.com/doctrine/inflector/issues",
+ "source": "https://github.com/doctrine/inflector/tree/2.0.x"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"parser",
"php"
],
+ "support": {
+ "issues": "https://github.com/doctrine/lexer/issues",
+ "source": "https://github.com/doctrine/lexer/tree/1.2.1"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
],
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
"homepage": "https://github.com/dompdf/dompdf",
+ "support": {
+ "issues": "https://github.com/dompdf/dompdf/issues",
+ "source": "https://github.com/dompdf/dompdf/tree/master"
+ },
"time": "2020-08-30T22:54:22+00:00"
},
{
"name": "dragonmantank/cron-expression",
- "version": "v2.3.0",
+ "version": "v2.3.1",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
- "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
- "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/65b2d8ee1f10915efb3b55597da3404f096acba2",
+ "reference": "65b2d8ee1f10915efb3b55597da3404f096acba2",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.0|^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.4|^7.0"
+ "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0"
},
"type": "library",
"extra": {
"cron",
"schedule"
],
- "time": "2019-03-31T00:38:28+00:00"
+ "support": {
+ "issues": "https://github.com/dragonmantank/cron-expression/issues",
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dragonmantank",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-13T00:52:37+00:00"
},
{
"name": "egulias/email-validator",
- "version": "2.1.20",
+ "version": "2.1.24",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
- "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff"
+ "reference": "ca90a3291eee1538cd48ff25163240695bd95448"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/f46887bc48db66c7f38f668eb7d6ae54583617ff",
- "reference": "f46887bc48db66c7f38f668eb7d6ae54583617ff",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ca90a3291eee1538cd48ff25163240695bd95448",
+ "reference": "ca90a3291eee1538cd48ff25163240695bd95448",
"shasum": ""
},
"require": {
"validation",
"validator"
],
- "time": "2020-09-06T13:44:32+00:00"
+ "support": {
+ "issues": "https://github.com/egulias/EmailValidator/issues",
+ "source": "https://github.com/egulias/EmailValidator/tree/2.1.24"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/egulias",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-14T15:56:27+00:00"
},
{
"name": "facade/flare-client-php",
- "version": "1.3.6",
+ "version": "1.3.7",
"source": {
"type": "git",
"url": "https://github.com/facade/flare-client-php.git",
- "reference": "451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799"
+ "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/facade/flare-client-php/zipball/451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799",
- "reference": "451fadf38e9f635e7f8e1f5b3cf5c9eb82f11799",
+ "url": "https://api.github.com/repos/facade/flare-client-php/zipball/fd688d3c06658f2b3b5f7bb19f051ee4ddf02492",
+ "reference": "fd688d3c06658f2b3b5f7bb19f051ee4ddf02492",
"shasum": ""
},
"require": {
"facade/ignition-contracts": "~1.0",
"illuminate/pipeline": "^5.5|^6.0|^7.0|^8.0",
- "php": "^7.1",
+ "php": "^7.1|^8.0",
"symfony/http-foundation": "^3.3|^4.1|^5.0",
"symfony/mime": "^3.4|^4.0|^5.1",
"symfony/var-dumper": "^3.4|^4.0|^5.0"
"flare",
"reporting"
],
+ "support": {
+ "issues": "https://github.com/facade/flare-client-php/issues",
+ "source": "https://github.com/facade/flare-client-php/tree/1.3.7"
+ },
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
- "time": "2020-09-18T06:35:11+00:00"
+ "time": "2020-10-21T16:02:39+00:00"
},
{
"name": "facade/ignition",
- "version": "1.16.3",
+ "version": "1.16.4",
"source": {
"type": "git",
"url": "https://github.com/facade/ignition.git",
- "reference": "19674150bb46a4de0ba138c747f538fe7be11dbc"
+ "reference": "1da1705e7f6b24ed45af05461463228da424e14f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/facade/ignition/zipball/19674150bb46a4de0ba138c747f538fe7be11dbc",
- "reference": "19674150bb46a4de0ba138c747f538fe7be11dbc",
+ "url": "https://api.github.com/repos/facade/ignition/zipball/1da1705e7f6b24ed45af05461463228da424e14f",
+ "reference": "1da1705e7f6b24ed45af05461463228da424e14f",
"shasum": ""
},
"require": {
"filp/whoops": "^2.4",
"illuminate/support": "~5.5.0 || ~5.6.0 || ~5.7.0 || ~5.8.0 || ^6.0",
"monolog/monolog": "^1.12 || ^2.0",
- "php": "^7.1",
+ "php": "^7.1|^8.0",
"scrivo/highlight.php": "^9.15",
"symfony/console": "^3.4 || ^4.0",
"symfony/var-dumper": "^3.4 || ^4.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^2.14",
- "mockery/mockery": "^1.2",
+ "mockery/mockery": "~1.3.3|^1.4.2",
"orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
},
"suggest": {
"laravel",
"page"
],
- "time": "2020-07-13T15:54:05+00:00"
+ "support": {
+ "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
+ "forum": "https://twitter.com/flareappio",
+ "issues": "https://github.com/facade/ignition/issues",
+ "source": "https://github.com/facade/ignition"
+ },
+ "time": "2020-10-30T13:40:01+00:00"
},
{
"name": "facade/ignition-contracts",
"flare",
"ignition"
],
+ "support": {
+ "issues": "https://github.com/facade/ignition-contracts/issues",
+ "source": "https://github.com/facade/ignition-contracts/tree/1.0.1"
+ },
"time": "2020-07-14T10:10:28+00:00"
},
{
"name": "fideloper/proxy",
- "version": "4.4.0",
+ "version": "4.4.1",
"source": {
"type": "git",
"url": "https://github.com/fideloper/TrustedProxy.git",
- "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8"
+ "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8",
- "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8",
+ "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/c073b2bd04d1c90e04dc1b787662b558dd65ade0",
+ "reference": "c073b2bd04d1c90e04dc1b787662b558dd65ade0",
"shasum": ""
},
"require": {
- "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0",
+ "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0",
"php": ">=5.4.0"
},
"require-dev": {
- "illuminate/http": "^5.0|^6.0|^7.0|^8.0",
+ "illuminate/http": "^5.0|^6.0|^7.0|^8.0|^9.0",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^6.0"
},
"proxy",
"trusted proxy"
],
- "time": "2020-06-23T01:36:47+00:00"
+ "support": {
+ "issues": "https://github.com/fideloper/TrustedProxy/issues",
+ "source": "https://github.com/fideloper/TrustedProxy/tree/4.4.1"
+ },
+ "time": "2020-10-22T13:48:01+00:00"
},
{
"name": "filp/whoops",
- "version": "2.7.3",
+ "version": "2.9.1",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d"
+ "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
- "reference": "5d5fe9bb3d656b514d455645b3addc5f7ba7714d",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/307fb34a5ab697461ec4c9db865b20ff2fd40771",
+ "reference": "307fb34a5ab697461ec4c9db865b20ff2fd40771",
"shasum": ""
},
"require": {
- "php": "^5.5.9 || ^7.0",
+ "php": "^5.5.9 || ^7.0 || ^8.0",
"psr/log": "^1.0.1"
},
"require-dev": {
"mockery/mockery": "^0.9 || ^1.0",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3",
"symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.6-dev"
+ "dev-master": "2.7-dev"
}
},
"autoload": {
"throwable",
"whoops"
],
- "time": "2020-06-14T09:00:00+00:00"
- },
- {
- "name": "gathercontent/htmldiff",
- "version": "0.2.1",
- "source": {
- "type": "git",
- "url": "https://github.com/gathercontent/htmldiff.git",
- "reference": "24674a62315f64330134b4a4c5b01a7b59193c93"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/gathercontent/htmldiff/zipball/24674a62315f64330134b4a4c5b01a7b59193c93",
- "reference": "24674a62315f64330134b4a4c5b01a7b59193c93",
- "shasum": ""
- },
- "require": {
- "cogpowered/finediff": "0.3.1",
- "ext-tidy": "*"
- },
- "require-dev": {
- "phpunit/phpunit": "4.*",
- "squizlabs/php_codesniffer": "1.*"
- },
- "type": "library",
- "autoload": {
- "psr-0": {
- "GatherContent\\Htmldiff": "src/"
- }
+ "support": {
+ "issues": "https://github.com/filp/whoops/issues",
+ "source": "https://github.com/filp/whoops/tree/2.9.1"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Andrew Cairns",
- },
- {
- "name": "Mathew Chapman",
- },
- {
- "name": "Peter Legierski",
- }
- ],
- "description": "Compare two HTML strings",
- "time": "2015-04-15T15:39:46+00:00"
+ "time": "2020-11-01T12:00:00+00:00"
},
{
"name": "guzzlehttp/guzzle",
- "version": "6.5.5",
+ "version": "7.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
+ "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
- "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79",
+ "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^1.0",
- "guzzlehttp/psr7": "^1.6.1",
- "php": ">=5.5",
- "symfony/polyfill-intl-idn": "^1.17.0"
+ "guzzlehttp/promises": "^1.4",
+ "guzzlehttp/psr7": "^1.7",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
},
"require-dev": {
"ext-curl": "*",
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+ "php-http/client-integration-tests": "^3.0",
+ "phpunit/phpunit": "^8.5.5 || ^9.3.5",
"psr/log": "^1.1"
},
"suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.5-dev"
+ "dev-master": "7.1-dev"
}
},
"autoload": {
"name": "Michael Dowling",
"homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "homepage": "https://sagikazarmark.hu"
}
],
"description": "Guzzle is a PHP HTTP client library",
"framework",
"http",
"http client",
+ "psr-18",
+ "psr-7",
"rest",
"web service"
],
- "time": "2020-06-16T21:01:06+00:00"
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/alexeyshockov",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/gmponos",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-10T11:47:56+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "v1.3.1",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+ "reference": "60d379c243457e073cff02bc323a2a86cb355631"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
- "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
+ "reference": "60d379c243457e073cff02bc323a2a86cb355631",
"shasum": ""
},
"require": {
- "php": ">=5.5.0"
+ "php": ">=5.5"
},
"require-dev": {
- "phpunit/phpunit": "^4.0"
+ "symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"keywords": [
"promise"
],
- "time": "2016-12-20T10:07:11+00:00"
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/1.4.0"
+ },
+ "time": "2020-09-30T07:37:28+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "1.6.1",
+ "version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+ "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
- "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3",
+ "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3",
"shasum": ""
},
"require": {
},
"require-dev": {
"ext-zlib": "*",
- "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
},
"suggest": {
- "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6-dev"
+ "dev-master": "1.7-dev"
}
},
"autoload": {
"uri",
"url"
],
- "time": "2019-07-01T23:21:34+00:00"
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/1.7.0"
+ },
+ "time": "2020-09-30T07:37:11+00:00"
},
{
"name": "intervention/image",
"thumbnail",
"watermark"
],
+ "support": {
+ "issues": "https://github.com/Intervention/image/issues",
+ "source": "https://github.com/Intervention/image/tree/master"
+ },
"time": "2019-11-02T09:15:47+00:00"
},
{
}
],
+ "support": {
+ "issues": "https://github.com/JakubOnderka/PHP-Console-Color/issues",
+ "source": "https://github.com/JakubOnderka/PHP-Console-Color/tree/master"
+ },
"abandoned": "php-parallel-lint/php-console-color",
"time": "2018-09-29T17:23:10+00:00"
},
}
],
"description": "Highlight PHP code in terminal",
+ "support": {
+ "issues": "https://github.com/JakubOnderka/PHP-Console-Highlighter/issues",
+ "source": "https://github.com/JakubOnderka/PHP-Console-Highlighter/tree/master"
+ },
"abandoned": "php-parallel-lint/php-console-highlighter",
"time": "2018-09-29T18:48:56+00:00"
},
"thumbnail",
"wkhtmltopdf"
],
+ "support": {
+ "issues": "https://github.com/KnpLabs/snappy/issues",
+ "source": "https://github.com/KnpLabs/snappy/tree/master"
+ },
"time": "2020-01-20T08:30:30+00:00"
},
{
"name": "laravel/framework",
- "version": "v6.18.40",
+ "version": "v6.20.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "e42450df0896b7130ccdb5290a114424e18887c9"
+ "reference": "bdc79701b567c5f8ed44d212dd4a261b8300b9c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/e42450df0896b7130ccdb5290a114424e18887c9",
- "reference": "e42450df0896b7130ccdb5290a114424e18887c9",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/bdc79701b567c5f8ed44d212dd4a261b8300b9c3",
+ "reference": "bdc79701b567c5f8ed44d212dd4a261b8300b9c3",
"shasum": ""
},
"require": {
"doctrine/inflector": "^1.4|^2.0",
- "dragonmantank/cron-expression": "^2.0",
+ "dragonmantank/cron-expression": "^2.3.1",
"egulias/email-validator": "^2.1.10",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"league/commonmark": "^1.3",
- "league/flysystem": "^1.0.34",
+ "league/flysystem": "^1.1",
"monolog/monolog": "^1.12|^2.0",
- "nesbot/carbon": "^2.0",
- "opis/closure": "^3.1",
- "php": "^7.2",
+ "nesbot/carbon": "^2.31",
+ "opis/closure": "^3.6",
+ "php": "^7.2.5|^8.0",
"psr/container": "^1.0",
"psr/simple-cache": "^1.0",
"ramsey/uuid": "^3.7",
"illuminate/view": "self.version"
},
"require-dev": {
- "aws/aws-sdk-php": "^3.0",
+ "aws/aws-sdk-php": "^3.155",
"doctrine/dbal": "^2.6",
- "filp/whoops": "^2.4",
- "guzzlehttp/guzzle": "^6.3|^7.0",
+ "filp/whoops": "^2.8",
+ "guzzlehttp/guzzle": "^6.3.1|^7.0.1",
"league/flysystem-cached-adapter": "^1.0",
- "mockery/mockery": "^1.3.1",
+ "mockery/mockery": "~1.3.3|^1.4.2",
"moontoast/math": "^1.1",
- "orchestra/testbench-core": "^4.0",
+ "orchestra/testbench-core": "^4.8",
"pda/pheanstalk": "^4.0",
- "phpunit/phpunit": "^7.5.15|^8.4|^9.0",
+ "phpunit/phpunit": "^7.5.15|^8.4|^9.3.3",
"predis/predis": "^1.1.1",
"symfony/cache": "^4.3.4"
},
"suggest": {
- "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+ "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
"ext-ftp": "Required to use the Flysystem FTP driver.",
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
- "filp/whoops": "Required for friendly error pages in development (^2.4).",
- "fzaninotto/faker": "Required to use the eloquent factory builder (^1.9.1).",
- "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0|^7.0).",
+ "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
+ "filp/whoops": "Required for friendly error pages in development (^2.8).",
+ "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.3.1|^7.0.1).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
"framework",
"laravel"
],
- "time": "2020-09-09T15:02:20+00:00"
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2020-12-08T15:31:27+00:00"
},
{
"name": "laravel/socialite",
- "version": "v4.4.1",
+ "version": "v5.1.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "80951df0d93435b773aa00efe1fad6d5015fac75"
+ "reference": "19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/80951df0d93435b773aa00efe1fad6d5015fac75",
- "reference": "80951df0d93435b773aa00efe1fad6d5015fac75",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7",
+ "reference": "19fc65ac28e0b4684a8735b14c1dc6f6ef5d62c7",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.0|^7.0",
- "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
- "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "illuminate/http": "^6.0|^7.0|^8.0",
+ "illuminate/support": "^6.0|^7.0|^8.0",
"league/oauth1-client": "^1.0",
- "php": "^7.1.3"
+ "php": "^7.2|^8.0"
},
"require-dev": {
- "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "illuminate/contracts": "^6.0|^7.0",
"mockery/mockery": "^1.0",
- "orchestra/testbench": "^3.7|^3.8|^4.0|^5.0",
- "phpunit/phpunit": "^7.0|^8.0"
+ "orchestra/testbench": "^4.0|^5.0|^6.0",
+ "phpunit/phpunit": "^8.0|^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.x-dev"
+ "dev-master": "5.x-dev"
},
"laravel": {
"providers": [
"laravel",
"oauth"
],
- "time": "2020-06-03T13:30:03+00:00"
+ "support": {
+ "issues": "https://github.com/laravel/socialite/issues",
+ "source": "https://github.com/laravel/socialite"
+ },
+ "time": "2020-12-04T15:30:50+00:00"
},
{
"name": "league/commonmark",
- "version": "1.5.5",
+ "version": "1.5.7",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
- "reference": "45832dfed6007b984c0d40addfac48d403dc6432"
+ "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/45832dfed6007b984c0d40addfac48d403dc6432",
- "reference": "45832dfed6007b984c0d40addfac48d403dc6432",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/11df9b36fd4f1d2b727a73bf14931d81373b9a54",
+ "reference": "11df9b36fd4f1d2b727a73bf14931d81373b9a54",
"shasum": ""
},
"require": {
"md",
"parser"
],
+ "support": {
+ "docs": "https://commonmark.thephpleague.com/",
+ "issues": "https://github.com/thephpleague/commonmark/issues",
+ "rss": "https://github.com/thephpleague/commonmark/releases.atom",
+ "source": "https://github.com/thephpleague/commonmark"
+ },
"funding": [
{
"url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark",
"type": "tidelift"
}
],
- "time": "2020-09-13T14:44:46+00:00"
+ "time": "2020-10-31T13:49:32+00:00"
},
{
"name": "league/flysystem",
- "version": "1.0.70",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493"
+ "reference": "9be3b16c877d477357c015cec057548cf9b2a14a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/585824702f534f8d3cf7fab7225e8466cc4b7493",
- "reference": "585824702f534f8d3cf7fab7225e8466cc4b7493",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a",
+ "reference": "9be3b16c877d477357c015cec057548cf9b2a14a",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
- "php": ">=5.5.9"
+ "league/mime-type-detection": "^1.3",
+ "php": "^7.2.5 || ^8.0"
},
"conflict": {
"league/flysystem-sftp": "<1.0.6"
},
"require-dev": {
- "phpspec/phpspec": "^3.4 || ^4.0 || ^5.0 || ^6.0",
- "phpunit/phpunit": "^5.7.26"
+ "phpspec/prophecy": "^1.11.1",
+ "phpunit/phpunit": "^8.5.8"
},
"suggest": {
"ext-fileinfo": "Required for MimeType",
"sftp",
"storage"
],
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem/issues",
+ "source": "https://github.com/thephpleague/flysystem/tree/1.x"
+ },
"funding": [
{
"url": "https://offset.earth/frankdejonge",
"type": "other"
}
],
- "time": "2020-07-26T07:20:36+00:00"
+ "time": "2020-08-23T07:39:11+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "1.0.28",
+ "version": "1.0.29",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990"
+ "reference": "4e25cc0582a36a786c31115e419c6e40498f6972"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af7384a12f7cd7d08183390d930c9d0ec629c990",
- "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4e25cc0582a36a786c31115e419c6e40498f6972",
+ "reference": "4e25cc0582a36a786c31115e419c6e40498f6972",
"shasum": ""
},
"require": {
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
- "time": "2020-08-22T08:43:01+00:00"
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
+ "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/1.0.29"
+ },
+ "time": "2020-10-08T18:58:37+00:00"
+ },
+ {
+ "name": "league/mime-type-detection",
+ "version": "1.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/mime-type-detection.git",
+ "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa",
+ "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.36",
+ "phpunit/phpunit": "^8.5.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\MimeTypeDetection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ }
+ ],
+ "description": "Mime-type detection for Flysystem",
+ "support": {
+ "issues": "https://github.com/thephpleague/mime-type-detection/issues",
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.5.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/frankdejonge",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-10-18T11:50:25+00:00"
},
{
"name": "league/oauth1-client",
- "version": "v1.8.1",
+ "version": "v1.8.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/oauth1-client.git",
- "reference": "3a68155c3f27a91f4b66a2dc03996cd6f3281c9f"
+ "reference": "159c3d2bf27568f9af87d6c3f4bb616a251eb12b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/3a68155c3f27a91f4b66a2dc03996cd6f3281c9f",
- "reference": "3a68155c3f27a91f4b66a2dc03996cd6f3281c9f",
+ "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/159c3d2bf27568f9af87d6c3f4bb616a251eb12b",
+ "reference": "159c3d2bf27568f9af87d6c3f4bb616a251eb12b",
"shasum": ""
},
"require": {
"tumblr",
"twitter"
],
- "time": "2020-09-04T11:07:03+00:00"
+ "support": {
+ "issues": "https://github.com/thephpleague/oauth1-client/issues",
+ "source": "https://github.com/thephpleague/oauth1-client/tree/v1.8.2"
+ },
+ "time": "2020-09-28T09:39:08+00:00"
},
{
"name": "monolog/monolog",
- "version": "2.1.1",
+ "version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5"
+ "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9eee5cec93dfb313a38b6b288741e84e53f02d5",
- "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084",
+ "reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084",
"shasum": ""
},
"require": {
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
- "elasticsearch/elasticsearch": "^6.0",
+ "elasticsearch/elasticsearch": "^7",
"graylog2/gelf-php": "^1.4.2",
+ "mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
- "php-parallel-lint/php-parallel-lint": "^1.0",
"phpspec/prophecy": "^1.6.1",
+ "phpstan/phpstan": "^0.12.59",
"phpunit/phpunit": "^8.5",
"predis/predis": "^1.1",
"rollbar/rollbar": "^1.3",
- "ruflin/elastica": ">=0.90 <3.0",
+ "ruflin/elastica": ">=0.90 <7.0.1",
"swiftmailer/swiftmailer": "^5.3|^6.0"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.x-dev"
+ "dev-main": "2.x-dev"
}
},
"autoload": {
{
"name": "Jordi Boggiano",
- "homepage": "http://seld.be"
+ "homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
- "homepage": "http://github.com/Seldaek/monolog",
+ "homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/2.2.0"
+ },
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "tidelift"
}
],
- "time": "2020-07-23T08:41:23+00:00"
+ "time": "2020-12-14T13:15:25+00:00"
},
{
"name": "mtdowling/jmespath.php",
"json",
"jsonpath"
],
+ "support": {
+ "issues": "https://github.com/jmespath/jmespath.php/issues",
+ "source": "https://github.com/jmespath/jmespath.php/tree/2.6.0"
+ },
"time": "2020-07-31T21:01:56+00:00"
},
{
"name": "nesbot/carbon",
- "version": "2.40.0",
+ "version": "2.42.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d"
+ "reference": "d0463779663437392fe42ff339ebc0213bd55498"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/6c7646154181013ecd55e80c201b9fd873c6ee5d",
- "reference": "6c7646154181013ecd55e80c201b9fd873c6ee5d",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/d0463779663437392fe42ff339ebc0213bd55498",
+ "reference": "d0463779663437392fe42ff339ebc0213bd55498",
"shasum": ""
},
"require": {
"kylekatarnls/multi-tester": "^2.0",
"phpmd/phpmd": "^2.9",
"phpstan/extension-installer": "^1.0",
- "phpstan/phpstan": "^0.12.35",
+ "phpstan/phpstan": "^0.12.54",
"phpunit/phpunit": "^7.5 || ^8.0",
"squizlabs/php_codesniffer": "^3.4"
},
"datetime",
"time"
],
+ "support": {
+ "issues": "https://github.com/briannesbitt/Carbon/issues",
+ "source": "https://github.com/briannesbitt/Carbon"
+ },
"funding": [
{
"url": "https://opencollective.com/Carbon",
"type": "tidelift"
}
],
- "time": "2020-09-11T19:00:58+00:00"
+ "time": "2020-11-28T14:25:28+00:00"
},
{
"name": "nunomaduro/collision",
- "version": "v3.0.1",
+ "version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
- "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+ "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
- "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/88b58b5bd9bdcc54756480fb3ce87234696544ee",
+ "reference": "88b58b5bd9bdcc54756480fb3ce87234696544ee",
"shasum": ""
},
"require": {
"filp/whoops": "^2.1.4",
"jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
- "php": "^7.1",
+ "php": "^7.1 || ^8.0",
"symfony/console": "~2.8|~3.3|~4.0"
},
"require-dev": {
- "laravel/framework": "5.8.*",
- "nunomaduro/larastan": "^0.3.0",
- "phpstan/phpstan": "^0.11",
- "phpunit/phpunit": "~8.0"
+ "laravel/framework": "^6.0",
+ "phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"php",
"symfony"
],
- "time": "2019-03-07T21:35:13+00:00"
+ "support": {
+ "issues": "https://github.com/nunomaduro/collision/issues",
+ "source": "https://github.com/nunomaduro/collision"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/nunomaduro",
+ "type": "patreon"
+ }
+ ],
+ "time": "2020-10-29T16:05:21+00:00"
},
{
"name": "onelogin/php-saml",
- "version": "3.4.1",
+ "version": "3.5.1",
"source": {
"type": "git",
"url": "https://github.com/onelogin/php-saml.git",
- "reference": "5fbf3486704ac9835b68184023ab54862c95f213"
+ "reference": "593aca859b67d607923fe50d8ad7315373f5b6dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/onelogin/php-saml/zipball/5fbf3486704ac9835b68184023ab54862c95f213",
- "reference": "5fbf3486704ac9835b68184023ab54862c95f213",
+ "url": "https://api.github.com/repos/onelogin/php-saml/zipball/593aca859b67d607923fe50d8ad7315373f5b6dd",
+ "reference": "593aca859b67d607923fe50d8ad7315373f5b6dd",
"shasum": ""
},
"require": {
"php": ">=5.4",
- "robrichards/xmlseclibs": ">=3.0.4"
+ "robrichards/xmlseclibs": ">=3.1.1"
},
"require-dev": {
"pdepend/pdepend": "^2.5.0",
"onelogin",
"saml"
],
- "time": "2019-11-25T17:30:07+00:00"
+ "support": {
+ "issues": "https://github.com/onelogin/php-saml/issues",
+ "source": "https://github.com/onelogin/php-saml/"
+ },
+ "time": "2020-12-03T20:08:41+00:00"
},
{
"name": "opis/closure",
- "version": "3.5.7",
+ "version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/opis/closure.git",
- "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf"
+ "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/opis/closure/zipball/4531e53afe2fc660403e76fb7644e95998bff7bf",
- "reference": "4531e53afe2fc660403e76fb7644e95998bff7bf",
+ "url": "https://api.github.com/repos/opis/closure/zipball/943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
+ "reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
"shasum": ""
},
"require": {
- "php": "^5.4 || ^7.0"
+ "php": "^5.4 || ^7.0 || ^8.0"
},
"require-dev": {
"jeremeamia/superclosure": "^2.0",
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.5.x-dev"
+ "dev-master": "3.6.x-dev"
}
},
"autoload": {
"serialization",
"serialize"
],
- "time": "2020-09-06T17:02:15+00:00"
+ "support": {
+ "issues": "https://github.com/opis/closure/issues",
+ "source": "https://github.com/opis/closure/tree/3.6.1"
+ },
+ "time": "2020-11-07T02:01:34+00:00"
},
{
"name": "paragonie/random_compat",
"pseudorandom",
"random"
],
+ "support": {
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
"time": "2018-07-02T15:55:56+00:00"
},
{
],
"description": "A library to read, parse, export and make subsets of different types of font files.",
"homepage": "https://github.com/PhenX/php-font-lib",
+ "support": {
+ "issues": "https://github.com/PhenX/php-font-lib/issues",
+ "source": "https://github.com/PhenX/php-font-lib/tree/0.5.2"
+ },
"time": "2020-03-08T15:31:32+00:00"
},
{
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
+ "support": {
+ "issues": "https://github.com/PhenX/php-svg-lib/issues",
+ "source": "https://github.com/PhenX/php-svg-lib/tree/master"
+ },
"time": "2019-09-11T20:02:13+00:00"
},
{
"php",
"type"
],
+ "support": {
+ "issues": "https://github.com/schmittjoh/php-option/issues",
+ "source": "https://github.com/schmittjoh/php-option/tree/1.7.5"
+ },
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"predis",
"redis"
],
+ "support": {
+ "issues": "https://github.com/predis/predis/issues",
+ "source": "https://github.com/predis/predis/tree/v1.1.6"
+ },
"funding": [
{
"url": "https://github.com/sponsors/tillkruss",
"container-interop",
"psr"
],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/master"
+ },
"time": "2017-02-14T16:28:37+00:00"
},
+ {
+ "name": "psr/http-client",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client/tree/master"
+ },
+ "time": "2020-06-29T06:28:15+00:00"
+ },
{
"name": "psr/http-message",
"version": "1.0.1",
"request",
"response"
],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
"time": "2016-08-06T14:39:51+00:00"
},
{
"psr",
"psr-3"
],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.3"
+ },
"time": "2020-03-23T09:12:05+00:00"
},
{
"psr-16",
"simple-cache"
],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
"time": "2017-10-23T01:57:42+00:00"
},
{
}
],
"description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
"time": "2019-03-08T08:55:37+00:00"
},
{
"identifier",
"uuid"
],
+ "support": {
+ "issues": "https://github.com/ramsey/uuid/issues",
+ "rss": "https://github.com/ramsey/uuid/releases.atom",
+ "source": "https://github.com/ramsey/uuid",
+ "wiki": "https://github.com/ramsey/uuid/wiki"
+ },
"time": "2020-02-21T04:36:14+00:00"
},
{
"xml",
"xmldsig"
],
+ "support": {
+ "issues": "https://github.com/robrichards/xmlseclibs/issues",
+ "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1"
+ },
"time": "2020-09-05T13:00:25+00:00"
},
{
"parser",
"stylesheet"
],
+ "support": {
+ "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues",
+ "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.3.1"
+ },
"time": "2020-06-01T09:10:00+00:00"
},
{
"name": "scrivo/highlight.php",
- "version": "v9.18.1.2",
+ "version": "v9.18.1.5",
"source": {
"type": "git",
"url": "https://github.com/scrivo/highlight.php.git",
- "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9"
+ "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/efb6e445494a9458aa59b0af5edfa4bdcc6809d9",
- "reference": "efb6e445494a9458aa59b0af5edfa4bdcc6809d9",
+ "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/fa75a865928a4a5d49e5e77faca6bd2f2410baaf",
+ "reference": "fa75a865928a4a5d49e5e77faca6bd2f2410baaf",
"shasum": ""
},
"require": {
"highlight.php",
"syntax"
],
+ "support": {
+ "issues": "https://github.com/scrivo/highlight.php/issues",
+ "source": "https://github.com/scrivo/highlight.php"
+ },
"funding": [
{
"url": "https://github.com/allejo",
"type": "github"
}
],
- "time": "2020-08-27T03:24:44+00:00"
+ "time": "2020-11-22T06:07:40+00:00"
},
{
"name": "socialiteproviders/discord",
- "version": "v2.0.2",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Discord.git",
- "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a"
+ "reference": "34c62db509c9680e120982f9239db5ce905eb027"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/e0cd8895f321943b36f533e7bf21ad29bcdece9a",
- "reference": "e0cd8895f321943b36f533e7bf21ad29bcdece9a",
+ "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/34c62db509c9680e120982f9239db5ce905eb027",
+ "reference": "34c62db509c9680e120982f9239db5ce905eb027",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "ext-json": "*",
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "Discord OAuth2 Provider for Laravel Socialite",
- "time": "2018-05-26T03:40:07+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Discord/tree/4.1.0"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/gitlab",
- "version": "v3.1",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/GitLab.git",
- "reference": "69e537f6192ca15483e98b8662495384f44299ca"
+ "reference": "a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/69e537f6192ca15483e98b8662495384f44299ca",
- "reference": "69e537f6192ca15483e98b8662495384f44299ca",
+ "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d",
+ "reference": "a8f67d3b02c9ee8c70c25c6728417c0eddcbbb9d",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "ext-json": "*",
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "GitLab OAuth2 Provider for Laravel Socialite",
- "time": "2018-06-27T05:10:32+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/GitLab/tree/4.1.0"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/manager",
- "version": "v3.6",
+ "version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
- "reference": "fc8dbcf0061f12bfe0cc347e9655af932860ad36"
+ "reference": "0f5e82af0404df0080bdc5c105cef936c1711524"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/fc8dbcf0061f12bfe0cc347e9655af932860ad36",
- "reference": "fc8dbcf0061f12bfe0cc347e9655af932860ad36",
+ "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/0f5e82af0404df0080bdc5c105cef936c1711524",
+ "reference": "0f5e82af0404df0080bdc5c105cef936c1711524",
"shasum": ""
},
"require": {
"illuminate/support": "^6.0|^7.0|^8.0",
"laravel/socialite": "~4.0|~5.0",
- "php": "^7.2"
+ "php": "^7.2 || ^8.0"
},
"require-dev": {
"mockery/mockery": "^1.2",
- "phpunit/phpunit": "^8.0"
+ "phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"homepage": "https://socialiteproviders.com/",
- "time": "2020-09-08T10:41:06+00:00"
+ "support": {
+ "issues": "https://github.com/SocialiteProviders/Manager/issues",
+ "source": "https://github.com/SocialiteProviders/Manager/tree/4.0.1"
+ },
+ "time": "2020-12-01T23:09:06+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
- "version": "v3.1.0",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Microsoft-Azure.git",
- "reference": "b22f4696cccecd6de902cf0bc923de7fc2e4608e"
+ "reference": "7808764f777a01df88be9ca6b14d683e50aaf88a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/b22f4696cccecd6de902cf0bc923de7fc2e4608e",
- "reference": "b22f4696cccecd6de902cf0bc923de7fc2e4608e",
+ "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/7808764f777a01df88be9ca6b14d683e50aaf88a",
+ "reference": "7808764f777a01df88be9ca6b14d683e50aaf88a",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "Microsoft Azure OAuth2 Provider for Laravel Socialite",
- "time": "2020-04-30T23:01:40+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Microsoft-Azure/tree/4.1.0"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/okta",
- "version": "v1.1.0",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Okta.git",
- "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea"
+ "reference": "60f88b8e8c88508889c61346af83290131b72fe7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/7c2512f0872316b139e3eea1c50c9351747a57ea",
- "reference": "7c2512f0872316b139e3eea1c50c9351747a57ea",
+ "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/60f88b8e8c88508889c61346af83290131b72fe7",
+ "reference": "60f88b8e8c88508889c61346af83290131b72fe7",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "Okta OAuth2 Provider for Laravel Socialite",
- "time": "2019-09-06T15:27:03+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Okta/tree/4.1.0"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
},
{
"name": "socialiteproviders/slack",
- "version": "v3.1",
+ "version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Slack.git",
- "reference": "d46826640fbeae8f34328d99c358404a1e1050a3"
+ "reference": "8efb25c71d98bedf4010a829d1e41ff9fe449bcc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/d46826640fbeae8f34328d99c358404a1e1050a3",
- "reference": "d46826640fbeae8f34328d99c358404a1e1050a3",
+ "url": "https://api.github.com/repos/SocialiteProviders/Slack/zipball/8efb25c71d98bedf4010a829d1e41ff9fe449bcc",
+ "reference": "8efb25c71d98bedf4010a829d1e41ff9fe449bcc",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "ext-json": "*",
+ "php": "^7.2|^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "Slack OAuth2 Provider for Laravel Socialite",
- "time": "2019-01-11T19:48:14+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Slack/tree/4.1.0"
+ },
+ "time": "2020-11-26T17:57:15+00:00"
},
{
"name": "socialiteproviders/twitch",
- "version": "v5.2.0",
+ "version": "5.3.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Twitch.git",
- "reference": "9ee6fe196d7c28777139b3cde04cbd537cf7e652"
+ "reference": "7accf30ae7a3139b757b4ca8f34989c09a3dbee7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/9ee6fe196d7c28777139b3cde04cbd537cf7e652",
- "reference": "9ee6fe196d7c28777139b3cde04cbd537cf7e652",
+ "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/7accf30ae7a3139b757b4ca8f34989c09a3dbee7",
+ "reference": "7accf30ae7a3139b757b4ca8f34989c09a3dbee7",
"shasum": ""
},
"require": {
"ext-json": "*",
- "php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~2.0 || ~3.0"
+ "php": "^7.2 || ^8.0",
+ "socialiteproviders/manager": "~4.0"
},
"type": "library",
"autoload": {
}
],
"description": "Twitch OAuth2 Provider for Laravel Socialite",
- "time": "2020-05-06T22:51:30+00:00"
+ "support": {
+ "source": "https://github.com/SocialiteProviders/Twitch/tree/5.3.1"
+ },
+ "time": "2020-12-01T23:10:59+00:00"
+ },
+ {
+ "name": "ssddanbrown/htmldiff",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ssddanbrown/HtmlDiff.git",
+ "reference": "d1978c7d1c685800997f982a0ae9cff1e45df70c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ssddanbrown/HtmlDiff/zipball/d1978c7d1c685800997f982a0ae9cff1e45df70c",
+ "reference": "d1978c7d1c685800997f982a0ae9cff1e45df70c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5|^9.4.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Ssddanbrown\\HtmlDiff\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dan Brown",
+ "role": "Developer"
+ }
+ ],
+ "description": "HTML Content Diff Generator",
+ "homepage": "https://github.com/ssddanbrown/htmldiff",
+ "support": {
+ "issues": "https://github.com/ssddanbrown/HtmlDiff/issues",
+ "source": "https://github.com/ssddanbrown/HtmlDiff/tree/v1.0.0"
+ },
+ "time": "2020-11-29T18:38:45+00:00"
},
{
"name": "swiftmailer/swiftmailer",
- "version": "v6.2.3",
+ "version": "v6.2.4",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
- "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9"
+ "reference": "56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
- "reference": "149cfdf118b169f7840bbe3ef0d4bc795d1780c9",
+ "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e",
+ "reference": "56f0ab23f54c4ccbb0d5dcc67ff8552e0c98d59e",
"shasum": ""
},
"require": {
- "egulias/email-validator": "~2.0",
+ "egulias/email-validator": "^2.0",
"php": ">=7.0.0",
"symfony/polyfill-iconv": "^1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
- "mockery/mockery": "~0.9.1",
- "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
+ "mockery/mockery": "^1.0",
+ "symfony/phpunit-bridge": "^4.4|^5.0"
},
"suggest": {
- "ext-intl": "Needed to support internationalized email addresses",
- "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed"
+ "ext-intl": "Needed to support internationalized email addresses"
},
"type": "library",
"extra": {
"mail",
"mailer"
],
- "time": "2019-11-12T09:31:26+00:00"
+ "support": {
+ "issues": "https://github.com/swiftmailer/swiftmailer/issues",
+ "source": "https://github.com/swiftmailer/swiftmailer/tree/v6.2.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-12-08T18:02:06+00:00"
},
{
"name": "symfony/console",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727"
+ "reference": "12e071278e396cc3e1c149857337e9e192deca0b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727",
- "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727",
+ "url": "https://api.github.com/repos/symfony/console/zipball/12e071278e396cc3e1c149857337e9e192deca0b",
+ "reference": "12e071278e396cc3e1c149857337e9e192deca0b",
"shasum": ""
},
"require": {
"symfony/process": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-09-02T07:07:21+00:00"
+ "time": "2020-12-18T07:41:31+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "bf17dc9f6ce144e41f786c32435feea4d8e11dcc"
+ "reference": "74bd82e75da256ad20851af6ded07823332216c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/bf17dc9f6ce144e41f786c32435feea4d8e11dcc",
- "reference": "bf17dc9f6ce144e41f786c32435feea4d8e11dcc",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/74bd82e75da256ad20851af6ded07823332216c7",
+ "reference": "74bd82e75da256ad20851af6ded07823332216c7",
"shasum": ""
},
"require": {
"php": ">=7.1.3"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-05T09:39:30+00:00"
+ "time": "2020-12-08T16:59:59+00:00"
},
{
"name": "symfony/debug",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
- "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e"
+ "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/debug/zipball/aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
- "reference": "aeb73aca16a8f1fe958230fe44e6cf4c84cbb85e",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544",
+ "reference": "5dfc7825f3bfe9bb74b23d8b8ce0e0894e32b544",
"shasum": ""
},
"require": {
"symfony/http-kernel": "^3.4|^4.0|^5.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Debug\\": ""
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/debug/tree/v4.4.18"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-12-10T16:34:26+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665",
+ "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.2-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/master"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-10T07:47:39+00:00"
+ "time": "2020-09-07T11:33:47+00:00"
},
{
"name": "symfony/error-handler",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
- "reference": "2434fb32851f252e4f27691eee0b77c16198db62"
+ "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/error-handler/zipball/2434fb32851f252e4f27691eee0b77c16198db62",
- "reference": "2434fb32851f252e4f27691eee0b77c16198db62",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3",
+ "reference": "ef2f7ddd3b9177bbf8ff2ecd8d0e970ed48da0c3",
"shasum": ""
},
"require": {
"symfony/serializer": "^4.4|^5.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\ErrorHandler\\": ""
],
"description": "Symfony ErrorHandler Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/error-handler/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T09:56:45+00:00"
+ "time": "2020-12-09T11:15:38+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030"
+ "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e8ea5ccddd00556b86d69d42f99f1061a704030",
- "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/5d4c874b0eb1c32d40328a09dbc37307a5a910b0",
+ "reference": "5d4c874b0eb1c32d40328a09dbc37307a5a910b0",
"shasum": ""
},
"require": {
"psr/log": "~1.0",
"symfony/config": "^3.4|^4.0|^5.0",
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
+ "symfony/error-handler": "~3.4|~4.4",
"symfony/expression-language": "^3.4|^4.0|^5.0",
"symfony/http-foundation": "^3.4|^4.0|^5.0",
"symfony/service-contracts": "^1.1|^2",
"symfony/http-kernel": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-13T14:18:44+00:00"
+ "time": "2020-12-18T07:41:31+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
"interoperability",
"standards"
],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.9"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
},
{
"name": "symfony/finder",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7"
+ "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/2a78590b2c7e3de5c429628457c47541c58db9c7",
- "reference": "2a78590b2c7e3de5c429628457c47541c58db9c7",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b",
+ "reference": "ebd0965f2dc2d4e0f11487c16fbb041e50b5c09b",
"shasum": ""
},
"require": {
"php": ">=7.1.3"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v4.4.18"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-12-08T16:59:59+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "41db680a15018f9c1d4b23516059633ce280ca33"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/41db680a15018f9c1d4b23516059633ce280ca33",
+ "reference": "41db680a15018f9c1d4b23516059633ce280ca33",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.5"
+ },
+ "suggest": {
+ "symfony/http-client-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-version": "2.3",
+ "branch-alias": {
+ "dev-main": "2.3-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v2.3.1"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T09:56:45+00:00"
+ "time": "2020-10-14T17:08:19+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "e3e5a62a6631a461954d471e7206e3750dbe8ee1"
+ "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e3e5a62a6631a461954d471e7206e3750dbe8ee1",
- "reference": "e3e5a62a6631a461954d471e7206e3750dbe8ee1",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5ebda66b51612516bf338d5f87da2f37ff74cf34",
+ "reference": "5ebda66b51612516bf338d5f87da2f37ff74cf34",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/mime": "^4.3|^5.0",
- "symfony/polyfill-mbstring": "~1.1"
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"predis/predis": "~1.0",
"symfony/expression-language": "^3.4|^4.0|^5.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpFoundation\\": ""
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T07:39:58+00:00"
+ "time": "2020-12-18T07:41:31+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "2bb7b90ecdc79813c0bf237b7ff20e79062b5188"
+ "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/2bb7b90ecdc79813c0bf237b7ff20e79062b5188",
- "reference": "2bb7b90ecdc79813c0bf237b7ff20e79062b5188",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/eaff9a43e74513508867ecfa66ef94fbb96ab128",
+ "reference": "eaff9a43e74513508867ecfa66ef94fbb96ab128",
"shasum": ""
},
"require": {
"psr/log": "~1.0",
"symfony/error-handler": "^4.4",
"symfony/event-dispatcher": "^4.4",
+ "symfony/http-client-contracts": "^1.1|^2",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php73": "^1.9",
"symfony/dependency-injection": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpKernel\\": ""
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-kernel/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-09-02T08:09:29+00:00"
+ "time": "2020-12-18T13:32:33+00:00"
},
{
"name": "symfony/mime",
- "version": "v4.4.13",
+ "version": "v5.2.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a"
+ "reference": "de97005aef7426ba008c46ba840fc301df577ada"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/50ad671306d3d3ffb888d95b4fb1859496831e3a",
- "reference": "50ad671306d3d3ffb888d95b4fb1859496831e3a",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/de97005aef7426ba008c46ba840fc301df577ada",
+ "reference": "de97005aef7426ba008c46ba840fc301df577ada",
"shasum": ""
},
"require": {
- "php": ">=7.1.3",
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-intl-idn": "^1.10",
- "symfony/polyfill-mbstring": "^1.0"
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/polyfill-php80": "^1.15"
},
"conflict": {
"symfony/mailer": "<4.4"
},
"require-dev": {
"egulias/email-validator": "^2.1.10",
- "symfony/dependency-injection": "^3.4|^4.1|^5.0"
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
+ "symfony/dependency-injection": "^4.4|^5.0",
+ "symfony/property-access": "^4.4|^5.1",
+ "symfony/property-info": "^4.4|^5.1",
+ "symfony/serializer": "^5.2"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
"mime",
"mime-type"
],
+ "support": {
+ "source": "https://github.com/symfony/mime/tree/v5.2.1"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T09:56:45+00:00"
+ "time": "2020-12-09T18:54:12+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "1c302646f6efc070cd46856e600e5e0684d6b454"
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454",
- "reference": "1c302646f6efc070cd46856e600e5e0684d6b454",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
+ "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"polyfill",
"portable"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/polyfill-iconv",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
- "reference": "6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36"
+ "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36",
- "reference": "6c2f78eb8f5ab8eaea98f6d414a5915f2e0fce36",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c536646fdb4f29104dd26effc2fdcb9a5b085024",
+ "reference": "c536646fdb4f29104dd26effc2fdcb9a5b085024",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-iconv": "For best performance"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-iconv/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
- "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251"
+ "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251",
- "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117",
+ "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117",
"shasum": ""
},
"require": {
- "php": ">=5.3.3",
+ "php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
- "symfony/polyfill-php70": "^1.10",
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-04T06:02:08+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
- "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e"
+ "reference": "727d1096295d807c309fb01a851577302394c897"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
- "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897",
+ "reference": "727d1096295d807c309fb01a851577302394c897",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-intl": "For best performance"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a"
+ "reference": "39d483bdf39be819deabf04ec872eb0b2410b531"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a",
- "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531",
+ "reference": "39d483bdf39be819deabf04ec872eb0b2410b531",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
- "name": "symfony/polyfill-php70",
- "version": "v1.18.1",
+ "name": "symfony/polyfill-php72",
+ "version": "v1.20.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-php70.git",
- "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3"
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3",
- "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930",
+ "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930",
"shasum": ""
},
"require": {
- "paragonie/random_compat": "~1.0|~2.0|~9.99",
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Php70\\": ""
+ "Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
- ],
- "classmap": [
- "Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php72/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
- "name": "symfony/polyfill-php72",
- "version": "v1.18.1",
+ "name": "symfony/polyfill-php73",
+ "version": "v1.20.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-php72.git",
- "reference": "639447d008615574653fb3bc60d1986d7172eaae"
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae",
- "reference": "639447d008615574653fb3bc60d1986d7172eaae",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed",
+ "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Php72\\": ""
+ "Symfony\\Polyfill\\Php73\\": ""
},
"files": [
"bootstrap.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "shim"
- ],
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2020-07-14T12:35:20+00:00"
- },
- {
- "name": "symfony/polyfill-php73",
- "version": "v1.18.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-php73.git",
- "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
- "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.18-dev"
- },
- "thanks": {
- "name": "symfony/polyfill",
- "url": "https://github.com/symfony/polyfill"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Php73\\": ""
- },
- "files": [
- "bootstrap.php"
- ],
- "classmap": [
- "Resources/stubs"
+ ],
+ "classmap": [
+ "Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php73/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/polyfill-php80",
- "version": "v1.18.1",
+ "version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
- "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981"
+ "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981",
- "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de",
+ "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de",
"shasum": ""
},
"require": {
- "php": ">=7.0.8"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.18-dev"
+ "dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"portable",
"shim"
],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.20.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-14T12:35:20+00:00"
+ "time": "2020-10-23T14:02:19+00:00"
},
{
"name": "symfony/process",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479"
+ "reference": "075316ff72233ce3d04a9743414292e834f2cb4a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479",
- "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479",
+ "url": "https://api.github.com/repos/symfony/process/zipball/075316ff72233ce3d04a9743414292e834f2cb4a",
+ "reference": "075316ff72233ce3d04a9743414292e834f2cb4a",
"shasum": ""
},
"require": {
"php": ">=7.1.3"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-23T08:31:43+00:00"
+ "time": "2020-12-08T16:59:59+00:00"
},
{
"name": "symfony/routing",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04"
+ "reference": "80b042c20b035818daec844723e23b9825134ba0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/e3387963565da9bae51d1d3ab8041646cc93bd04",
- "reference": "e3387963565da9bae51d1d3ab8041646cc93bd04",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/80b042c20b035818daec844723e23b9825134ba0",
+ "reference": "80b042c20b035818daec844723e23b9825134ba0",
"shasum": ""
},
"require": {
"symfony/yaml": "For using the YAML loader"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Routing\\": ""
"uri",
"url"
],
+ "support": {
+ "source": "https://github.com/symfony/routing/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-10T07:27:51+00:00"
+ "time": "2020-12-08T16:59:59+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v1.1.9",
+ "version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26"
+ "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b776d18b303a39f56c63747bcb977ad4b27aca26",
- "reference": "b776d18b303a39f56c63747bcb977ad4b27aca26",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1",
+ "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1",
"shasum": ""
},
"require": {
- "php": ">=7.1.3",
+ "php": ">=7.2.5",
"psr/container": "^1.0"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",
"interoperability",
"standards"
],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/master"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-07-06T13:19:58+00:00"
+ "time": "2020-09-07T11:33:47+00:00"
},
{
"name": "symfony/translation",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "700e6e50174b0cdcf0fa232773bec5c314680575"
+ "reference": "c1001b7d75b3136648f94b245588209d881c6939"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/700e6e50174b0cdcf0fa232773bec5c314680575",
- "reference": "700e6e50174b0cdcf0fa232773bec5c314680575",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/c1001b7d75b3136648f94b245588209d881c6939",
+ "reference": "c1001b7d75b3136648f94b245588209d881c6939",
"shasum": ""
},
"require": {
"symfony/yaml": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Translation\\": ""
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T09:56:45+00:00"
+ "time": "2020-12-08T16:59:59+00:00"
},
{
"name": "symfony/translation-contracts",
- "version": "v1.1.10",
+ "version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
- "reference": "84180a25fad31e23bebd26ca09d89464f082cacc"
+ "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/84180a25fad31e23bebd26ca09d89464f082cacc",
- "reference": "84180a25fad31e23bebd26ca09d89464f082cacc",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e2eaa60b558f26a4b0354e1bbb25636efaaad105",
+ "reference": "e2eaa60b558f26a4b0354e1bbb25636efaaad105",
"shasum": ""
},
"require": {
- "php": ">=7.1.3"
+ "php": ">=7.2.5"
},
"suggest": {
"symfony/translation-implementation": ""
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "2.3-dev"
},
"thanks": {
"name": "symfony/contracts",
"interoperability",
"standards"
],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v2.3.0"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-09-02T16:08:58+00:00"
+ "time": "2020-09-28T13:05:58+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "1bef32329f3166486ab7cb88599cae4875632b99"
+ "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/1bef32329f3166486ab7cb88599cae4875632b99",
- "reference": "1bef32329f3166486ab7cb88599cae4875632b99",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/4f31364bbc8177f2a6dbc125ac3851634ebe2a03",
+ "reference": "4f31364bbc8177f2a6dbc125ac3851634ebe2a03",
"shasum": ""
},
"require": {
"Resources/bin/var-dump-server"
],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"files": [
"Resources/functions/dump.php"
"debug",
"dump"
],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-17T07:31:35+00:00"
+ "time": "2020-12-08T16:59:59+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
],
"description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
+ "support": {
+ "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
+ "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/2.2.3"
+ },
"time": "2020-07-13T06:12:54+00:00"
},
{
"env",
"environment"
],
+ "support": {
+ "issues": "https://github.com/vlucas/phpdotenv/issues",
+ "source": "https://github.com/vlucas/phpdotenv/tree/v3.6.7"
+ },
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"profiler",
"webprofiler"
],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-debugbar/issues",
+ "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.5.1"
+ },
"funding": [
{
"url": "https://github.com/barryvdh",
},
{
"name": "barryvdh/laravel-ide-helper",
- "version": "v2.8.1",
+ "version": "v2.8.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
- "reference": "affa55122f83575888d4ebf1728992686e8223de"
+ "reference": "5515cabea39b9cf55f98980d0f269dc9d85cfcca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/affa55122f83575888d4ebf1728992686e8223de",
- "reference": "affa55122f83575888d4ebf1728992686e8223de",
+ "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5515cabea39b9cf55f98980d0f269dc9d85cfcca",
+ "reference": "5515cabea39b9cf55f98980d0f269dc9d85cfcca",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.0.6",
- "composer/composer": "^1.6 || ^2.0@dev",
+ "composer/composer": "^1.6 || ^2",
"doctrine/dbal": "~2.3",
"ext-json": "*",
"illuminate/console": "^6 || ^7 || ^8",
"phpdocumentor/type-resolver": "^1.1.0"
},
"require-dev": {
+ "ext-pdo_sqlite": "*",
"friendsofphp/php-cs-fixer": "^2",
"illuminate/config": "^6 || ^7 || ^8",
"illuminate/view": "^6 || ^7 || ^8",
- "mockery/mockery": "^1.3",
+ "mockery/mockery": "^1.3.3",
"orchestra/testbench": "^4 || ^5 || ^6",
"phpunit/phpunit": "^8.5 || ^9",
- "spatie/phpunit-snapshot-assertions": "^1.4 || ^2.2 || ^3",
+ "spatie/phpunit-snapshot-assertions": "^1.4 || ^2.2 || ^3 || ^4",
"vimeo/psalm": "^3.12"
},
"type": "library",
"phpstorm",
"sublime"
],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
+ "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.8.2"
+ },
"funding": [
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
- "time": "2020-09-07T07:36:37+00:00"
+ "time": "2020-12-06T08:55:05+00:00"
},
{
"name": "barryvdh/reflection-docblock",
}
],
+ "support": {
+ "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.0.6"
+ },
"time": "2018-12-13T10:34:14+00:00"
},
{
"ssl",
"tls"
],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/ca-bundle/issues",
+ "source": "https://github.com/composer/ca-bundle/tree/1.2.8"
+ },
"funding": [
{
"url": "https://packagist.com",
},
{
"name": "composer/composer",
- "version": "1.10.13",
+ "version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b"
+ "reference": "62139b2806178adb979d76bd3437534a1a9fd490"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
- "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b",
+ "url": "https://api.github.com/repos/composer/composer/zipball/62139b2806178adb979d76bd3437534a1a9fd490",
+ "reference": "62139b2806178adb979d76bd3437534a1a9fd490",
"shasum": ""
},
"require": {
"composer/ca-bundle": "^1.0",
- "composer/semver": "^1.0",
+ "composer/semver": "^3.0",
"composer/spdx-licenses": "^1.2",
"composer/xdebug-handler": "^1.1",
"justinrainbow/json-schema": "^5.2.10",
- "php": "^5.3.2 || ^7.0",
+ "php": "^5.3.2 || ^7.0 || ^8.0",
"psr/log": "^1.0",
+ "react/promise": "^1.2 || ^2.7",
"seld/jsonlint": "^1.4",
"seld/phar-utils": "^1.0",
- "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
- "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0",
- "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0",
- "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0"
- },
- "conflict": {
- "symfony/console": "2.8.38"
+ "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+ "symfony/filesystem": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+ "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0",
+ "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0"
},
"require-dev": {
"phpspec/prophecy": "^1.10",
- "symfony/phpunit-bridge": "^4.2"
+ "symfony/phpunit-bridge": "^4.2 || ^5.0"
},
"suggest": {
"ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.10-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
{
"name": "Nils Adermann",
- "homepage": "http://www.naderman.de"
+ "homepage": "https://www.naderman.de"
},
{
"name": "Jordi Boggiano",
- "homepage": "http://seld.be"
+ "homepage": "https://seld.be"
}
],
"description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
"dependency",
"package"
],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/composer/issues",
+ "source": "https://github.com/composer/composer/tree/2.0.8"
+ },
"funding": [
{
"url": "https://packagist.com",
"type": "tidelift"
}
],
- "time": "2020-09-09T09:46:34+00:00"
+ "time": "2020-12-03T16:20:39+00:00"
},
{
"name": "composer/semver",
- "version": "1.7.0",
+ "version": "3.2.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "114f819054a2ea7db03287f5efb757e2af6e4079"
+ "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/114f819054a2ea7db03287f5efb757e2af6e4079",
- "reference": "114f819054a2ea7db03287f5efb757e2af6e4079",
+ "url": "https://api.github.com/repos/composer/semver/zipball/a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
+ "reference": "a02fdf930a3c1c3ed3a49b5f63859c0c20e10464",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0"
+ "php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.5 || ^5.0.5"
+ "phpstan/phpstan": "^0.12.54",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-main": "3.x-dev"
}
},
"autoload": {
"validation",
"versioning"
],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.2.4"
+ },
"funding": [
{
"url": "https://packagist.com",
"type": "tidelift"
}
],
- "time": "2020-09-09T09:34:06+00:00"
+ "time": "2020-11-13T08:59:24+00:00"
},
{
"name": "composer/spdx-licenses",
- "version": "1.5.4",
+ "version": "1.5.5",
"source": {
"type": "git",
"url": "https://github.com/composer/spdx-licenses.git",
- "reference": "6946f785871e2314c60b4524851f3702ea4f2223"
+ "reference": "de30328a7af8680efdc03e396aad24befd513200"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/6946f785871e2314c60b4524851f3702ea4f2223",
- "reference": "6946f785871e2314c60b4524851f3702ea4f2223",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/de30328a7af8680efdc03e396aad24befd513200",
+ "reference": "de30328a7af8680efdc03e396aad24befd513200",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-main": "1.x-dev"
}
},
"autoload": {
"spdx",
"validator"
],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/spdx-licenses/issues",
+ "source": "https://github.com/composer/spdx-licenses/tree/1.5.5"
+ },
"funding": [
{
"url": "https://packagist.com",
"type": "tidelift"
}
],
- "time": "2020-07-15T15:35:07+00:00"
+ "time": "2020-12-03T16:04:16+00:00"
},
{
"name": "composer/xdebug-handler",
- "version": "1.4.3",
+ "version": "1.4.5",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
- "reference": "ebd27a9866ae8254e873866f795491f02418c5a5"
+ "reference": "f28d44c286812c714741478d968104c5e604a1d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5",
- "reference": "ebd27a9866ae8254e873866f795491f02418c5a5",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4",
+ "reference": "f28d44c286812c714741478d968104c5e604a1d4",
"shasum": ""
},
"require": {
"Xdebug",
"performance"
],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/1.4.5"
+ },
"funding": [
{
"url": "https://packagist.com",
"type": "tidelift"
}
],
- "time": "2020-08-19T10:27:58+00:00"
+ "time": "2020-11-13T08:04:11+00:00"
},
{
"name": "doctrine/instantiator",
- "version": "1.3.1",
+ "version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
- "reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
- "doctrine/coding-standard": "^6.0",
+ "doctrine/coding-standard": "^8.0",
"ext-pdo": "*",
"ext-phar": "*",
- "phpbench/phpbench": "^0.13",
- "phpstan/phpstan-phpunit": "^0.11",
- "phpstan/phpstan-shim": "^0.11",
- "phpunit/phpunit": "^7.0"
+ "phpbench/phpbench": "^0.13 || 1.0.0-alpha2",
+ "phpstan/phpstan": "^0.12",
+ "phpstan/phpstan-phpunit": "^0.12",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.2.x-dev"
- }
- },
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
{
"name": "Marco Pivetta",
- "homepage": "http://ocramius.github.com/"
+ "homepage": "https://ocramius.github.io/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"constructor",
"instantiate"
],
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.0"
+ },
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "tidelift"
}
],
- "time": "2020-05-29T17:27:14+00:00"
+ "time": "2020-11-10T18:47:58+00:00"
},
{
- "name": "fzaninotto/faker",
- "version": "v1.9.1",
+ "name": "fakerphp/faker",
+ "version": "v1.13.0",
"source": {
"type": "git",
- "url": "https://github.com/fzaninotto/Faker.git",
- "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f"
+ "url": "https://github.com/FakerPHP/Faker.git",
+ "reference": "ab3f5364d01f2c2c16113442fb987d26e4004913"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/fc10d778e4b84d5bd315dad194661e091d307c6f",
- "reference": "fc10d778e4b84d5bd315dad194661e091d307c6f",
+ "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/ab3f5364d01f2c2c16113442fb987d26e4004913",
+ "reference": "ab3f5364d01f2c2c16113442fb987d26e4004913",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0"
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "fzaninotto/faker": "*"
},
"require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4.1",
"ext-intl": "*",
- "phpunit/phpunit": "^4.8.35 || ^5.7",
- "squizlabs/php_codesniffer": "^2.9.2"
+ "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.4.2"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.9-dev"
- }
- },
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
"faker",
"fixtures"
],
- "time": "2019-12-12T13:22:17+00:00"
+ "support": {
+ "issues": "https://github.com/FakerPHP/Faker/issues",
+ "source": "https://github.com/FakerPHP/Faker/tree/v1.13.0"
+ },
+ "time": "2020-12-18T16:50:48+00:00"
},
{
"name": "hamcrest/hamcrest-php",
"keywords": [
"test"
],
+ "support": {
+ "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1"
+ },
"time": "2020-07-09T08:09:16+00:00"
},
{
"json",
"schema"
],
+ "support": {
+ "issues": "https://github.com/justinrainbow/json-schema/issues",
+ "source": "https://github.com/justinrainbow/json-schema/tree/5.2.10"
+ },
"time": "2020-05-27T16:41:55+00:00"
},
{
"name": "laravel/browser-kit-testing",
- "version": "v5.1.4",
+ "version": "v5.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/browser-kit-testing.git",
- "reference": "7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1"
+ "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1",
- "reference": "7664a30d2dbabcdb0315bfaa867fef2df8cb8fb1",
+ "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/fa0efb279c009e2a276f934f8aff946caf66edc7",
+ "reference": "fa0efb279c009e2a276f934f8aff946caf66edc7",
"shasum": ""
},
"require": {
"illuminate/http": "~5.7.0|~5.8.0|^6.0",
"illuminate/support": "~5.7.0|~5.8.0|^6.0",
"mockery/mockery": "^1.0",
- "php": ">=7.1.3",
- "phpunit/phpunit": "^7.5|^8.0",
+ "php": "^7.1.3|^8.0",
+ "phpunit/phpunit": "^7.5|^8.0|^9.3",
"symfony/console": "^4.2",
"symfony/css-selector": "^4.2",
"symfony/dom-crawler": "^4.2",
"laravel",
"testing"
],
- "time": "2020-08-25T16:54:44+00:00"
+ "support": {
+ "issues": "https://github.com/laravel/browser-kit-testing/issues",
+ "source": "https://github.com/laravel/browser-kit-testing/tree/v5.2.0"
+ },
+ "time": "2020-10-30T08:49:09+00:00"
},
{
"name": "maximebf/debugbar",
- "version": "v1.16.3",
+ "version": "v1.16.4",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
- "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372"
+ "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1a1605b8e9bacb34cc0c6278206d699772e1d372",
- "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372",
+ "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/c86c717e4bf3c6d98422da5c38bfa7b0f494b04c",
+ "reference": "c86c717e4bf3c6d98422da5c38bfa7b0f494b04c",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": "^7.1|^8",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3|^4|^5"
},
"require-dev": {
- "phpunit/phpunit": "^5"
+ "phpunit/phpunit": "^7.5.20 || ^9.4.2"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"debug",
"debugbar"
],
- "time": "2020-05-06T07:06:27+00:00"
+ "support": {
+ "issues": "https://github.com/maximebf/php-debugbar/issues",
+ "source": "https://github.com/maximebf/php-debugbar/tree/v1.16.4"
+ },
+ "time": "2020-12-07T10:48:48+00:00"
},
{
"name": "mockery/mockery",
"test double",
"testing"
],
+ "support": {
+ "issues": "https://github.com/mockery/mockery/issues",
+ "source": "https://github.com/mockery/mockery/tree/1.3.3"
+ },
"time": "2020-08-11T18:10:21+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.10.1",
+ "version": "1.10.2",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
- "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
"shasum": ""
},
"require": {
"object",
"object graph"
],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2"
+ },
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
- "time": "2020-06-29T13:22:24+00:00"
+ "time": "2020-11-13T09:40:50+00:00"
},
{
"name": "phar-io/manifest",
- "version": "1.0.3",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
- "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
+ "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
- "phar-io/version": "^2.0",
- "php": "^5.6 || ^7.0"
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
"authors": [
{
"name": "Arne Blankerts",
- "role": "Developer",
+ "role": "Developer"
},
{
"name": "Sebastian Heuer",
- "role": "Developer",
+ "role": "Developer"
},
{
"name": "Sebastian Bergmann",
- "role": "Developer",
+ "role": "Developer"
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
- "time": "2018-07-08T19:23:20+00:00"
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/master"
+ },
+ "time": "2020-06-27T14:33:11+00:00"
},
{
"name": "phar-io/version",
- "version": "2.0.1",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+ "reference": "e4782611070e50613683d2b9a57730e9a3ba5451"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
- "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451",
+ "reference": "e4782611070e50613683d2b9a57730e9a3ba5451",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
}
],
"description": "Library for handling version information and constraints",
- "time": "2018-07-08T19:19:57+00:00"
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.0.4"
+ },
+ "time": "2020-12-13T23:18:30+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"reflection",
"static analysis"
],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
"time": "2020-06-27T09:03:43+00:00"
},
{
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
+ },
"time": "2020-09-03T19:13:55+00:00"
},
{
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
- "time": "2020-09-17T18:55:26+00:00"
- },
- {
- "name": "phploc/phploc",
- "version": "5.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phploc.git",
- "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phploc/zipball/5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
- "reference": "5b714ccb7cb8ca29ccf9caf6eb1aed0131d3a884",
- "shasum": ""
- },
- "require": {
- "php": "^7.2",
- "sebastian/finder-facade": "^1.1",
- "sebastian/version": "^2.0",
- "symfony/console": "^4.0"
- },
- "bin": [
- "phploc"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.0-dev"
- }
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0"
},
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "role": "lead"
- }
- ],
- "description": "A tool for quickly measuring the size of a PHP project.",
- "homepage": "https://github.com/sebastianbergmann/phploc",
- "time": "2019-03-16T10:41:19+00:00"
+ "time": "2020-09-17T18:55:26+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "1.11.1",
+ "version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
- "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+ "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2",
- "php": "^7.2",
- "phpdocumentor/reflection-docblock": "^5.0",
+ "php": "^7.2 || ~8.0, <8.1",
+ "phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^6.0",
- "phpunit/phpunit": "^8.0"
+ "phpunit/phpunit": "^8.0 || ^9.0 <9.3"
},
"type": "library",
"extra": {
"spy",
"stub"
],
- "time": "2020-07-08T12:44:21+00:00"
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/1.12.1"
+ },
+ "time": "2020-09-29T09:10:42+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "7.0.10",
+ "version": "7.0.14",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf"
+ "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf",
- "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+ "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
- "php": "^7.2",
+ "php": ">=7.2",
"phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
- "phpunit/php-token-stream": "^3.1.1",
+ "phpunit/php-token-stream": "^3.1.1 || ^4.0",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
"sebastian/environment": "^4.2.2",
"sebastian/version": "^2.0.1",
"testing",
"xunit"
],
- "time": "2019-11-20T13:55:58+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-12-02T13:39:03+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "2.0.2",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "050bedf145a257b1ff02746c31894800e5122946"
+ "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
- "reference": "050bedf145a257b1ff02746c31894800e5122946",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+ "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^7.1"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"filesystem",
"iterator"
],
- "time": "2018-09-13T20:33:42+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:25:21+00:00"
},
{
"name": "phpunit/php-text-template",
"keywords": [
"template"
],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+ },
"time": "2015-06-21T13:50:34+00:00"
},
{
"name": "phpunit/php-timer",
- "version": "2.1.2",
+ "version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
+ "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
- "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+ "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^7.0"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"keywords": [
"timer"
],
- "time": "2019-06-07T04:22:29+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:20:02+00:00"
},
{
"name": "phpunit/php-token-stream",
- "version": "3.1.1",
+ "version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
+ "reference": "472b687829041c24b25f475e14c2f38a09edf1c2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
- "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2",
+ "reference": "472b687829041c24b25f475e14c2f38a09edf1c2",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "php": "^7.1"
+ "php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"keywords": [
"tokenizer"
],
- "time": "2019-09-17T06:23:10+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
+ "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "abandoned": true,
+ "time": "2020-11-30T08:38:46+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "8.5.8",
+ "version": "8.5.13",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997"
+ "reference": "8e86be391a58104ef86037ba8a846524528d784e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997",
- "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e86be391a58104ef86037ba8a846524528d784e",
+ "reference": "8e86be391a58104ef86037ba8a846524528d784e",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.2.0",
+ "doctrine/instantiator": "^1.3.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.9.1",
- "phar-io/manifest": "^1.0.3",
- "phar-io/version": "^2.0.1",
- "php": "^7.2",
- "phpspec/prophecy": "^1.8.1",
- "phpunit/php-code-coverage": "^7.0.7",
+ "myclabs/deep-copy": "^1.10.0",
+ "phar-io/manifest": "^2.0.1",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.2",
+ "phpspec/prophecy": "^1.10.3",
+ "phpunit/php-code-coverage": "^7.0.12",
"phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-timer": "^2.1.2",
"sebastian/comparator": "^3.0.2",
"sebastian/diff": "^3.0.2",
- "sebastian/environment": "^4.2.2",
- "sebastian/exporter": "^3.1.1",
+ "sebastian/environment": "^4.2.3",
+ "sebastian/exporter": "^3.1.2",
"sebastian/global-state": "^3.0.0",
"sebastian/object-enumerator": "^3.0.3",
"sebastian/resource-operations": "^2.0.1",
"testing",
"xunit"
],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.13"
+ },
"funding": [
{
"url": "https://phpunit.de/donate.html",
"type": "github"
}
],
- "time": "2020-06-22T07:06:58+00:00"
+ "time": "2020-12-01T04:53:52+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+ "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v2.8.0"
+ },
+ "time": "2020-05-12T15:16:56+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+ "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+ "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": ">=5.6"
},
"require-dev": {
- "phpunit/phpunit": "^5.7 || ^6.0"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2017-03-04T06:30:41+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:15:22+00:00"
},
{
"name": "sebastian/comparator",
- "version": "3.0.2",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
+ "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
- "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
+ "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
"shasum": ""
},
"require": {
- "php": "^7.1",
+ "php": ">=7.1",
"sebastian/diff": "^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
- "phpunit/phpunit": "^7.1"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ },
{
"name": "Jeff Welch",
{
"name": "Bernhard Schussek",
- },
- {
- "name": "Sebastian Bergmann",
}
],
"description": "Provides the functionality to compare PHP values for equality",
"compare",
"equality"
],
- "time": "2018-07-12T15:12:46+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:04:30+00:00"
},
{
"name": "sebastian/diff",
- "version": "3.0.2",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
+ "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
- "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+ "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.0",
"BSD-3-Clause"
],
"authors": [
- {
- "name": "Kore Nordmann",
- },
{
"name": "Sebastian Bergmann",
+ },
+ {
+ "name": "Kore Nordmann",
}
],
"description": "Diff implementation",
"unidiff",
"unified diff"
],
- "time": "2019-02-04T06:01:07+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:59:04+00:00"
},
{
"name": "sebastian/environment",
- "version": "4.2.3",
+ "version": "4.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
+ "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
- "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+ "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5"
"environment",
"hhvm"
],
- "time": "2019-11-20T08:46:58+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:53:42+00:00"
},
{
"name": "sebastian/exporter",
- "version": "3.1.2",
+ "version": "3.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
+ "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
- "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
+ "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
"shasum": ""
},
"require": {
- "php": "^7.0",
+ "php": ">=7.0",
"sebastian/recursion-context": "^3.0"
},
"require-dev": {
"export",
"exporter"
],
- "time": "2019-09-14T09:02:43+00:00"
- },
- {
- "name": "sebastian/finder-facade",
- "version": "1.2.3",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/finder-facade.git",
- "reference": "167c45d131f7fc3d159f56f191a0a22228765e16"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16",
- "reference": "167c45d131f7fc3d159f56f191a0a22228765e16",
- "shasum": ""
- },
- "require": {
- "php": "^7.1",
- "symfony/finder": "^2.3|^3.0|^4.0|^5.0",
- "theseer/fdomdocument": "^1.6"
- },
- "type": "library",
- "extra": {
- "branch-alias": []
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
+ "funding": [
{
- "name": "Sebastian Bergmann",
- "role": "lead"
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
}
],
- "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
- "homepage": "https://github.com/sebastianbergmann/finder-facade",
- "time": "2020-01-16T08:08:45+00:00"
+ "time": "2020-11-30T07:47:53+00:00"
},
{
"name": "sebastian/global-state",
- "version": "3.0.0",
+ "version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
+ "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
- "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b",
+ "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b",
"shasum": ""
},
"require": {
- "php": "^7.2",
+ "php": ">=7.2",
"sebastian/object-reflector": "^1.1.1",
"sebastian/recursion-context": "^3.0"
},
"keywords": [
"global state"
],
- "time": "2019-02-01T05:30:01+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:43:24+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "3.0.3",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
+ "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
- "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+ "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"shasum": ""
},
"require": {
- "php": "^7.0",
+ "php": ">=7.0",
"sebastian/object-reflector": "^1.1.1",
"sebastian/recursion-context": "^3.0"
},
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
- "time": "2017-08-03T12:35:26+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:40:27+00:00"
},
{
"name": "sebastian/object-reflector",
- "version": "1.1.1",
+ "version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "773f97c67f28de00d397be301821b06708fca0be"
+ "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
- "reference": "773f97c67f28de00d397be301821b06708fca0be",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+ "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
- "time": "2017-03-29T09:07:27+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:37:18+00:00"
},
{
"name": "sebastian/recursion-context",
- "version": "3.0.0",
+ "version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
+ "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
- "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
+ "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0"
"BSD-3-Clause"
],
"authors": [
- {
- "name": "Jeff Welch",
- },
{
"name": "Sebastian Bergmann",
},
+ {
+ "name": "Jeff Welch",
+ },
{
"name": "Adam Harvey",
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2017-03-03T06:23:57+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:34:24+00:00"
},
{
"name": "sebastian/resource-operations",
- "version": "2.0.1",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
+ "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
- "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+ "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
"shasum": ""
},
"require": {
- "php": "^7.1"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2018-10-04T04:07:39+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:30:19+00:00"
},
{
"name": "sebastian/type",
- "version": "1.1.3",
+ "version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+ "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
- "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
+ "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
"shasum": ""
},
"require": {
- "php": "^7.2"
+ "php": ">=7.2"
},
"require-dev": {
"phpunit/phpunit": "^8.2"
],
"description": "Collection of value objects that represent the types of the PHP type system",
"homepage": "https://github.com/sebastianbergmann/type",
- "time": "2019-07-02T08:10:15+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:25:11+00:00"
},
{
"name": "sebastian/version",
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/master"
+ },
"time": "2016-10-03T07:35:21+00:00"
},
{
"name": "seld/jsonlint",
- "version": "1.8.2",
+ "version": "1.8.3",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/jsonlint.git",
- "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337"
+ "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/590cfec960b77fd55e39b7d9246659e95dd6d337",
- "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
+ "reference": "9ad6ce79c342fbd44df10ea95511a1b24dee5b57",
"shasum": ""
},
"require": {
"parser",
"validator"
],
+ "support": {
+ "issues": "https://github.com/Seldaek/jsonlint/issues",
+ "source": "https://github.com/Seldaek/jsonlint/tree/1.8.3"
+ },
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "tidelift"
}
],
- "time": "2020-08-25T06:56:57+00:00"
+ "time": "2020-11-11T09:19:24+00:00"
},
{
"name": "seld/phar-utils",
"keywords": [
"phar"
],
+ "support": {
+ "issues": "https://github.com/Seldaek/phar-utils/issues",
+ "source": "https://github.com/Seldaek/phar-utils/tree/master"
+ },
"time": "2020-07-07T18:42:57+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.5.6",
+ "version": "3.5.8",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
+ "reference": "9d583721a7157ee997f235f327de038e7ea6dac4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
- "reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4",
+ "reference": "9d583721a7157ee997f235f327de038e7ea6dac4",
"shasum": ""
},
"require": {
"phpcs",
"standards"
],
- "time": "2020-08-10T04:50:15+00:00"
+ "support": {
+ "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
+ "source": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
+ },
+ "time": "2020-10-23T02:01:07+00:00"
},
{
"name": "symfony/dom-crawler",
- "version": "v4.4.13",
+ "version": "v4.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "6dd1e7adef4b7efeeb9691fd619279027d4dcf85"
+ "reference": "d44fbb02b458fe18d00fea18f24c97cefb87577e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6dd1e7adef4b7efeeb9691fd619279027d4dcf85",
- "reference": "6dd1e7adef4b7efeeb9691fd619279027d4dcf85",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/d44fbb02b458fe18d00fea18f24c97cefb87577e",
+ "reference": "d44fbb02b458fe18d00fea18f24c97cefb87577e",
"shasum": ""
},
"require": {
"symfony/css-selector": ""
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\DomCrawler\\": ""
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/dom-crawler/tree/v4.4.18"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-12T06:20:35+00:00"
+ "time": "2020-12-18T07:41:31+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v4.4.13",
+ "version": "v5.2.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "27575bcbc68db1f6d06218891296572c9b845704"
+ "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/27575bcbc68db1f6d06218891296572c9b845704",
- "reference": "27575bcbc68db1f6d06218891296572c9b845704",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
+ "reference": "fa8f8cab6b65e2d99a118e082935344c5ba8c60d",
"shasum": ""
},
"require": {
- "php": ">=7.1.3",
+ "php": ">=7.2.5",
"symfony/polyfill-ctype": "~1.8"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.4-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v5.2.1"
+ },
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "tidelift"
}
],
- "time": "2020-08-21T17:19:37+00:00"
- },
- {
- "name": "theseer/fdomdocument",
- "version": "1.6.6",
- "source": {
- "type": "git",
- "url": "https://github.com/theseer/fDOMDocument.git",
- "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
- "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
- "shasum": ""
- },
- "require": {
- "ext-dom": "*",
- "lib-libxml": "*",
- "php": ">=5.3.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Arne Blankerts",
- "role": "lead",
- }
- ],
- "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
- "homepage": "https://github.com/theseer/fDOMDocument",
- "time": "2017-06-30T11:53:12+00:00"
+ "time": "2020-11-30T17:05:38+00:00"
},
{
"name": "theseer/tokenizer",
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/master"
+ },
"funding": [
{
"url": "https://github.com/theseer",
"check",
"validate"
],
- "time": "2020-07-08T17:02:28+00:00"
- },
- {
- "name": "wnx/laravel-stats",
- "version": "v2.0.2",
- "source": {
- "type": "git",
- "url": "https://github.com/stefanzweifel/laravel-stats.git",
- "reference": "e86ebfdd149383b18a41fe3efa1601d82d447140"
+ "support": {
+ "issues": "https://github.com/webmozart/assert/issues",
+ "source": "https://github.com/webmozart/assert/tree/master"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/e86ebfdd149383b18a41fe3efa1601d82d447140",
- "reference": "e86ebfdd149383b18a41fe3efa1601d82d447140",
- "shasum": ""
- },
- "require": {
- "illuminate/console": "~5.8.0|^6.0|^7.0",
- "illuminate/support": "~5.8.0|^6.0|^7.0",
- "php": ">=7.2.0",
- "phploc/phploc": "~5.0|~6.0",
- "symfony/finder": "~4.0"
- },
- "require-dev": {
- "friendsofphp/php-cs-fixer": "^2.15",
- "laravel/browser-kit-testing": "~5.0",
- "laravel/dusk": "~5.0",
- "mockery/mockery": "^1.1",
- "orchestra/testbench": "^3.8|^4.0|^5.0",
- "phpunit/phpunit": "8.*|9.*"
- },
- "type": "library",
- "extra": {
- "laravel": {
- "providers": [
- "Wnx\\LaravelStats\\StatsServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "Wnx\\LaravelStats\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Stefan Zweifel",
- "homepage": "https://stefanzweifel.io",
- "role": "Developer"
- }
- ],
- "description": "Get insights about your Laravel Project",
- "homepage": "https://github.com/stefanzweifel/laravel-stats",
- "keywords": [
- "laravel",
- "statistics",
- "stats",
- "wnx"
- ],
- "time": "2020-02-22T19:09:14+00:00"
+ "time": "2020-07-08T17:02:28+00:00"
}
],
"aliases": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
- "php": "^7.2",
+ "php": "^7.2.5",
"ext-curl": "*",
"ext-dom": "*",
"ext-gd": "*",
"ext-json": "*",
"ext-mbstring": "*",
- "ext-tidy": "*",
"ext-xml": "*"
},
"platform-dev": [],
"platform-overrides": {
- "php": "7.2.0"
+ "php": "7.2.5"
},
- "plugin-api-version": "1.1.0"
+ "plugin-api-version": "2.0.0"
}
];
});
-$factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Bookshelf::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => Str::random(10),
];
});
-$factory->define(\BookStack\Entities\Book::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Book::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => Str::random(10),
];
});
-$factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Chapter::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => Str::random(10),
];
});
-$factory->define(\BookStack\Entities\Page::class, function ($faker) {
+$factory->define(\BookStack\Entities\Models\Page::class, function ($faker) {
$html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
return [
'name' => $faker->sentence,
Schema::dropIfExists('bookshelves');
// Drop related polymorphic items
- DB::table('activities')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
- DB::table('views')->where('viewable_type', '=', 'BookStack\Entities\Bookshelf')->delete();
- DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Entities\Bookshelf')->delete();
- DB::table('tags')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
- DB::table('search_terms')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
- DB::table('comments')->where('entity_type', '=', 'BookStack\Entities\Bookshelf')->delete();
+ DB::table('activities')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+ DB::table('views')->where('viewable_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+ DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+ DB::table('tags')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+ DB::table('search_terms')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
+ DB::table('comments')->where('entity_type', '=', 'BookStack\Entities\Models\Bookshelf')->delete();
}
}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddEntitySoftDeletes extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('bookshelves', function(Blueprint $table) {
+ $table->softDeletes();
+ });
+ Schema::table('books', function(Blueprint $table) {
+ $table->softDeletes();
+ });
+ Schema::table('chapters', function(Blueprint $table) {
+ $table->softDeletes();
+ });
+ Schema::table('pages', function(Blueprint $table) {
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('bookshelves', function(Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ Schema::table('books', function(Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ Schema::table('chapters', function(Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ Schema::table('pages', function(Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateDeletionsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('deletions', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('deleted_by');
+ $table->string('deletable_type', 100);
+ $table->integer('deletable_id');
+ $table->timestamps();
+
+ $table->index('deleted_by');
+ $table->index('deletable_type');
+ $table->index('deletable_id');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('deletions');
+ }
+}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+class SimplifyActivitiesTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('activities', function (Blueprint $table) {
+ $table->renameColumn('key', 'type');
+ $table->renameColumn('extra', 'detail');
+ $table->dropColumn('book_id');
+ $table->integer('entity_id')->nullable()->change();
+ $table->string('entity_type', 191)->nullable()->change();
+ });
+
+ DB::table('activities')
+ ->where('entity_id', '=', 0)
+ ->update([
+ 'entity_id' => null,
+ 'entity_type' => null,
+ ]);
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ DB::table('activities')
+ ->whereNull('entity_id')
+ ->update([
+ 'entity_id' => 0,
+ 'entity_type' => '',
+ ]);
+
+ Schema::table('activities', function (Blueprint $table) {
+ $table->renameColumn('type', 'key');
+ $table->renameColumn('detail', 'extra');
+ $table->integer('book_id');
+
+ $table->integer('entity_id')->change();
+ $table->string('entity_type', 191)->change();
+
+ $table->index('book_id');
+ });
+ }
+}
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Facades\DB;
+
+class AddOwnedByFieldToEntities extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ $tables = ['pages', 'books', 'chapters', 'bookshelves'];
+ foreach ($tables as $table) {
+ Schema::table($table, function (Blueprint $table) {
+ $table->integer('owned_by')->unsigned()->index();
+ });
+
+ DB::table($table)->update(['owned_by' => DB::raw('`created_by`')]);
+ }
+
+ Schema::table('joint_permissions', function (Blueprint $table) {
+ $table->renameColumn('created_by', 'owned_by');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $tables = ['pages', 'books', 'chapters', 'bookshelves'];
+ foreach ($tables as $table) {
+ Schema::table($table, function (Blueprint $table) {
+ $table->dropColumn('owned_by');
+ });
+ }
+
+ Schema::table('joint_permissions', function (Blueprint $table) {
+ $table->renameColumn('owned_by', 'created_by');
+ });
+ }
+}
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
-use BookStack\Entities\SearchService;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\SearchIndex;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
$role = Role::getRole('viewer');
$viewerUser->attachRole($role);
- $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
+ $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];
- factory(\BookStack\Entities\Book::class, 5)->create($byData)
+ factory(\BookStack\Entities\Models\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
$chapters = factory(Chapter::class, 3)->create($byData)
->each(function($chapter) use ($editorUser, $book, $byData){
$book->pages()->saveMany($pages);
});
- $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
+ $largeBook = factory(\BookStack\Entities\Models\Book::class)->create(array_merge($byData, ['name' => 'Large book' . Str::random(10)]));
$pages = factory(Page::class, 200)->make($byData);
$chapters = factory(Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages);
$token->save();
app(PermissionService::class)->buildJointPermissions();
- app(SearchService::class)->indexAllEntities();
+ app(SearchIndex::class)->indexAllEntities();
}
}
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\Role;
use BookStack\Auth\User;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
-use BookStack\Entities\SearchService;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\SearchIndex;
use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
$editorRole = Role::getRole('editor');
$editorUser->attachRole($editorRole);
- $largeBook = factory(\BookStack\Entities\Book::class)->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $largeBook = factory(\BookStack\Entities\Models\Book::class)->create(['name' => 'Large book' . Str::random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = factory(Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(PermissionService::class)->buildJointPermissions();
- app(SearchService::class)->indexAllEntities();
+ app(SearchIndex::class)->indexAllEntities();
}
}
--- /dev/null
+{
+ "book_id": 1,
+ "name": "My API Page",
+ "html": "<p>my new API page</p>",
+ "tags": [
+ {"name": "Category", "value": "Not Bad Content"},
+ {"name": "Rating", "value": "Average"}
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "chapter_id": 1,
+ "name": "My updated API Page",
+ "html": "<p>my new API page - Updated</p>",
+ "tags": [
+ {"name": "Category", "value": "API Examples"},
+ {"name": "Rating", "value": "Alright"}
+ ]
+}
\ No newline at end of file
"tags": [
{
"id": 13,
- "entity_id": 16,
- "entity_type": "BookStack\\Book",
"name": "Category",
"value": "Guide",
- "order": 0,
- "created_at": "2020-01-12 14:11:51",
- "updated_at": "2020-01-12 14:11:51"
+ "order": 0
}
],
"cover": {
{
"name": "Category",
"value": "Guide",
- "order": 0,
- "created_at": "2020-05-22 22:51:51",
- "updated_at": "2020-05-22 22:51:51"
+ "order": 0
}
],
"pages": [
"updated_at": "2019-08-26 14:32:59",
"created_by": 1,
"updated_by": 1,
- "draft": 0,
+ "draft": false,
"revision_count": 2,
- "template": 0
+ "template": false
},
{
"id": 7,
"updated_at": "2019-06-06 12:03:04",
"created_by": 3,
"updated_by": 3,
- "draft": 0,
+ "draft": false,
"revision_count": 1,
- "template": 0
+ "template": false
}
]
}
\ No newline at end of file
--- /dev/null
+{
+ "id": 358,
+ "book_id": 1,
+ "chapter_id": 0,
+ "name": "My API Page",
+ "slug": "my-api-page",
+ "html": "<p id=\"bkmrk-my-new-api-page\">my new API page</p>",
+ "priority": 14,
+ "created_at": "2020-11-28 15:01:39",
+ "updated_at": "2020-11-28 15:01:39",
+ "created_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "updated_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "draft": false,
+ "markdown": "",
+ "revision_count": 1,
+ "template": false,
+ "tags": [
+ {
+ "name": "Category",
+ "value": "Not Bad Content",
+ "order": 0
+ },
+ {
+ "name": "Rating",
+ "value": "Average",
+ "order": 1
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "data": [
+ {
+ "id": 1,
+ "book_id": 1,
+ "chapter_id": 1,
+ "name": "How to create page content",
+ "slug": "how-to-create-page-content",
+ "priority": 0,
+ "draft": false,
+ "template": false,
+ "created_at": "2019-05-05 21:49:58",
+ "updated_at": "2020-07-04 15:50:58",
+ "created_by": 1,
+ "updated_by": 1
+ },
+ {
+ "id": 2,
+ "book_id": 1,
+ "chapter_id": 1,
+ "name": "How to use images",
+ "slug": "how-to-use-images",
+ "priority": 2,
+ "draft": false,
+ "template": false,
+ "created_at": "2019-05-05 21:53:30",
+ "updated_at": "2019-06-06 12:03:04",
+ "created_by": 1,
+ "updated_by": 1
+ },
+ {
+ "id": 3,
+ "book_id": 1,
+ "chapter_id": 1,
+ "name": "Drawings via draw.io",
+ "slug": "drawings-via-drawio",
+ "priority": 3,
+ "draft": false,
+ "template": false,
+ "created_at": "2019-05-05 21:53:49",
+ "updated_at": "2019-12-18 21:56:52",
+ "created_by": 1,
+ "updated_by": 1
+ }
+ ],
+ "total": 322
+}
\ No newline at end of file
--- /dev/null
+{
+ "id": 306,
+ "book_id": 1,
+ "chapter_id": 0,
+ "name": "A page written in markdown",
+ "slug": "a-page-written-in-markdown",
+ "html": "<h1 id=\"bkmrk-how-this-is-built\">How this is built</h1>\r\n<p id=\"bkmrk-this-page-is-written\">This page is written in markdown. BookStack stores the page data in HTML.</p>\r\n<p id=\"bkmrk-here%27s-a-cute-pictur\">Here's a cute picture of my cat:</p>\r\n<p id=\"bkmrk-\"><a href=\"http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg\"><img src=\"http://example.com/uploads/images/gallery/2020-04/scaled-1680-/yXSrubes.jpg\" alt=\"yXSrubes.jpg\"></a></p>",
+ "priority": 13,
+ "created_at": "2020-02-02 21:40:38",
+ "updated_at": "2020-11-28 14:43:20",
+ "created_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "updated_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "draft": false,
+ "markdown": "# How this is built\r\n\r\nThis page is written in markdown. BookStack stores the page data in HTML.\r\n\r\nHere's a cute picture of my cat:\r\n\r\n[](http://example.com/uploads/images/gallery/2020-04/yXSrubes.jpg)",
+ "revision_count": 5,
+ "template": false,
+ "tags": [
+ {
+ "name": "Category",
+ "value": "Top Content",
+ "order": 0
+ },
+ {
+ "name": "Animal",
+ "value": "Cat",
+ "order": 1
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "id": 361,
+ "book_id": 1,
+ "chapter_id": 1,
+ "name": "My updated API Page",
+ "slug": "my-updated-api-page",
+ "html": "<p id=\"bkmrk-my-new-api-page---up\">my new API page - Updated</p>",
+ "priority": 16,
+ "created_at": "2020-11-28 15:10:54",
+ "updated_at": "2020-11-28 15:13:03",
+ "created_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "updated_by": {
+ "id": 1,
+ "name": "Admin"
+ },
+ "draft": false,
+ "markdown": "",
+ "revision_count": 5,
+ "template": false,
+ "tags": [
+ {
+ "name": "Category",
+ "value": "API Examples",
+ "order": 0
+ },
+ {
+ "name": "Rating",
+ "value": "Alright",
+ "order": 0
+ }
+ ]
+}
\ No newline at end of file
"tags": [
{
"id": 16,
- "entity_id": 14,
- "entity_type": "BookStack\\Bookshelf",
"name": "Category",
"value": "Guide",
- "order": 0,
- "created_at": "2020-04-10 13:31:04",
- "updated_at": "2020-04-10 13:31:04"
+ "order": 0
}
],
"cover": {
if [[ -n "$1" ]]; then
exec "$@"
else
+ composer install
wait-for-it db:3306 -t 45
php artisan migrate --database=mysql
chown -R www-data:www-data storage
exec apache2-foreground
-fi
\ No newline at end of file
+fi
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
- bootstrap="bootstrap/init.php"
+ bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
<server name="APP_LANG" value="en"/>
<server name="APP_THEME" value="none"/>
<server name="APP_AUTO_LANG_PUBLIC" value="true"/>
+ <server name="APP_URL" value="http://bookstack.dev"/>
+ <server name="ALLOWED_IFRAME_HOSTS" value=""/>
<server name="CACHE_DRIVER" value="array"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
<server name="AVATAR_URL" value=""/>
<server name="LDAP_VERSION" value="3"/>
+ <server name="SESSION_SECURE_COOKIE" value="null"/>
<server name="STORAGE_TYPE" value="local"/>
<server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
<server name="STORAGE_IMAGE_TYPE" value="local"/>
<server name="GOOGLE_AUTO_REGISTER" value=""/>
<server name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
<server name="GOOGLE_SELECT_ACCOUNT" value=""/>
- <server name="APP_URL" value="http://bookstack.dev"/>
<server name="DEBUGBAR_ENABLED" value="false"/>
<server name="SAML2_ENABLED" value="false"/>
<server name="API_REQUESTS_PER_MIN" value="180"/>
/*
|--------------------------------------------------------------------------
-| Initialize The App
+| Register The Auto Loader
|--------------------------------------------------------------------------
|
-| We need to get things going before we start up the app.
-| The init file loads everything in, in the correct order.
+| Composer provides a convenient, automatically generated class loader for
+| our application. We just need to utilize it! We'll simply require it
+| into the script here so that we don't have to worry about manual
+| loading any of our classes later on. It feels great to relax.
|
*/
-require __DIR__.'/../bootstrap/init.php';
+require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
-For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
+For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
## 🛠️ Development & Testing
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
-* [Node.js](https://nodejs.org/en/) v10.0+
+* [Node.js](https://nodejs.org/en/) v12.0+
This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks:
If all the conditions are met, you can proceed with the following steps:
-1. Install PHP/Composer dependencies with **`docker-compose run app composer install`** (first time can take a while because the image has to be built).
-2. **Copy `.env.example` to `.env`** and change `APP_KEY` to a random 32 char string.
-3. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.
-4. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.
-5. **Run `docker-compose up`** and wait until all database migrations have been done.
-6. You can now login with `
[email protected]` and `password` as password on `localhost:8080` (or another port if specified).
+1. **Copy `.env.example` to `.env`**, change `APP_KEY` to a random 32 char string and set `APP_ENV` to `local`.
+2. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.
+3. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.
+4. **Run `docker-compose up`** and wait until the image is built and all database migrations have been done.
+5. You can now login with `
[email protected]` and `password` as password on `localhost:8080` (or another port if specified).
If needed, You'll be able to run any artisan commands via docker-compose like so:
Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
-If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
+If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
* [diagrams.net](https://github.com/jgraph/drawio)
-* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
-* [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
\ No newline at end of file
+* [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
+++ /dev/null
-
-
-class BreadcrumbListing {
-
- constructor(elem) {
- this.elem = elem;
- this.searchInput = elem.querySelector('input');
- this.loadingElem = elem.querySelector('.loading-container');
- this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
-
- // this.loadingElem.style.display = 'none';
- const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
- this.entityType = entityDescriptor[0];
- this.entityId = Number(entityDescriptor[1]);
-
- this.elem.addEventListener('show', this.onShow.bind(this));
- this.searchInput.addEventListener('input', this.onSearch.bind(this));
- }
-
- onShow() {
- this.loadEntityView();
- }
-
- onSearch() {
- const input = this.searchInput.value.toLowerCase().trim();
- const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
- for (let listItem of listItems) {
- const match = !input || listItem.textContent.toLowerCase().includes(input);
- listItem.style.display = match ? 'flex' : 'none';
- listItem.classList.toggle('hidden', !match);
- }
- }
-
- loadEntityView() {
- this.toggleLoading(true);
-
- const params = {
- 'entity_id': this.entityId,
- 'entity_type': this.entityType,
- };
-
- window.$http.get('/search/entity/siblings', params).then(resp => {
- this.entityListElem.innerHTML = resp.data;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.toggleLoading(false);
- this.onSearch();
- });
- }
-
- toggleLoading(show = false) {
- this.loadingElem.style.display = show ? 'block' : 'none';
- }
-
-}
-
-export default BreadcrumbListing;
\ No newline at end of file
--- /dev/null
+import {debounce} from "../services/util";
+
+class DropdownSearch {
+
+ setup() {
+ this.elem = this.$el;
+ this.searchInput = this.$refs.searchInput;
+ this.loadingElem = this.$refs.loading;
+ this.listContainerElem = this.$refs.listContainer;
+
+ this.localSearchSelector = this.$opts.localSearchSelector;
+ this.url = this.$opts.url;
+
+ this.elem.addEventListener('show', this.onShow.bind(this));
+ this.searchInput.addEventListener('input', this.onSearch.bind(this));
+
+ this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false);
+ }
+
+ onShow() {
+ this.loadList();
+ }
+
+ onSearch() {
+ const input = this.searchInput.value.toLowerCase().trim();
+ if (this.localSearchSelector) {
+ this.runLocalSearch(input);
+ } else {
+ this.toggleLoading(true);
+ this.runAjaxSearch(input);
+ }
+ }
+
+ runAjaxSearch(searchTerm) {
+ this.loadList(searchTerm);
+ }
+
+ runLocalSearch(searchTerm) {
+ const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
+ for (let listItem of listItems) {
+ const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
+ listItem.style.display = match ? 'flex' : 'none';
+ listItem.classList.toggle('hidden', !match);
+ }
+ }
+
+ async loadList(searchTerm = '') {
+ this.listContainerElem.innerHTML = '';
+ this.toggleLoading(true);
+
+ try {
+ const resp = await window.$http.get(this.getAjaxUrl(searchTerm));
+ this.listContainerElem.innerHTML = resp.data;
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.toggleLoading(false);
+ if (this.localSearchSelector) {
+ this.onSearch();
+ }
+ }
+
+ getAjaxUrl(searchTerm = null) {
+ if (!searchTerm) {
+ return this.url;
+ }
+
+ const joiner = this.url.includes('?') ? '&' : '?';
+ return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`;
+ }
+
+ toggleLoading(show = false) {
+ this.loadingElem.style.display = show ? 'block' : 'none';
+ }
+
+}
+
+export default DropdownSearch;
\ No newline at end of file
this.body = document.body;
this.showing = false;
this.setupListeners();
+ this.hide = this.hide.bind(this);
}
show(event = null) {
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
-import breadcrumbListing from "./breadcrumb-listing.js"
import chapterToggle from "./chapter-toggle.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
+import dropdownSearch from "./dropdown-search.js"
import dropzone from "./dropzone.js"
import editorToolbox from "./editor-toolbox.js"
import entityPermissionsEditor from "./entity-permissions-editor.js"
import templateManager from "./template-manager.js"
import toggleSwitch from "./toggle-switch.js"
import triLayout from "./tri-layout.js"
+import userSelect from "./user-select.js"
import wysiwygEditor from "./wysiwyg-editor.js"
const componentMapping = {
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,
- "breadcrumb-listing": breadcrumbListing,
"chapter-toggle": chapterToggle,
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
+ "dropdown-search": dropdownSearch,
"dropzone": dropzone,
"editor-toolbox": editorToolbox,
"entity-permissions-editor": entityPermissionsEditor,
"template-manager": templateManager,
"toggle-switch": toggleSwitch,
"tri-layout": triLayout,
+ "user-select": userSelect,
"wysiwyg-editor": wysiwygEditor,
};
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
+ this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
const newContent = `[](${resp.data.url})`;
replaceContent(placeHolderText, newContent);
}).catch(err => {
- window.$events.emit('error', trans('errors.image_upload_error'));
+ window.$events.emit('error', context.imageUploadErrorText);
replaceContent(placeHolderText, selectedText);
console.log(err);
});
this.cm.focus();
DrawIO.close();
}).catch(err => {
- window.$events.emit('error', trans('errors.image_upload_error'));
+ window.$events.emit('error', this.imageUploadErrorText);
console.log(err);
});
});
--- /dev/null
+import {onChildEvent} from "../services/dom";
+
+class UserSelect {
+
+ setup() {
+
+ this.input = this.$refs.input;
+ this.userInfoContainer = this.$refs.userInfo;
+
+ this.hide = this.$el.components.dropdown.hide;
+
+ onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+ }
+
+ selectUser(event, userEl) {
+ const id = userEl.getAttribute('data-id');
+ this.input.value = id;
+ this.userInfoContainer.innerHTML = userEl.innerHTML;
+ this.hide();
+ }
+
+}
+
+export default UserSelect;
\ No newline at end of file
editor.dom.replace(newEl, id);
}).catch(err => {
editor.dom.remove(id);
- window.$events.emit('error', trans('errors.image_upload_error'));
+ window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
});
}, 10);
});
}
-function drawIoPlugin(drawioUrl, isDarkMode, pageId) {
+function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
let pageEditor = null;
let currentNode = null;
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
} catch (err) {
- window.$events.emit('error', trans('errors.image_upload_error'));
+ window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
}
return;
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
} catch (err) {
pageEditor.dom.remove(id);
- window.$events.emit('error', trans('errors.image_upload_error'));
+ window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
}
}, 5);
class WysiwygEditor {
-
setup() {
this.elem = this.$el;
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
+ this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.isDarkMode = document.documentElement.classList.contains('dark-mode');
this.plugins = "image table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
const drawioUrlElem = document.querySelector('[drawio-url]');
if (drawioUrlElem) {
const url = drawioUrlElem.getAttribute('drawio-url');
- drawIoPlugin(url, this.isDarkMode, this.pageId);
+ drawIoPlugin(url, this.isDarkMode, this.pageId, this);
this.plugins += ' drawio';
}
// Other
'commented_on' => 'commented on',
+ 'permissions_update' => 'updated permissions',
];
'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user',
+ 'meta_owned_name' => 'Owned by :user',
'entity_select' => 'Entity Select',
'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts',
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
'permissions_enable' => 'Enable Custom Permissions',
'permissions_save' => 'Save Permissions',
+ 'permissions_owner' => 'Owner',
// Search
'search_results' => 'Search Results',
'chapters_create' => 'Create New Chapter',
'chapters_delete' => 'Delete Chapter',
'chapters_delete_named' => 'Delete Chapter :chapterName',
- 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages will be removed and added directly to the parent book.',
+ 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
'chapters_edit' => 'Edit Chapter',
'chapters_edit_named' => 'Edit Chapter :chapterName',
'maint' => 'Maintenance',
'maint_image_cleanup' => 'Cleanup Images',
'maint_image_cleanup_desc' => "Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.",
- 'maint_image_cleanup_ignore_revisions' => 'Ignore images in revisions',
+ 'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
'maint_image_cleanup_run' => 'Run Cleanup',
'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
'maint_send_test_email_mail_subject' => 'Test Email',
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
+ 'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
+ 'maint_recycle_bin_open' => 'Open Recycle Bin',
+
+ // Recycle Bin
+ 'recycle_bin' => 'Recycle Bin',
+ 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
+ 'recycle_bin_deleted_item' => 'Deleted Item',
+ 'recycle_bin_deleted_by' => 'Deleted By',
+ 'recycle_bin_deleted_at' => 'Deletion Time',
+ 'recycle_bin_permanently_delete' => 'Permanently Delete',
+ 'recycle_bin_restore' => 'Restore',
+ 'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
+ 'recycle_bin_empty' => 'Empty Recycle Bin',
+ 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',
+ 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?',
+ 'recycle_bin_destroy_list' => 'Items to be Destroyed',
+ 'recycle_bin_restore_list' => 'Items to be Restored',
+ 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',
+ 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',
+ 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
+ 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
// Audit Log
'audit' => 'Audit Log',
'audit_deleted_item_name' => 'Name: :name',
'audit_table_user' => 'User',
'audit_table_event' => 'Event',
- 'audit_table_item' => 'Related Item',
+ 'audit_table_related' => 'Related Item or Detail',
'audit_table_date' => 'Activity Date',
'audit_date_from' => 'Date Range From',
'audit_date_to' => 'Date Range To',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
+ 'users_latest_activity' => 'Latest Activity',
'users_details' => 'User Details',
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
- 'users_delete_success' => 'Users successfully removed',
+ 'users_migrate_ownership' => 'Migrate Ownership',
+ 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
+ 'users_none_selected' => 'No user selected',
+ 'users_delete_success' => 'User successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
+ 'safe_url' => 'The provided link may not be safe.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
.sticky-sidebar {
position: sticky;
top: $-m;
+ max-height: calc(100vh - #{$-m});
+ overflow-y: auto;
}
.bg-chapter {
background-color: var(--color-chapter);
}
-.bg-shelf {
+.bg-bookshelf {
background-color: var(--color-bookshelf);
}
.template-item-actions button:first-child {
border-top: 0;
}
+}
+
+.dropdown-search-dropdown {
+ box-shadow: $bs-med;
+ overflow: hidden;
+ min-height: 100px;
+ width: 240px;
+ display: none;
+ position: absolute;
+ z-index: 80;
+ right: -$-m;
+ @include rtl {
+ right: auto;
+ left: -$-m;
+ }
+ .dropdown-search-search .svg-icon {
+ position: absolute;
+ left: $-s;
+ @include rtl {
+ right: $-s;
+ left: auto;
+ }
+ top: 11px;
+ fill: #888;
+ pointer-events: none;
+ }
+ .dropdown-search-list {
+ max-height: 400px;
+ overflow-y: scroll;
+ text-align: start;
+ }
+ .dropdown-search-item {
+ padding: $-s $-m;
+ &:hover,&:focus {
+ background-color: #F2F2F2;
+ text-decoration: none;
+ }
+ }
+ input {
+ padding-inline-start: $-xl;
+ border-radius: 0;
+ border: 0;
+ border-bottom: 1px solid #DDD;
+ }
+}
+
+@include smaller-than($m) {
+ .dropdown-search-dropdown {
+ position: fixed;
+ right: auto;
+ left: $-m;
+ }
+ .dropdown-search-dropdown .dropdown-search-list {
+ max-height: 240px;
+ }
+}
+
+.custom-select-input {
+ max-width: 280px;
+ border: 1px solid #DDD;
+ border-radius: 4px;
}
\ No newline at end of file
}
}
-.breadcrumb-listing {
+.dropdown-search {
position: relative;
- .breadcrumb-listing-toggle {
+ .dropdown-search-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
}
}
-.breadcrumb-listing-dropdown {
- box-shadow: $bs-med;
- overflow: hidden;
- min-height: 100px;
- width: 240px;
- display: none;
- position: absolute;
- z-index: 80;
- right: -$-m;
- @include rtl {
- right: auto;
- left: -$-m;
- }
- .breadcrumb-listing-search .svg-icon {
- position: absolute;
- left: $-s;
- @include rtl {
- right: $-s;
- left: auto;
- }
- top: 11px;
- fill: #888;
- pointer-events: none;
- }
- .breadcrumb-listing-entity-list {
- max-height: 400px;
- overflow-y: scroll;
- text-align: start;
- }
- input {
- padding-inline-start: $-xl;
- border-radius: 0;
- border: 0;
- border-bottom: 1px solid #DDD;
- }
-}
-
-@include smaller-than($m) {
- .breadcrumb-listing-dropdown {
- position: fixed;
- right: auto;
- left: $-m;
- }
- .breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
- max-height: 240px;
- }
-}
-
.faded {
a, button, span, span > div {
color: #666;
.justify-flex-end {
justify-content: flex-end;
}
+.justify-center {
+ justify-content: center;
+}
+.items-center {
+ align-items: center;
+}
/**
* Display and float utilities
*/
.block {
- display: block;
+ display: block !important;
position: relative;
}
.inline {
- display: inline;
+ display: inline !important;
}
.block.inline {
- display: inline-block;
+ display: inline-block !important;
}
.hidden {
display: none !important;
}
+.fill-height {
+ height: 100%;
+}
+
.float {
float: left;
&.right {
min-height: 50vh;
overflow-y: scroll;
overflow-x: hidden;
+ height: 100%;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
margin-inline-start: 0;
margin-inline-end: 0;
}
-}
\ No newline at end of file
+}
overflow-wrap: break-word;
}
-.limit-text {
+.text-limit-lines-1 {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+.text-limit-lines-2 {
+ // -webkit use here is actually standardised cross-browser:
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+}
+
/**
* Grouping
*/
}
}
-table a.audit-log-user {
+table.table .table-user-item {
display: grid;
grid-template-columns: 42px 1fr;
align-items: center;
}
-table a.icon-list-item {
+table.table .table-entity-item {
display: grid;
grid-template-columns: 36px 1fr;
align-items: center;
<ul class="contents">
@foreach($bookChildren as $bookChild)
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
- @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
+ @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
<ul>
- @foreach($bookChild->pages as $page)
+ @foreach($bookChild->visible_pages as $page)
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
@endforeach
</ul>
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
- @if(count($bookChild->pages) > 0)
- @foreach($bookChild->pages as $page)
+ @if(count($bookChild->visible_pages) > 0)
+ @foreach($bookChild->visible_pages as $page)
<div class="page-break"></div>
<div class="chapter-hint">{{$bookChild->name}}</div>
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
+++ /dev/null
-<a href="{{$book->getUrl()}}" class="grid-card" data-entity-type="book" data-entity-id="{{$book->id}}">
- <div class="bg-book featured-image-container-wrap">
- <div class="featured-image-container" @if($book->cover) style="background-image: url('{{ $book->getBookCover() }}')"@endif>
- </div>
- @icon('book')
- </div>
- <div class="grid-card-content">
- <h2>{{$book->getShortName(35)}}</h2>
- @if(isset($book->searchSnippet))
- <p class="text-muted">{!! $book->searchSnippet !!}</p>
- @else
- <p class="text-muted">{{ $book->getExcerpt(130) }}</p>
- @endif
- </div>
- <div class="grid-card-footer text-muted ">
- <p>@icon('star')<span title="{{$book->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $book->created_at->diffForHumans()]) }}</span></p>
- <p>@icon('edit')<span title="{{ $book->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $book->updated_at->diffForHumans()]) }}</span></p>
- </div>
-</a>
\ No newline at end of file
-
<main class="content-wrap mt-m card">
<div class="grid half v-center no-row-gap">
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
@else
<div class="grid third">
@foreach($books as $key => $book)
- @include('books.grid-item', ['book' => $book])
+ @include('partials.entity-grid-item', ['entity' => $book])
@endforeach
</div>
@endif
<ul class="sortable-page-list sort-list">
@foreach($bookChildren as $bookChild)
- <li class="text-{{ $bookChild->getClassName() }}"
- data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getClassName() }}"
+ <li class="text-{{ $bookChild->getType() }}"
+ data-id="{{$bookChild->id}}" data-type="{{ $bookChild->getType() }}"
data-name="{{ $bookChild->name }}" data-created="{{ $bookChild->created_at->timestamp }}"
data-updated="{{ $bookChild->updated_at->timestamp }}">
<div class="entity-list-item">
</div>
@if($bookChild->isA('chapter'))
<ul>
- @foreach($bookChild->pages as $page)
+ @foreach($bookChild->visible_pages as $page)
<li class="text-page"
data-id="{{$page->id}}" data-type="page"
data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
<div class="chapter-child-menu">
<button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
class="text-muted @if($isOpen) open @endif">
- @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
+ @icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->visible_pages->count()) }}</span>
</button>
<ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
- @foreach($bookChild->pages as $childPage)
+ @foreach($bookChild->visible_pages as $childPage)
<li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
@include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
</li>
-<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->hasChildren()) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
+{{--This view display child pages in a list if pre-loaded onto a 'visible_pages' property,--}}
+{{--To ensure that the pages have been loaded efficiently with permissions taken into account.--}}
+<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->visible_pages->count() > 0) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
<span class="icon text-chapter">@icon('chapter')</span>
<div class="content">
<h4 class="entity-list-item-name break-text">{{ $chapter->name }}</h4>
</div>
</div>
</a>
-@if ($chapter->hasChildren())
+@if ($chapter->visible_pages->count() > 0)
<div class="chapter chapter-expansion">
<span class="icon text-chapter">@icon('page')</span>
<div class="content">
<button type="button" chapter-toggle
aria-expanded="false"
- class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></button>
+ class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->visible_pages->count()) }}</span></button>
<div class="inset-list">
<div class="entity-list-item-children">
- @include('partials.entity-list', ['entities' => $chapter->pages])
+ @include('partials.entity-list', ['entities' => $chapter->visible_pages])
</div>
</div>
</div>
<div class="links text-center">
@if (hasAppAccess())
<a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
- @if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
+ @if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
<div page-picker>
<div class="input-base">
<span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
- <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Page::find($value)->name : '' }}</a>
+ <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
</div>
<br>
<input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
--- /dev/null
+@foreach($users as $user)
+ <a href="#" class="flex-container-row items-center dropdown-search-item" data-id="{{ $user->id }}">
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ </a>
+@endforeach
\ No newline at end of file
--- /dev/null
+<div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select"
+ option:dropdown-search:url="/search/users/select"
+>
+ <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id ?? '' }}">
+ <div refs="dropdown@toggle"
+ class="dropdown-search-toggle flex-container-row items-center"
+ aria-haspopup="true" aria-expanded="false" tabindex="0">
+ <div refs="user-select@user-info" class="flex-container-row items-center px-s">
+ @if($user)
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ @else
+ <span>{{ trans('settings.users_none_selected') }}</span>
+ @endif
+ </div>
+ <span style="font-size: 1.5rem; margin-left: auto;">
+ @icon('caret-down')
+ </span>
+ </div>
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
+ @icon('search')
+ <input refs="dropdown-search@searchInput"
+ aria-label="{{ trans('common.search') }}"
+ autocomplete="off"
+ placeholder="{{ trans('common.search') }}"
+ type="text">
+ </div>
+ <div refs="dropdown-search@loading" class="text-center">
+ @include('partials.loading-icon')
+ </div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div>
+ </div>
+</div>
\ No newline at end of file
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- <p class="mb-none">{{ trans('entities.permissions_intro') }}</p>
-
- <div class="form-group">
- @include('form.checkbox', [
- 'name' => 'restricted',
- 'label' => trans('entities.permissions_enable'),
- ])
+ <div class="grid half left-focus v-center">
+ <div>
+ <p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p>
+ <div>
+ @include('form.checkbox', [
+ 'name' => 'restricted',
+ 'label' => trans('entities.permissions_enable'),
+ ])
+ </div>
+ </div>
+ <div>
+ <div class="form-group">
+ <label for="owner">{{ trans('entities.permissions_owner') }}</label>
+ @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
+ </div>
+ </div>
</div>
+ <hr>
+
<table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
<tr>
<th>{{ trans('common.role') }}</th>
@section('content')
- <div class="flex-fill flex">
+ <div class="flex-fill flex fill-height">
<form action="{{ $page->getUrl() }}" autocomplete="off" data-page-id="{{ $page->id }}" method="POST" class="flex flex-fill">
{{ csrf_field() }}
<div id="markdown-editor" component="markdown-editor"
option:markdown-editor:page-id="{{ $model->id ?? 0 }}"
option:markdown-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}"
+ option:markdown-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}"
class="flex-fill flex code-fill">
- @exposeTranslations([
- 'errors.image_upload_error',
- ])
<div class="markdown-editor-wrap active">
<div class="editor-toolbar">
<div class="sidebar-page-nav menu">
@foreach($pageNav as $navItem)
<li class="page-nav-item h{{ $navItem['level'] }}">
- <a href="{{ $navItem['link'] }}" class="limit-text block">{{ $navItem['text'] }}</a>
+ <a href="{{ $navItem['link'] }}" class="text-limit-lines-1 block">{{ $navItem['text'] }}</a>
<div class="primary-background sidebar-page-nav-bullet"></div>
</li>
@endforeach
<div component="wysiwyg-editor"
option:wysiwyg-editor:page-id="{{ $model->id ?? 0 }}"
option:wysiwyg-editor:text-direction="{{ config('app.rtl') ? 'rtl' : 'ltr' }}"
+ option:wysiwyg-editor:image-upload-error-text="{{ trans('errors.image_upload_error') }}"
class="flex-fill flex">
- @exposeTranslations([
- 'errors.image_upload_error',
- ])
-
<textarea id="html-editor" name="html" rows="5"
@if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>
</div>
{{ $activity->getText() }}
- @if($activity->entity)
+ @if($activity->entity && is_null($activity->entity->deleted_at))
<a href="{{ $activity->entity->getUrl() }}">{{ $activity->entity->name }}</a>
@endif
+ @if($activity->entity && !is_null($activity->entity->deleted_at))
+ "{{ $activity->entity->name }}"
+ @endif
+
@if($activity->extra) "{{ $activity->extra }}" @endif
<br>
@endif
@foreach($sidebarTree as $bookChild)
- <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
+ <li class="list-item-{{ $bookChild->getType() }} {{ $bookChild->getType() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
@include('partials.entity-list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
- @if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
+ @if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
<div class="entity-list-item no-hover">
<span role="presentation" class="icon text-chapter"></span>
<div class="content">
-<div class="breadcrumb-listing" component="dropdown" breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
- <div class="breadcrumb-listing-toggle" refs="dropdown@toggle"
+<div class="dropdown-search" components="dropdown dropdown-search"
+ option:dropdown-search:url="/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}"
+ option:dropdown-search:local-search-selector=".entity-list-item"
+>
+ <div class="dropdown-search-toggle" refs="dropdown@toggle"
aria-haspopup="true" aria-expanded="false" tabindex="0">
<div class="separator">@icon('chevron-right')</div>
</div>
- <div refs="dropdown@menu" class="breadcrumb-listing-dropdown card" role="menu">
- <div class="breadcrumb-listing-search">
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
@icon('search')
- <input autocomplete="off" type="text" name="entity-search" placeholder="{{ trans('common.search') }}" aria-label="{{ trans('common.search') }}">
+ <input refs="dropdown-search@searchInput"
+ aria-label="{{ trans('common.search') }}"
+ autocomplete="off"
+ placeholder="{{ trans('common.search') }}"
+ type="text">
</div>
- @include('partials.loading-icon')
- <div class="breadcrumb-listing-entity-list px-m"></div>
+ <div refs="dropdown-search@loading">
+ @include('partials.loading-icon')
+ </div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div>
</div>
</div>
\ No newline at end of file
<?php $breadcrumbCount = 0; ?>
{{-- Show top level books item --}}
- @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Book)
+ @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Book)
<a href="{{ url('/books') }}" class="text-book icon-list-item outline-hover">
<span>@icon('books')</span>
<span>{{ trans('entities.books') }}</span>
@endif
{{-- Show top level shelves item --}}
- @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Bookshelf)
+ @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Models\Bookshelf)
<a href="{{ url('/shelves') }}" class="text-bookshelf icon-list-item outline-hover">
<span>@icon('bookshelf')</span>
<span>{{ trans('entities.shelves') }}</span>
@endif
@foreach($crumbs as $key => $crumb)
- <?php $isEntity = ($crumb instanceof \BookStack\Entities\Entity); ?>
+ <?php $isEntity = ($crumb instanceof \BookStack\Entities\Models\Entity); ?>
@if (is_null($crumb))
<?php continue; ?>
--- /dev/null
+<?php $type = $entity->getType(); ?>
+<div class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item no-hover">
+ <span role="presentation" class="icon text-{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}}">@icon($type)</span>
+ <div class="content">
+ <div class="entity-list-item-name break-text">{{ $entity->name }}</div>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+<a href="{{ $entity->getUrl() }}" class="grid-card"
+ data-entity-type="{{ $entity->getType() }}" data-entity-id="{{ $entity->id }}">
+ <div class="bg-{{ $entity->getType() }} featured-image-container-wrap">
+ <div class="featured-image-container" @if($entity->cover) style="background-image: url('{{ $entity->getBookCover() }}')"@endif>
+ </div>
+ @icon($entity->getType())
+ </div>
+ <div class="grid-card-content">
+ <h2 class="text-limit-lines-2">{{ $entity->name }}</h2>
+ <p class="text-muted">{{ $entity->getExcerpt(130) }}</p>
+ </div>
+ <div class="grid-card-footer text-muted ">
+ <p>@icon('star')<span title="{{ $entity->created_at->toDayDateTimeString() }}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span></p>
+ <p>@icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span></p>
+ </div>
+</a>
\ No newline at end of file
<div class="entity-meta">
@if($entity->isA('revision'))
- @icon('history'){{ trans('entities.pages_revision') }}
- {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
- <br>
+ <div>
+ @icon('history'){{ trans('entities.pages_revision') }}
+ {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
+ </div>
@endif
@if ($entity->isA('page'))
- @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
- @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>
+ <div>
+ @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
+ @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
@if (userCan('page-update', $entity))</a>@endif
+ </div>
@endif
+ @if ($entity->ownedBy && $entity->ownedBy->id !== $entity->createdBy->id)
+ <div>
+ @icon('user'){!! trans('entities.meta_owned_name', [
+ 'user' => "<a href='{$entity->ownedBy->getProfileUrl()}'>".e($entity->ownedBy->name). "</a>"
+ ]) !!}
+ </div>
+ @endif
@if ($entity->createdBy)
- @icon('star'){!! trans('entities.meta_created_name', [
+ <div>
+ @icon('star'){!! trans('entities.meta_created_name', [
'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
- 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"
+ 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".e($entity->createdBy->name). "</a>"
]) !!}
+ </div>
@else
- @icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
+ <div>
+ @icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
+ </div>
@endif
- <br>
-
@if ($entity->updatedBy)
- @icon('edit'){!! trans('entities.meta_updated_name', [
+ <div>
+ @icon('edit'){!! trans('entities.meta_updated_name', [
'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
- 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"
+ 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".e($entity->updatedBy->name). "</a>"
]) !!}
+ </div>
@elseif (!$entity->isA('revision'))
- @icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
+ <div>
+ @icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
+ </div>
@endif
</div>
\ No newline at end of file
--- /dev/null
+{{--
+$user - User mode to display, Can be null.
+$user_id - Id of user to show. Must be provided.
+--}}
+@if($user)
+ <a href="{{ $user->getEditUrl() }}" class="table-user-item">
+ <div><img class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
+ <div>{{ $user->name }}</div>
+ </a>
+@else
+ [ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
+@endif
\ No newline at end of file
+++ /dev/null
-<div class="page-list">
- @if(count($pages) > 0)
- @foreach($pages as $pageIndex => $page)
- <div class="anim searchResult" style="animation-delay: {{$pageIndex*50 . 'ms'}};">
- @include('pages.list-item', ['page' => $page])
- <hr>
- </div>
- @endforeach
- @else
- <p class="text-muted">{{ trans('entities.search_no_pages') }}</p>
- @endif
-</div>
-
-@if(count($chapters) > 0)
- <div class="page-list">
- @foreach($chapters as $chapterIndex => $chapter)
- <div class="anim searchResult" style="animation-delay: {{($chapterIndex+count($pages))*50 . 'ms'}};">
- @include('chapters.list-item', ['chapter' => $chapter, 'hidePages' => true])
- <hr>
- </div>
- @endforeach
- </div>
-@endif
-
<button refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
<ul refs="dropdown@menu" class="dropdown-menu">
<li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
- @foreach($activityKeys as $key)
- <li @if($key === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $key]) }}">{{ $key }}</a></li>
+ @foreach($activityTypes as $type)
+ <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}">{{ $type }}</a></li>
@endforeach
</ul>
</div>
<th>
<a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'key']) }}">{{ trans('settings.audit_table_event') }}</a>
</th>
- <th>{{ trans('settings.audit_table_item') }}</th>
+ <th>{{ trans('settings.audit_table_related') }}</th>
<th>
<a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'created_at']) }}">{{ trans('settings.audit_table_date') }}</a></th>
</tr>
@foreach($activities as $activity)
<tr>
<td>
- @if($activity->user)
- <a href="{{ $activity->user->getEditUrl() }}" class="audit-log-user">
- <div><img class="avatar block" src="{{ $activity->user->getAvatar(40)}}" alt="{{ $activity->user->name }}"></div>
- <div>{{ $activity->user->name }}</div>
- </a>
- @else
- [ID: {{ $activity->user_id }}] {{ trans('common.deleted_user') }}
- @endif
+ @include('partials.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
</td>
- <td>{{ $activity->key }}</td>
+ <td>{{ $activity->type }}</td>
<td>
@if($activity->entity)
- <a href="{{ $activity->entity->getUrl() }}" class="icon-list-item">
+ <a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
<span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
<div class="text-{{ $activity->entity->getType() }}">
{{ $activity->entity->name }}
</div>
</a>
- @elseif($activity->extra)
+ @elseif($activity->detail && $activity->isForEntity())
<div class="px-m">
{{ trans('settings.audit_deleted_item') }} <br>
- {{ trans('settings.audit_deleted_item_name', ['name' => $activity->extra]) }}
+ {{ trans('settings.audit_deleted_item_name', ['name' => $activity->detail]) }}
</div>
+ @elseif($activity->detail)
+ <div class="px-m">{{ $activity->detail }}</div>
@endif
</td>
<td>{{ $activity->created_at }}</td>
@include('settings.navbar-with-version', ['selected' => 'maintenance'])
+ <div class="card content-wrap auto-height pb-xl">
+ <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
+ <div class="grid half gap-xl">
+ <div>
+ <p class="small text-muted">{{ trans('settings.maint_recycle_bin_desc') }}</p>
+ </div>
+ <div>
+ <div class="grid half no-gap mb-m">
+ <p class="mb-xs text-bookshelf">@icon('bookshelf'){{ trans('entities.shelves') }}: {{ $recycleStats['bookshelf'] }}</p>
+ <p class="mb-xs text-book">@icon('book'){{ trans('entities.books') }}: {{ $recycleStats['book'] }}</p>
+ <p class="mb-xs text-chapter">@icon('chapter'){{ trans('entities.chapters') }}: {{ $recycleStats['chapter'] }}</p>
+ <p class="mb-xs text-page">@icon('page'){{ trans('entities.pages') }}: {{ $recycleStats['page'] }}</p>
+ </div>
+ <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('settings.maint_recycle_bin_open') }}</a>
+ </div>
+ </div>
+ </div>
+
<div id="image-cleanup" class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
<div class="grid half gap-xl">
<form method="POST" action="{{ url('/settings/maintenance/cleanup-images') }}">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
- <div>
+ <div class="mb-s">
@if(session()->has('cleanup-images-warning'))
<p class="text-neg">
{{ session()->get('cleanup-images-warning') }}
<input type="hidden" name="ignore_revisions" value="{{ session()->getOldInput('ignore_revisions', 'false') }}">
<input type="hidden" name="confirm" value="true">
@else
- <label>
- <input type="checkbox" name="ignore_revisions" value="true">
- {{ trans('settings.maint_image_cleanup_ignore_revisions') }}
+ <label class="flex-container-row">
+ <div class="mr-s"><input type="checkbox" name="ignore_revisions" value="true"></div>
+ <div>{{ trans('settings.maint_delete_images_only_in_revisions') }}</div>
</label>
@endif
</div>
--- /dev/null
+@include('partials.entity-display-item', ['entity' => $entity])
+@if($entity->isA('book'))
+ @foreach($entity->chapters()->withTrashed()->get() as $chapter)
+ @include('partials.entity-display-item', ['entity' => $chapter])
+ @endforeach
+@endif
+@if($entity->isA('book') || $entity->isA('chapter'))
+ @foreach($entity->pages()->withTrashed()->get() as $page)
+ @include('partials.entity-display-item', ['entity' => $page])
+ @endforeach
+@endif
\ No newline at end of file
--- /dev/null
+@extends('simple-layout')
+
+@section('body')
+ <div class="container small">
+
+ <div class="grid left-focus v-center no-row-gap">
+ <div class="py-m">
+ @include('settings.navbar', ['selected' => 'maintenance'])
+ </div>
+ </div>
+
+ <div class="card content-wrap auto-height">
+ <h2 class="list-heading">{{ trans('settings.recycle_bin_permanently_delete') }}</h2>
+ <p class="text-muted">{{ trans('settings.recycle_bin_destroy_confirm') }}</p>
+ <form action="{{ url('/settings/recycle-bin/' . $deletion->id) }}" method="post">
+ {!! method_field('DELETE') !!}
+ {!! csrf_field() !!}
+ <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('common.cancel') }}</a>
+ <button type="submit" class="button">{{ trans('common.delete_confirm') }}</button>
+ </form>
+
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
+ <hr class="mt-m">
+ <h5>{{ trans('settings.recycle_bin_destroy_list') }}</h5>
+ @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
+ @endif
+
+ </div>
+
+ </div>
+@stop
--- /dev/null
+@extends('simple-layout')
+
+@section('body')
+ <div class="container">
+
+ <div class="grid left-focus v-center no-row-gap">
+ <div class="py-m">
+ @include('settings.navbar', ['selected' => 'maintenance'])
+ </div>
+ </div>
+
+ <div class="card content-wrap auto-height">
+ <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
+
+ <div class="grid half left-focus">
+ <div>
+ <p class="text-muted">{{ trans('settings.recycle_bin_desc') }}</p>
+ </div>
+ <div class="text-right">
+ <div component="dropdown" class="dropdown-container">
+ <button refs="dropdown@toggle"
+ type="button"
+ class="button outline">{{ trans('settings.recycle_bin_empty') }} </button>
+ <div refs="dropdown@menu" class="dropdown-menu">
+ <p class="text-neg small px-m mb-xs">{{ trans('settings.recycle_bin_empty_confirm') }}</p>
+
+ <form action="{{ url('/settings/recycle-bin/empty') }}" method="POST">
+ {!! csrf_field() !!}
+ <button type="submit" class="text-primary small delete">{{ trans('common.confirm') }}</button>
+ </form>
+ </div>
+ </div>
+
+ </div>
+ </div>
+
+
+ <hr class="mt-l mb-s">
+
+ {!! $deletions->links() !!}
+
+ <table class="table">
+ <tr>
+ <th>{{ trans('settings.recycle_bin_deleted_item') }}</th>
+ <th>{{ trans('settings.recycle_bin_deleted_by') }}</th>
+ <th>{{ trans('settings.recycle_bin_deleted_at') }}</th>
+ <th></th>
+ </tr>
+ @if(count($deletions) === 0)
+ <tr>
+ <td colspan="4">
+ <p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
+ </td>
+ </tr>
+ @endif
+ @foreach($deletions as $deletion)
+ <tr>
+ <td>
+ <div class="table-entity-item">
+ <span role="presentation" class="icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
+ <div class="text-{{ $deletion->deletable->getType() }}">
+ {{ $deletion->deletable->name }}
+ </div>
+ </div>
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+ <div class="mb-m"></div>
+ @endif
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
+ <div class="pl-xl block inline">
+ <div class="text-chapter">
+ @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
+ </div>
+ </div>
+ @endif
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+ <div class="pl-xl block inline">
+ <div class="text-page">
+ @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
+ </div>
+ </div>
+ @endif
+ </td>
+ <td>@include('partials.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
+ <td width="200">{{ $deletion->created_at }}</td>
+ <td width="150" class="text-right">
+ <div component="dropdown" class="dropdown-container">
+ <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
+ <ul refs="dropdown@menu" class="dropdown-menu">
+ <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+ <li><a class="block" href="{{ url('/settings/recycle-bin/'.$deletion->id.'/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+ </ul>
+ </div>
+ </td>
+ </tr>
+ @endforeach
+ </table>
+
+ {!! $deletions->links() !!}
+
+ </div>
+
+ </div>
+@stop
--- /dev/null
+@extends('simple-layout')
+
+@section('body')
+ <div class="container small">
+
+ <div class="grid left-focus v-center no-row-gap">
+ <div class="py-m">
+ @include('settings.navbar', ['selected' => 'maintenance'])
+ </div>
+ </div>
+
+ <div class="card content-wrap auto-height">
+ <h2 class="list-heading">{{ trans('settings.recycle_bin_restore') }}</h2>
+ <p class="text-muted">{{ trans('settings.recycle_bin_restore_confirm') }}</p>
+ <form action="{{ url('/settings/recycle-bin/' . $deletion->id . '/restore') }}" method="post">
+ {!! csrf_field() !!}
+ <a href="{{ url('/settings/recycle-bin') }}" class="button outline">{{ trans('common.cancel') }}</a>
+ <button type="submit" class="button">{{ trans('settings.recycle_bin_restore') }}</button>
+ </form>
+
+ @if($deletion->deletable instanceof \BookStack\Entities\Models\Entity)
+ <hr class="mt-m">
+ <h5>{{ trans('settings.recycle_bin_restore_list') }}</h5>
+ @if($deletion->deletable->getParent() && $deletion->deletable->getParent()->trashed())
+ <p class="text-neg">{{ trans('settings.recycle_bin_restore_deleted_parent') }}</p>
+ @endif
+ @include('settings.recycle-bin.deletable-entity-list', ['entity' => $deletion->deletable])
+ @endif
+
+ </div>
+
+ </div>
+@stop
+++ /dev/null
-<a href="{{$shelf->getUrl()}}" class="bookshelf-grid-item grid-card"
- data-entity-type="bookshelf" data-entity-id="{{$shelf->id}}">
- <div class="bg-shelf featured-image-container-wrap">
- <div class="featured-image-container" @if($shelf->cover) style="background-image: url('{{ $shelf->getBookCover() }}')"@endif>
- </div>
- @icon('bookshelf')
- </div>
- <div class="grid-card-content">
- <h2>{{$shelf->getShortName(35)}}</h2>
- @if(isset($shelf->searchSnippet))
- <p class="text-muted">{!! $shelf->searchSnippet !!}</p>
- @else
- <p class="text-muted">{{ $shelf->getExcerpt(130) }}</p>
- @endif
- </div>
- <div class="grid-card-footer text-muted text-small">
- @icon('star')<span title="{{$shelf->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $shelf->created_at->diffForHumans()]) }}</span>
- <br>
- @icon('edit')<span title="{{ $shelf->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $shelf->updated_at->diffForHumans()]) }}</span>
- </div>
-</a>
\ No newline at end of file
<a href="{{ $shelf->getUrl() }}" class="shelf entity-list-item" data-entity-type="bookshelf" data-entity-id="{{$shelf->id}}">
- <div class="entity-list-item-image bg-shelf @if($shelf->image_id) has-image @endif" style="background-image: url('{{ $shelf->getBookCover() }}')">
+ <div class="entity-list-item-image bg-bookshelf @if($shelf->image_id) has-image @endif" style="background-image: url('{{ $shelf->getBookCover() }}')">
@icon('bookshelf')
</div>
<div class="content py-xs">
@else
<div class="grid third">
@foreach($shelves as $key => $shelf)
- @include('shelves.grid-item', ['shelf' => $shelf])
+ @include('partials.entity-grid-item', ['entity' => $shelf])
@endforeach
</div>
@endif
@else
<div class="grid third">
@foreach($shelf->visibleBooks as $key => $book)
- @include('books.grid-item', ['book' => $book])
+ @include('partials.entity-grid-item', ['entity' => $book])
@endforeach
</div>
@endif
<p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
+ <hr class="my-l">
+
+ <div class="grid half gap-xl v-center">
+ <div>
+ <label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
+ <p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
+ </div>
+ <div>
+ @include('components.user-select', ['name' => 'new_owner_id', 'user' => null])
+ </div>
+ </div>
+
+ <hr class="my-l">
+
<div class="grid half">
<p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>
<div>
</div>
</div>
- {{--TODO - Add last login--}}
<table class="table">
<tr>
<th></th>
<a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">{{ trans('auth.email') }}</a>
</th>
<th>{{ trans('settings.role_user_roles') }}</th>
+ <th class="text-right">
+ <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'latest_activity']) }}">{{ trans('settings.users_latest_activity') }}</a>
+ </th>
</tr>
@foreach($users as $user)
<tr>
<small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
@endforeach
</td>
+ <td class="text-right text-muted">
+ @if($user->latestActivity)
+ <small title="{{ $user->latestActivity->created_at->format('Y-m-d H:i:s') }}">{{ $user->latestActivity->created_at->diffForHumans() }}</small>
+ @endif
+ </td>
</tr>
@endforeach
</table>
Route::get('chapters/{id}/export/pdf', 'ChapterExportApiController@exportPdf');
Route::get('chapters/{id}/export/plaintext', 'ChapterExportApiController@exportPlainText');
+Route::get('pages', 'PageApiController@list');
+Route::post('pages', 'PageApiController@create');
+Route::get('pages/{id}', 'PageApiController@read');
+Route::put('pages/{id}', 'PageApiController@update');
+Route::delete('pages/{id}', 'PageApiController@delete');
+
+Route::get('pages/{id}/export/html', 'PageExportApiController@exportHtml');
+Route::get('pages/{id}/export/pdf', 'PageExportApiController@exportPdf');
+Route::get('pages/{id}/export/plaintext', 'PageExportApiController@exportPlainText');
+
Route::get('shelves', 'BookshelfApiController@list');
Route::post('shelves', 'BookshelfApiController@create');
Route::get('shelves/{id}', 'BookshelfApiController@read');
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
+ // User Search
+ Route::get('/search/users/select', 'UserSearchController@forSelect');
+
Route::get('/templates', 'PageTemplateController@list');
Route::get('/templates/{templateId}', 'PageTemplateController@get');
Route::delete('/maintenance/cleanup-images', 'MaintenanceController@cleanupImages');
Route::post('/maintenance/send-test-email', 'MaintenanceController@sendTestEmail');
+ // Recycle Bin
+ Route::get('/recycle-bin', 'RecycleBinController@index');
+ Route::post('/recycle-bin/empty', 'RecycleBinController@empty');
+ Route::get('/recycle-bin/{id}/destroy', 'RecycleBinController@showDestroy');
+ Route::delete('/recycle-bin/{id}', 'RecycleBinController@destroy');
+ Route::get('/recycle-bin/{id}/restore', 'RecycleBinController@showRestore');
+ Route::post('/recycle-bin/{id}/restore', 'RecycleBinController@restore');
+
// Audit Log
Route::get('/audit', 'AuditLogController@index');
Route::delete('/users/{userId}/api-tokens/{tokenId}', 'UserApiTokenController@destroy');
// Roles
- Route::get('/roles', 'PermissionController@listRoles');
- Route::get('/roles/new', 'PermissionController@createRole');
- Route::post('/roles/new', 'PermissionController@storeRole');
- Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
- Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
- Route::get('/roles/{id}', 'PermissionController@editRole');
- Route::put('/roles/{id}', 'PermissionController@updateRole');
+ Route::get('/roles', 'RoleController@list');
+ Route::get('/roles/new', 'RoleController@create');
+ Route::post('/roles/new', 'RoleController@store');
+ Route::get('/roles/delete/{id}', 'RoleController@showDelete');
+ Route::delete('/roles/delete/{id}', 'RoleController@delete');
+ Route::get('/roles/{id}', 'RoleController@edit');
+ Route::put('/roles/{id}', 'RoleController@update');
});
});
<?php namespace Tests;
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
class ActivityTrackingTest extends BrowserKitTest
{
<?php namespace Tests\Api;
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
use Tests\TestCase;
class ApiListingTest extends TestCase
<?php namespace Tests\Api;
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
use Tests\TestCase;
class BooksApiTest extends TestCase
<?php namespace Tests\Api;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
use Tests\TestCase;
class ChaptersApiTest extends TestCase
--- /dev/null
+<?php namespace Tests\Api;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class PagesApiTest extends TestCase
+{
+ use TestsApi;
+
+ protected $baseEndpoint = '/api/pages';
+
+ public function test_index_endpoint_returns_expected_page()
+ {
+ $this->actingAsApiEditor();
+ $firstPage = Page::query()->orderBy('id', 'asc')->first();
+
+ $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,
+ 'priority' => $firstPage->priority,
+ ]
+ ]]);
+ }
+
+ public function test_create_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $book = Book::query()->first();
+ $details = [
+ 'name' => 'My API page',
+ 'book_id' => $book->id,
+ 'html' => '<p>My new page content</p>',
+ 'tags' => [
+ [
+ 'name' => 'tagname',
+ 'value' => 'tagvalue',
+ ]
+ ]
+ ];
+
+ $resp = $this->postJson($this->baseEndpoint, $details);
+ unset($details['html']);
+ $resp->assertStatus(200);
+ $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_type' => $newItem->getMorphClass(),
+ 'name' => 'tagname',
+ 'value' => 'tagvalue',
+ ]);
+ $resp->assertSeeText('My new page content');
+ $resp->assertJsonMissing(['book' => []]);
+ $this->assertActivityExists('page_create', $newItem);
+ }
+
+ public function test_page_name_needed_to_create()
+ {
+ $this->actingAsApiEditor();
+ $book = Book::query()->first();
+ $details = [
+ 'book_id' => $book->id,
+ '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."]
+ ]));
+ }
+
+ public function test_book_id_or_chapter_id_needed_to_create()
+ {
+ $this->actingAsApiEditor();
+ $details = [
+ 'name' => 'My api page',
+ 'html' => '<p>A page created via the API</p>',
+ ];
+
+ $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."]
+ ]));
+
+ $chapter = Chapter::visible()->first();
+ $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['chapter_id' => $chapter->id]));
+ $resp->assertStatus(200);
+
+ $book = Book::visible()->first();
+ $resp = $this->postJson($this->baseEndpoint, array_merge($details, ['book_id' => $book->id]));
+ $resp->assertStatus(200);
+ }
+
+ public function test_markdown_can_be_provided_for_create()
+ {
+ $this->actingAsApiEditor();
+ $book = Book::visible()->first();
+ $details = [
+ 'book_id' => $book->id,
+ 'name' => 'My api page',
+ 'markdown' => "# A new API page \n[link](https://example.com)",
+ ];
+
+ $resp = $this->postJson($this->baseEndpoint, $details);
+ $resp->assertJson(['markdown' => $details['markdown']]);
+
+ $respHtml = $resp->json('html');
+ $this->assertStringContainsString('new API page</h1>', $respHtml);
+ $this->assertStringContainsString('link</a>', $respHtml);
+ $this->assertStringContainsString('href="https://example.com"', $respHtml);
+ }
+
+ public function test_read_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+
+ $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
+ $resp->assertStatus(200);
+ $resp->assertJson([
+ 'id' => $page->id,
+ 'slug' => $page->slug,
+ 'created_by' => [
+ 'name' => $page->createdBy->name,
+ ],
+ 'book_id' => $page->book_id,
+ 'updated_by' => [
+ 'name' => $page->createdBy->name,
+ ],
+ ]);
+ }
+
+ public function test_read_endpoint_provides_rendered_html()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+ $page->html = "<p>testing</p><script>alert('danger')</script><h1>Hello</h1>";
+ $page->save();
+
+ $resp = $this->getJson($this->baseEndpoint . "/{$page->id}");
+ $html = $resp->json('html');
+ $this->assertStringNotContainsString('script', $html);
+ $this->assertStringContainsString('Hello', $html);
+ $this->assertStringContainsString('testing', $html);
+ }
+
+ public function test_update_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+ $details = [
+ 'name' => 'My updated API page',
+ 'html' => '<p>A page created via the API</p>',
+ 'tags' => [
+ [
+ 'name' => 'freshtag',
+ 'value' => 'freshtagval',
+ ]
+ ],
+ ];
+
+ $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+ $page->refresh();
+
+ $resp->assertStatus(200);
+ unset($details['html']);
+ $resp->assertJson(array_merge($details, [
+ 'id' => $page->id, 'slug' => $page->slug, 'book_id' => $page->book_id
+ ]));
+ $this->assertActivityExists('page_update', $page);
+ }
+
+ public function test_providing_new_chapter_id_on_update_will_move_page()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+ $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
+ $details = [
+ 'name' => 'My updated API page',
+ 'chapter_id' => $chapter->id,
+ '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,
+ ]);
+ }
+
+ public function test_providing_move_via_update_requires_page_create_permission_on_new_parent()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+ $chapter = Chapter::visible()->where('book_id', '!=', $page->book_id)->first();
+ $this->setEntityRestrictions($chapter, ['view'], [$this->getEditor()->roles()->first()]);
+ $details = [
+ 'name' => 'My updated API page',
+ 'chapter_id' => $chapter->id,
+ 'html' => '<p>A page created via the API</p>',
+ ];
+
+ $resp = $this->putJson($this->baseEndpoint . "/{$page->id}", $details);
+ $resp->assertStatus(403);
+ }
+
+ public function test_delete_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+ $resp = $this->deleteJson($this->baseEndpoint . "/{$page->id}");
+
+ $resp->assertStatus(204);
+ $this->assertActivityExists('page_delete', $page);
+ }
+
+ public function test_export_html_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+
+ $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/html");
+ $resp->assertStatus(200);
+ $resp->assertSee($page->name);
+ $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.html"');
+ }
+
+ public function test_export_plain_text_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+
+ $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/plaintext");
+ $resp->assertStatus(200);
+ $resp->assertSee($page->name);
+ $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.txt"');
+ }
+
+ public function test_export_pdf_endpoint()
+ {
+ $this->actingAsApiEditor();
+ $page = Page::visible()->first();
+
+ $resp = $this->get($this->baseEndpoint . "/{$page->id}/export/pdf");
+ $resp->assertStatus(200);
+ $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $page->slug . '.pdf"');
+ }
+}
\ No newline at end of file
<?php namespace Tests\Api;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
use Tests\TestCase;
class ShelvesApiTest extends TestCase
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityService;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\UserRepo;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use Carbon\Carbon;
class AuditLogTest extends TestCase
{
+ /** @var ActivityService */
+ protected $activityService;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->activityService = app(ActivityService::class);
+ }
public function test_only_accessible_with_right_permissions()
{
$admin = $this->getAdmin();
$this->actingAs($admin);
$page = Page::query()->first();
- app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+ $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
$activity = Activity::query()->orderBy('id', 'desc')->first();
$resp = $this->get('settings/audit');
$resp->assertSeeText($page->name);
$resp->assertSeeText('page_create');
$resp->assertSeeText($activity->created_at->toDateTimeString());
- $resp->assertElementContains('.audit-log-user', $admin->name);
+ $resp->assertElementContains('.table-user-item', $admin->name);
}
public function test_shows_name_for_deleted_items()
$this->actingAs( $this->getAdmin());
$page = Page::query()->first();
$pageName = $page->name;
- app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+ $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
app(PageRepo::class)->destroy($page);
+ app(TrashCan::class)->empty();
$resp = $this->get('settings/audit');
$resp->assertSeeText('Deleted Item');
$viewer = $this->getViewer();
$this->actingAs($viewer);
$page = Page::query()->first();
- app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+ $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
$this->actingAs($this->getAdmin());
app(UserRepo::class)->destroy($viewer);
{
$this->actingAs($this->getAdmin());
$page = Page::query()->first();
- app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+ $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
$resp = $this->get('settings/audit');
$resp->assertSeeText($page->name);
{
$this->actingAs($this->getAdmin());
$page = Page::query()->first();
- app(ActivityService::class)->add($page, 'page_create', $page->book->id);
+ $this->activityService->addForEntity($page, ActivityType::PAGE_CREATE);
$yesterday = (Carbon::now()->subDay()->format('Y-m-d'));
$tomorrow = (Carbon::now()->addDay()->format('Y-m-d'));
use BookStack\Auth\Role;
use BookStack\Auth\User;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Notifications\ResetPassword;
use BookStack\Settings\SettingService;
<?php namespace Tests;
-use BookStack\Entities\Entity;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
use BookStack\Auth\Role;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Models\Page;
use BookStack\Settings\SettingService;
+use DB;
use Illuminate\Contracts\Console\Kernel;
+use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase;
use Symfony\Component\DomCrawler\Crawler;
public function tearDown() : void
{
- \DB::disconnect();
+ DB::disconnect();
parent::tearDown();
}
/**
* Creates the application.
*
- * @return \Illuminate\Foundation\Application
+ * @return Application
*/
public function createApplication()
{
*/
public function getNormalUser()
{
- return \BookStack\Auth\User::where('system_name', '=', null)->get()->last();
+ return User::where('system_name', '=', null)->get()->last();
}
/**
/**
* Create a group of entities that belong to a specific user.
- * @param $creatorUser
- * @param $updaterUser
- * @return array
*/
- protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
+ protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
{
- if ($updaterUser === false) $updaterUser = $creatorUser;
- $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
- $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
- $page = factory(\BookStack\Entities\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id, 'chapter_id' => $chapter->id]);
+ if (empty($updaterUser)) {
+ $updaterUser = $creatorUser;
+ }
+
+ $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id];
+ $book = factory(Book::class)->create($userAttrs);
+ $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs));
+ $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs));
$restrictionService = $this->app[PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($book);
- return [
- 'book' => $book,
- 'chapter' => $chapter,
- 'page' => $page
- ];
+
+ return compact('book', 'chapter', 'page');
}
/**
*/
protected function getNewBlankUser($attributes = [])
{
- $user = factory(\BookStack\Auth\User::class)->create($attributes);
+ $user = factory(User::class)->create($attributes);
return $user;
}
<?php namespace Tests;
+use BookStack\Actions\ActivityType;
use BookStack\Actions\Comment;
use BookStack\Actions\CommentRepo;
use BookStack\Auth\Permissions\JointPermission;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Page;
use BookStack\Auth\User;
use BookStack\Entities\Repos\PageRepo;
use Symfony\Component\Console\Exception\RuntimeException;
{
$this->asEditor();
$page = Page::first();
- \Activity::add($page, 'page_update', $page->book->id);
+ \Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
$this->assertDatabaseHas('activities', [
- 'key' => 'page_update',
+ 'type' => 'page_update',
'entity_id' => $page->id,
'user_id' => $this->getEditor()->id
]);
$this->assertDatabaseMissing('activities', [
- 'key' => 'page_update'
+ 'type' => 'page_update'
]);
}
<?php namespace Tests\Entity;
use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
use BookStack\Uploads\Image;
use Illuminate\Support\Str;
use Tests\TestCase;
public function test_shelf_delete()
{
- $shelf = Bookshelf::first();
- $resp = $this->asEditor()->get($shelf->getUrl('/delete'));
- $resp->assertSeeText('Delete Bookshelf');
- $resp->assertSee("action=\"{$shelf->getUrl()}\"");
-
- $resp = $this->delete($shelf->getUrl());
- $resp->assertRedirect('/shelves');
- $this->assertDatabaseMissing('bookshelves', ['id' => $shelf->id]);
- $this->assertDatabaseMissing('bookshelves_books', ['bookshelf_id' => $shelf->id]);
- $this->assertSessionHas('success');
+ $shelf = Bookshelf::query()->whereHas('books')->first();
+ $this->assertNull($shelf->deleted_at);
+ $bookCount = $shelf->books()->count();
+
+ $deleteViewReq = $this->asEditor()->get($shelf->getUrl('/delete'));
+ $deleteViewReq->assertSeeText('Are you sure you want to delete this bookshelf?');
+
+ $deleteReq = $this->delete($shelf->getUrl());
+ $deleteReq->assertRedirect(url('/shelves'));
+ $this->assertActivityExists('bookshelf_delete', $shelf);
+
+ $shelf->refresh();
+ $this->assertNotNull($shelf->deleted_at);
+
+ $this->assertTrue($shelf->books()->count() === $bookCount);
+ $this->assertTrue($shelf->deletions()->count() === 1);
+
+ $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+ $redirectReq->assertNotificationContains('Bookshelf Successfully Deleted');
}
public function test_shelf_copy_permissions()
--- /dev/null
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Book;
+use Tests\TestCase;
+
+class BookTest extends TestCase
+{
+ public function test_book_delete()
+ {
+ $book = Book::query()->whereHas('pages')->whereHas('chapters')->first();
+ $this->assertNull($book->deleted_at);
+ $pageCount = $book->pages()->count();
+ $chapterCount = $book->chapters()->count();
+
+ $deleteViewReq = $this->asEditor()->get($book->getUrl('/delete'));
+ $deleteViewReq->assertSeeText('Are you sure you want to delete this book?');
+
+ $deleteReq = $this->delete($book->getUrl());
+ $deleteReq->assertRedirect(url('/books'));
+ $this->assertActivityExists('book_delete', $book);
+
+ $book->refresh();
+ $this->assertNotNull($book->deleted_at);
+
+ $this->assertTrue($book->pages()->count() === 0);
+ $this->assertTrue($book->chapters()->count() === 0);
+ $this->assertTrue($book->pages()->withTrashed()->count() === $pageCount);
+ $this->assertTrue($book->chapters()->withTrashed()->count() === $chapterCount);
+ $this->assertTrue($book->deletions()->count() === 1);
+
+ $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+ $redirectReq->assertNotificationContains('Book Successfully Deleted');
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Chapter;
+use Tests\TestCase;
+
+class ChapterTest extends TestCase
+{
+ public function test_chapter_delete()
+ {
+ $chapter = Chapter::query()->whereHas('pages')->first();
+ $this->assertNull($chapter->deleted_at);
+ $pageCount = $chapter->pages()->count();
+
+ $deleteViewReq = $this->asEditor()->get($chapter->getUrl('/delete'));
+ $deleteViewReq->assertSeeText('Are you sure you want to delete this chapter?');
+
+ $deleteReq = $this->delete($chapter->getUrl());
+ $deleteReq->assertRedirect($chapter->getParent()->getUrl());
+ $this->assertActivityExists('chapter_delete', $chapter);
+
+ $chapter->refresh();
+ $this->assertNotNull($chapter->deleted_at);
+
+ $this->assertTrue($chapter->pages()->count() === 0);
+ $this->assertTrue($chapter->pages()->withTrashed()->count() === $pageCount);
+ $this->assertTrue($chapter->deletions()->count() === 1);
+
+ $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+ $redirectReq->assertNotificationContains('Chapter Successfully Deleted');
+ }
+}
\ No newline at end of file
<?php namespace Tests\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use Tests\BrowserKitTest;
class CommentSettingTest extends BrowserKitTest
<?php namespace Tests\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Actions\Comment;
use Tests\TestCase;
<?php namespace Tests\Entity;
use BookStack\Actions\Tag;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use Tests\TestCase;
class EntitySearchTest extends TestCase
<?php namespace Tests\Entity;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Repos\PageRepo;
use Carbon\Carbon;
-use Illuminate\Support\Facades\DB;
use Tests\BrowserKitTest;
class EntityTest extends BrowserKitTest
// Test Creation
$book = $this->bookCreation();
$chapter = $this->chapterCreation($book);
- $page = $this->pageCreation($chapter);
+ $this->pageCreation($chapter);
// Test Updating
- $book = $this->bookUpdate($book);
-
- // Test Deletion
- $this->bookDelete($book);
- }
-
- public function bookDelete(Book $book)
- {
- $this->asAdmin()
- ->visit($book->getUrl())
- // Check link works correctly
- ->click('Delete')
- ->seePageIs($book->getUrl() . '/delete')
- // Ensure the book name is show to user
- ->see($book->name)
- ->press('Confirm')
- ->seePageIs('/books')
- ->notSeeInDatabase('books', ['id' => $book->id]);
+ $this->bookUpdate($book);
}
public function bookUpdate(Book $book)
->seePageIs($chapter->getUrl());
}
- public function test_page_delete_removes_entity_from_its_activity()
- {
- $page = Page::query()->first();
-
- $this->asEditor()->put($page->getUrl(), [
- 'name' => 'My updated page',
- 'html' => '<p>updated content</p>',
- ]);
- $page->refresh();
-
- $this->seeInDatabase('activities', [
- 'entity_id' => $page->id,
- 'entity_type' => $page->getMorphClass(),
- ]);
-
- $resp = $this->delete($page->getUrl());
- $resp->assertResponseStatus(302);
-
- $this->dontSeeInDatabase('activities', [
- 'entity_id' => $page->id,
- 'entity_type' => $page->getMorphClass(),
- ]);
-
- $this->seeInDatabase('activities', [
- 'extra' => 'My updated page',
- 'entity_id' => 0,
- 'entity_type' => '',
- ]);
- }
-
}
<?php namespace Tests\Entity;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
-use BookStack\Uploads\HttpFetcher;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;
public function test_page_export_sets_right_data_type_for_svg_embeds()
{
$page = Page::first();
- $page->html = '<img src="http://example.com/image.svg">';
+ Storage::disk('local')->makeDirectory('uploads/images/gallery');
+ Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');
+ $page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg">';
$page->save();
$this->asEditor();
- $this->mockHttpFetch('<svg></svg>');
$resp = $this->get($page->getUrl('/export/html'));
+ Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');
+
$resp->assertStatus(200);
$resp->assertSee('<img src="data:image/svg+xml;base64');
}
-}
\ No newline at end of file
+ public function test_page_image_containment_works_on_multiple_images_within_a_single_line()
+ {
+ $page = Page::first();
+ Storage::disk('local')->makeDirectory('uploads/images/gallery');
+ Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');
+ Storage::disk('local')->put('uploads/images/gallery/svg_test2.svg', '<svg></svg>');
+ $page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg" class="a"><img src="http://localhost/uploads/images/gallery/svg_test2.svg" class="b">';
+ $page->save();
+
+ $resp = $this->asEditor()->get($page->getUrl('/export/html'));
+ Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');
+ Storage::disk('local')->delete('uploads/images/gallery/svg_test2.svg');
+
+ $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test');
+ }
+
+ public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder()
+ {
+ $page = Page::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"/>';
+ $storageDisk = Storage::disk('local');
+ $storageDisk->makeDirectory('uploads/images/gallery');
+ $storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');
+ $storageDisk->put('uploads/svg_test.svg', '<svg>bad</svg>');
+ $page->save();
+
+ $resp = $this->asEditor()->get($page->getUrl('/export/html'));
+
+ $storageDisk->delete('uploads/images/gallery/svg_test.svg');
+ $storageDisk->delete('uploads/svg_test.svg');
+
+ $resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test.svg');
+ $resp->assertSee('http://localhost/uploads/svg_test.svg');
+ $resp->assertSee('src="/uploads/svg_test.svg"');
+ }
+
+}
public function setUp(): void
{
parent::setUp();
- $this->page = \BookStack\Entities\Page::first();
+ $this->page = \BookStack\Entities\Models\Page::first();
}
protected function setMarkdownEditor()
<?php namespace Tests\Entity;
-use BookStack\Entities\Managers\PageContent;
-use BookStack\Entities\Page;
+use BookStack\Entities\Tools\PageContent;
+use BookStack\Entities\Models\Page;
use Tests\TestCase;
class PageContentTest extends TestCase
}
+ 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>'
+ ];
+
+ $this->asEditor();
+ $page = Page::first();
+
+ foreach ($checks as $check) {
+ $page->html = $check;
+ $page->save();
+
+ $pageView = $this->get($page->getUrl());
+ $pageView->assertStatus(200);
+ $pageView->assertElementNotContains('.page-content', '<a id="xss">');
+ $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>'
+ ];
+
+ $this->asEditor();
+ $page = Page::first();
+
+ foreach ($checks as $check) {
+ $page->html = $check;
+ $page->save();
+
+ $pageView = $this->get($page->getUrl());
+ $pageView->assertStatus(200);
+ $pageView->assertElementNotContains('.page-content', '<button id="xss"');
+ $pageView->assertElementNotContains('.page-content', '<input id="xss"');
+ $pageView->assertElementNotContains('.page-content', '<form id="xss"');
+ $pageView->assertElementNotContains('.page-content', 'action=javascript:');
+ $pageView->assertElementNotContains('.page-content', 'formaction=javascript:');
+ }
+ }
+
+ public function test_metadata_redirects_are_removed()
+ {
+ $checks = [
+ '<meta http-equiv="refresh" content="0; url=//external_url">',
+ ];
+
+ $this->asEditor();
+ $page = Page::first();
+
+ foreach ($checks as $check) {
+ $page->html = $check;
+ $page->save();
+
+ $pageView = $this->get($page->getUrl());
+ $pageView->assertStatus(200);
+ $pageView->assertElementNotContains('.page-content', '<meta>');
+ $pageView->assertElementNotContains('.page-content', '</meta>');
+ $pageView->assertElementNotContains('.page-content', 'content=');
+ $pageView->assertElementNotContains('.page-content', 'external_url');
+ }
+ }
public function test_page_inline_on_attributes_removed_by_default()
{
$this->asEditor();
<?php namespace Tests\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use Tests\BrowserKitTest;
public function setUp(): void
{
parent::setUp();
- $this->page = \BookStack\Entities\Page::first();
+ $this->page = \BookStack\Entities\Models\Page::first();
$this->pageRepo = app(PageRepo::class);
}
public function test_alert_message_shows_if_someone_else_editing()
{
- $nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
+ $nonEditedPage = \BookStack\Entities\Models\Page::take(10)->get()->last();
$addedContent = '<p>test message content</p>';
$this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
public function test_draft_pages_show_on_homepage()
{
- $book = \BookStack\Entities\Book::first();
+ $book = \BookStack\Entities\Models\Book::first();
$this->asAdmin()->visit('/')
->dontSeeInElement('#recent-drafts', 'New Page')
->visit($book->getUrl() . '/create-page')
public function test_draft_pages_not_visible_by_others()
{
- $book = \BookStack\Entities\Book::first();
+ $book = \BookStack\Entities\Models\Book::first();
$chapter = $book->chapters->first();
$newUser = $this->getEditor();
<?php namespace Tests\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use Tests\TestCase;
<?php namespace Tests\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use Tests\TestCase;
class PageTemplateTest extends TestCase
--- /dev/null
+<?php namespace Tests\Entity;
+
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class PageTest extends TestCase
+{
+ public function test_page_delete()
+ {
+ $page = Page::query()->first();
+ $this->assertNull($page->deleted_at);
+
+ $deleteViewReq = $this->asEditor()->get($page->getUrl('/delete'));
+ $deleteViewReq->assertSeeText('Are you sure you want to delete this page?');
+
+ $deleteReq = $this->delete($page->getUrl());
+ $deleteReq->assertRedirect($page->getParent()->getUrl());
+ $this->assertActivityExists('page_delete', $page);
+
+ $page->refresh();
+ $this->assertNotNull($page->deleted_at);
+ $this->assertTrue($page->deletions()->count() === 1);
+
+ $redirectReq = $this->get($deleteReq->baseResponse->headers->get('location'));
+ $redirectReq->assertNotificationContains('Page Successfully Deleted');
+ }
+}
\ No newline at end of file
<?php namespace Tests\Entity;
-use BookStack\Entities\SearchOptions;
+use BookStack\Entities\Tools\SearchOptions;
use Tests\TestCase;
class SearchOptionsTest extends TestCase
<?php namespace Tests\Entity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use Tests\TestCase;
$movePageResp = $this->actingAs($this->getEditor())->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
- $page = Page::find($page->id);
+ $page->refresh();
$movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page parent is now the new book');
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertDontSee($page->getUrl('/copy'));
- $newBook->created_by = $viewer->id;
+ $newBook->owned_by = $viewer->id;
$newBook->save();
$this->giveUserPermissions($viewer, ['page-create-own']);
$this->regenEntityPermissions($newBook);
<?php namespace Tests\Entity;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
use BookStack\Actions\Tag;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
use BookStack\Auth\Permissions\PermissionService;
use Tests\BrowserKitTest;
<?php namespace Tests;
-use BookStack\Entities\Book;
+use BookStack\Entities\Models\Book;
use Illuminate\Support\Facades\Log;
class ErrorTest extends TestCase
<?php namespace Tests;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
class HomepageTest extends TestCase
{
{
$editor = $this->getEditor();
setting()->putUser($editor, 'bookshelves_view_type', 'grid');
+ $shelf = Bookshelf::query()->firstOrFail();
$this->setSettings(['app-homepage-type' => 'bookshelves']);
$this->asEditor();
$homeVisit = $this->get('/');
$homeVisit->assertSee('Shelves');
- $homeVisit->assertSee('bookshelf-grid-item grid-card');
$homeVisit->assertSee('grid-card-content');
- $homeVisit->assertSee('grid-card-footer');
$homeVisit->assertSee('featured-image-container');
+ $homeVisit->assertElementContains('.grid-card', $shelf->name);
$this->setSettings(['app-homepage-type' => false]);
$this->test_default_homepage_visible();
--- /dev/null
+<?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();
+ $user = User::query()->where('id', '!=', $page->owned_by)->first();
+
+ $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('pages', ['owned_by' => $user->id, 'id' => $page->id]);
+ }
+
+ public function test_changing_chapter_owner()
+ {
+ $chapter = Chapter::query()->first();
+ $user = User::query()->where('id', '!=', $chapter->owned_by)->first();
+
+ $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('chapters', ['owned_by' => $user->id, 'id' => $chapter->id]);
+ }
+
+ public function test_changing_book_owner()
+ {
+ $book = Book::query()->first();
+ $user = User::query()->where('id', '!=', $book->owned_by)->first();
+
+ $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('books', ['owned_by' => $user->id, 'id' => $book->id]);
+ }
+
+ public function test_changing_shelf_owner()
+ {
+ $shelf = Bookshelf::query()->first();
+ $user = User::query()->where('id', '!=', $shelf->owned_by)->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;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
+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\Page;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Str;
use Tests\BrowserKitTest;
-class RestrictionsTest extends BrowserKitTest
+class EntityPermissionsTest extends BrowserKitTest
{
/**
public function test_bookshelf_update_restriction()
{
- $shelf = BookShelf::first();
+ $shelf = Bookshelf::first();
$this->actingAs($this->user)
->visit($shelf->getUrl('/edit'))
->dontSee($page->name);
}
+ public function test_restricted_chapter_pages_not_visible_on_book_page()
+ {
+ $chapter = Chapter::query()->first();
+ $this->actingAs($this->user)
+ ->visit($chapter->book->getUrl())
+ ->see($chapter->pages->first()->name);
+
+ foreach ($chapter->pages as $page) {
+ $this->setEntityRestrictions($page, []);
+ }
+
+ $this->actingAs($this->user)
+ ->visit($chapter->book->getUrl())
+ ->dontSee($chapter->pages->first()->name);
+ }
+
public function test_bookshelf_update_restriction_override()
{
$shelf = Bookshelf::first();
public function test_page_visible_if_has_permissions_when_book_not_visible()
{
$book = Book::first();
-
- $this->setEntityRestrictions($book, []);
-
$bookChapter = $book->chapters->first();
$bookPage = $bookChapter->pages->first();
+
+ foreach ([$book, $bookChapter, $bookPage] as $entity) {
+ $entity->name = Str::random(24);
+ $entity->save();
+ }
+
+ $this->setEntityRestrictions($book, []);
$this->setEntityRestrictions($bookPage, ['view']);
$this->actingAs($this->viewer);
--- /dev/null
+<?php namespace Tests\Permissions;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use Illuminate\Support\Str;
+use Tests\TestCase;
+
+class ExportPermissionsTest extends TestCase
+{
+
+ public function test_page_content_without_view_access_hidden_on_chapter_export()
+ {
+ $chapter = Chapter::query()->first();
+ $page = $chapter->pages()->firstOrFail();
+ $pageContent = Str::random(48);
+ $page->html = '<p>' . $pageContent . '</p>';
+ $page->save();
+ $viewer = $this->getViewer();
+ $this->actingAs($viewer);
+ $formats = ['html', 'plaintext'];
+
+ foreach ($formats as $format) {
+ $resp = $this->get($chapter->getUrl("export/{$format}"));
+ $resp->assertStatus(200);
+ $resp->assertSee($page->name);
+ $resp->assertSee($pageContent);
+ }
+
+ $this->setEntityRestrictions($page, []);
+
+ foreach ($formats as $format) {
+ $resp = $this->get($chapter->getUrl("export/{$format}"));
+ $resp->assertStatus(200);
+ $resp->assertDontSee($page->name);
+ $resp->assertDontSee($pageContent);
+ }
+ }
+
+ public function test_page_content_without_view_access_hidden_on_book_export()
+ {
+ $book = Book::query()->first();
+ $page = $book->pages()->firstOrFail();
+ $pageContent = Str::random(48);
+ $page->html = '<p>' . $pageContent . '</p>';
+ $page->save();
+ $viewer = $this->getViewer();
+ $this->actingAs($viewer);
+ $formats = ['html', 'plaintext'];
+
+ foreach ($formats as $format) {
+ $resp = $this->get($book->getUrl("export/{$format}"));
+ $resp->assertStatus(200);
+ $resp->assertSee($page->name);
+ $resp->assertSee($pageContent);
+ }
+
+ $this->setEntityRestrictions($page, []);
+
+ foreach ($formats as $format) {
+ $resp = $this->get($book->getUrl("export/{$format}"));
+ $resp->assertStatus(200);
+ $resp->assertDontSee($page->name);
+ $resp->assertDontSee($pageContent);
+ }
+ }
+
+}
\ No newline at end of file
<?php namespace Tests\Permissions;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Page;
+use BookStack\Actions\Comment;
+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;
public function test_cannot_delete_admin_role()
{
- $adminRole = \BookStack\Auth\Role::getRole('admin');
+ $adminRole = Role::getRole('admin');
$deletePageUrl = '/settings/roles/delete/' . $adminRole->id;
$this->asAdmin()->visit($deletePageUrl)
->press('Confirm')
public function test_restrictions_manage_all_permission()
{
- $page = \BookStack\Entities\Page::take(1)->get()->first();
+ $page = Page::take(1)->get()->first();
$this->actingAs($this->user)->visit($page->getUrl())
->dontSee('Permissions')
->visit($page->getUrl() . '/permissions')
public function test_restrictions_manage_own_permission()
{
- $otherUsersPage = \BookStack\Entities\Page::first();
+ $otherUsersPage = Page::first();
$content = $this->createEntityChainBelongingToUser($this->user);
// Check can't restrict other's content
$this->actingAs($this->user)->visit($otherUsersPage->getUrl())
{
$otherShelf = Bookshelf::first();
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
- $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-update-own', [
public function test_bookshelves_edit_all_permission()
{
- $otherShelf = \BookStack\Entities\Bookshelf::first();
+ $otherShelf = Bookshelf::first();
$this->checkAccessPermission('bookshelf-update-all', [
$otherShelf->getUrl('/edit')
], [
public function test_bookshelves_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
- $otherShelf = \BookStack\Entities\Bookshelf::first();
+ $otherShelf = Bookshelf::first();
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
- $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-delete-own', [
public function test_bookshelves_delete_all_permission()
{
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
- $otherShelf = \BookStack\Entities\Bookshelf::first();
+ $otherShelf = Bookshelf::first();
$this->checkAccessPermission('bookshelf-delete-all', [
$otherShelf->getUrl('/delete')
], [
public function test_books_edit_own_permission()
{
- $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+ $otherBook = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('book-update-own', [
$ownBook->getUrl() . '/edit'
public function test_books_edit_all_permission()
{
- $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+ $otherBook = Book::take(1)->get()->first();
$this->checkAccessPermission('book-update-all', [
$otherBook->getUrl() . '/edit'
], [
public function test_books_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['book-update-all']);
- $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+ $otherBook = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('book-delete-own', [
$ownBook->getUrl() . '/delete'
public function test_books_delete_all_permission()
{
$this->giveUserPermissions($this->user, ['book-update-all']);
- $otherBook = \BookStack\Entities\Book::take(1)->get()->first();
+ $otherBook = Book::take(1)->get()->first();
$this->checkAccessPermission('book-delete-all', [
$otherBook->getUrl() . '/delete'
], [
public function test_chapter_create_own_permissions()
{
- $book = \BookStack\Entities\Book::take(1)->get()->first();
+ $book = Book::take(1)->get()->first();
$ownBook = $this->createEntityChainBelongingToUser($this->user)['book'];
$this->checkAccessPermission('chapter-create-own', [
$ownBook->getUrl('/create-chapter')
public function test_chapter_create_all_permissions()
{
- $book = \BookStack\Entities\Book::take(1)->get()->first();
+ $book = Book::take(1)->get()->first();
$this->checkAccessPermission('chapter-create-all', [
$book->getUrl('/create-chapter')
], [
public function test_chapter_edit_own_permission()
{
- $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+ $otherChapter = Chapter::take(1)->get()->first();
$ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
$this->checkAccessPermission('chapter-update-own', [
$ownChapter->getUrl() . '/edit'
public function test_chapter_edit_all_permission()
{
- $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+ $otherChapter = Chapter::take(1)->get()->first();
$this->checkAccessPermission('chapter-update-all', [
$otherChapter->getUrl() . '/edit'
], [
public function test_chapter_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['chapter-update-all']);
- $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+ $otherChapter = Chapter::take(1)->get()->first();
$ownChapter = $this->createEntityChainBelongingToUser($this->user)['chapter'];
$this->checkAccessPermission('chapter-delete-own', [
$ownChapter->getUrl() . '/delete'
public function test_chapter_delete_all_permission()
{
$this->giveUserPermissions($this->user, ['chapter-update-all']);
- $otherChapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+ $otherChapter = Chapter::take(1)->get()->first();
$this->checkAccessPermission('chapter-delete-all', [
$otherChapter->getUrl() . '/delete'
], [
public function test_page_create_own_permissions()
{
- $book = \BookStack\Entities\Book::first();
- $chapter = \BookStack\Entities\Chapter::first();
+ $book = Book::first();
+ $chapter = Chapter::first();
$entities = $this->createEntityChainBelongingToUser($this->user);
$ownBook = $entities['book'];
foreach ($accessUrls as $index => $url) {
$this->actingAs($this->user)->visit($url);
- $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+ $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
$this->seePageIs($expectedUrl);
}
public function test_page_create_all_permissions()
{
- $book = \BookStack\Entities\Book::take(1)->get()->first();
- $chapter = \BookStack\Entities\Chapter::take(1)->get()->first();
+ $book = Book::take(1)->get()->first();
+ $chapter = Chapter::take(1)->get()->first();
$baseUrl = $book->getUrl() . '/page';
$createUrl = $book->getUrl('/create-page');
foreach ($accessUrls as $index => $url) {
$this->actingAs($this->user)->visit($url);
- $expectedUrl = \BookStack\Entities\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
+ $expectedUrl = Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
$this->seePageIs($expectedUrl);
}
public function test_page_edit_own_permission()
{
- $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+ $otherPage = Page::take(1)->get()->first();
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->checkAccessPermission('page-update-own', [
$ownPage->getUrl() . '/edit'
public function test_page_edit_all_permission()
{
- $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+ $otherPage = Page::take(1)->get()->first();
$this->checkAccessPermission('page-update-all', [
$otherPage->getUrl() . '/edit'
], [
public function test_page_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['page-update-all']);
- $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+ $otherPage = Page::take(1)->get()->first();
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];
$this->checkAccessPermission('page-delete-own', [
$ownPage->getUrl() . '/delete'
public function test_page_delete_all_permission()
{
$this->giveUserPermissions($this->user, ['page-update-all']);
- $otherPage = \BookStack\Entities\Page::take(1)->get()->first();
+ $otherPage = Page::take(1)->get()->first();
$this->checkAccessPermission('page-delete-all', [
$otherPage->getUrl() . '/delete'
], [
public function test_public_role_visible_in_user_edit_screen()
{
- $user = \BookStack\Auth\User::first();
+ $user = User::first();
$adminRole = Role::getSystemRole('admin');
$publicRole = Role::getSystemRole('public');
$this->asAdmin()->visit('/settings/users/' . $user->id)
public function test_image_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['image-update-all']);
- $page = \BookStack\Entities\Page::first();
- $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
+ $page = Page::first();
+ $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(403);
{
$this->giveUserPermissions($this->user, ['image-update-all']);
$admin = $this->getAdmin();
- $page = \BookStack\Entities\Page::first();
- $image = factory(\BookStack\Uploads\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
+ $page = Page::first();
+ $image = factory(Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(403);
{
// To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.
$page = Page::first();
- $viewerRole = \BookStack\Auth\Role::getRole('viewer');
+ $viewerRole = Role::getRole('viewer');
$viewer = $this->getViewer();
$this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(200);
{
$admin = $this->getAdmin();
// Book links
- $book = factory(\BookStack\Entities\Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
+ $book = factory(Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->updateEntityPermissions($book);
$this->actingAs($this->getViewer())->visit($book->getUrl())
->dontSee('Create a new page')
->dontSee('Add a chapter');
// Chapter links
- $chapter = factory(\BookStack\Entities\Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
+ $chapter = factory(Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
$this->updateEntityPermissions($chapter);
$this->actingAs($this->getViewer())->visit($chapter->getUrl())
->dontSee('Create a new page')
}
private function addComment($page) {
- $comment = factory(\BookStack\Actions\Comment::class)->make();
+ $comment = factory(Comment::class)->make();
$url = "/comment/$page->id";
$request = [
'text' => $comment->text,
}
private function updateComment($commentId) {
- $comment = factory(\BookStack\Actions\Comment::class)->make();
+ $comment = factory(Comment::class)->make();
$url = "/comment/$commentId";
$request = [
'text' => $comment->text,
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
class PublicActionTest extends BrowserKitTest
{
--- /dev/null
+<?php namespace Tests;
+
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Deletion;
+use BookStack\Entities\Models\Page;
+use DB;
+use Illuminate\Support\Carbon;
+
+class RecycleBinTest extends TestCase
+{
+ public function test_recycle_bin_routes_permissions()
+ {
+ $page = Page::query()->first();
+ $editor = $this->getEditor();
+ $this->actingAs($editor)->delete($page->getUrl());
+ $deletion = Deletion::query()->firstOrFail();
+
+ $routes = [
+ 'GET:/settings/recycle-bin',
+ 'POST:/settings/recycle-bin/empty',
+ "GET:/settings/recycle-bin/{$deletion->id}/destroy",
+ "GET:/settings/recycle-bin/{$deletion->id}/restore",
+ "POST:/settings/recycle-bin/{$deletion->id}/restore",
+ "DELETE:/settings/recycle-bin/{$deletion->id}",
+ ];
+
+ 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) {
+ [$method, $url] = explode(':', $route);
+ $resp = $this->call($method, $url);
+ $this->assertPermissionError($resp);
+ }
+
+ $this->giveUserPermissions($editor, ['settings-manage']);
+
+ 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()
+ {
+ $page = Page::query()->first();
+ $book = Book::query()->whereHas('pages')->whereHas('chapters')->withCount(['pages', 'chapters'])->first();
+ $editor = $this->getEditor();
+ $this->actingAs($editor)->delete($page->getUrl());
+ $this->actingAs($editor)->delete($book->getUrl());
+
+ $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
+ $viewReq->assertElementContains('table.table', $page->name);
+ $viewReq->assertElementContains('table.table', $editor->name);
+ $viewReq->assertElementContains('table.table', $book->name);
+ $viewReq->assertElementContains('table.table', $book->pages_count . ' Pages');
+ $viewReq->assertElementContains('table.table', $book->chapters_count . ' Chapters');
+ }
+
+ 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();
+ $editor = $this->getEditor();
+ $this->actingAs($editor)->delete($page->getUrl());
+ $this->actingAs($editor)->delete($book->getUrl());
+
+ $this->assertTrue(Deletion::query()->count() === 2);
+ $emptyReq = $this->asAdmin()->post('/settings/recycle-bin/empty');
+ $emptyReq->assertRedirect('/settings/recycle-bin');
+
+ $this->assertTrue(Deletion::query()->count() === 0);
+ $this->assertDatabaseMissing('books', ['id' => $book->id]);
+ $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+ $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
+ $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
+
+ $itemCount = 2 + $book->pages->count() + $book->chapters->count();
+ $redirectReq = $this->get('/settings/recycle-bin');
+ $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+ }
+
+ public function test_entity_restore()
+ {
+ $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+ $this->asEditor()->delete($book->getUrl());
+ $deletion = Deletion::query()->firstOrFail();
+
+ $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
+ $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNotNull('deleted_at')->count());
+
+ $restoreReq = $this->asAdmin()->post("/settings/recycle-bin/{$deletion->id}/restore");
+ $restoreReq->assertRedirect('/settings/recycle-bin');
+ $this->assertTrue(Deletion::query()->count() === 0);
+
+ $this->assertEquals($book->pages->count(), DB::table('pages')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
+ $this->assertEquals($book->chapters->count(), DB::table('chapters')->where('book_id', '=', $book->id)->whereNull('deleted_at')->count());
+
+ $itemCount = 1 + $book->pages->count() + $book->chapters->count();
+ $redirectReq = $this->get('/settings/recycle-bin');
+ $redirectReq->assertNotificationContains('Restored '.$itemCount.' total items from the recycle bin');
+ }
+
+ public function test_permanent_delete()
+ {
+ $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+ $this->asEditor()->delete($book->getUrl());
+ $deletion = Deletion::query()->firstOrFail();
+
+ $deleteReq = $this->asAdmin()->delete("/settings/recycle-bin/{$deletion->id}");
+ $deleteReq->assertRedirect('/settings/recycle-bin');
+ $this->assertTrue(Deletion::query()->count() === 0);
+
+ $this->assertDatabaseMissing('books', ['id' => $book->id]);
+ $this->assertDatabaseMissing('pages', ['id' => $book->pages->first()->id]);
+ $this->assertDatabaseMissing('chapters', ['id' => $book->chapters->first()->id]);
+
+ $itemCount = 1 + $book->pages->count() + $book->chapters->count();
+ $redirectReq = $this->get('/settings/recycle-bin');
+ $redirectReq->assertNotificationContains('Deleted '.$itemCount.' total items from the recycle bin');
+ }
+
+ public function test_permanent_entity_delete_updates_existing_activity_with_entity_name()
+ {
+ $page = Page::query()->firstOrFail();
+ $this->asEditor()->delete($page->getUrl());
+ $deletion = $page->deletions()->firstOrFail();
+
+ $this->assertDatabaseHas('activities', [
+ '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,
+ 'entity_type' => $page->getMorphClass(),
+ ]);
+
+ $this->assertDatabaseHas('activities', [
+ 'type' => 'page_delete',
+ 'entity_id' => null,
+ 'entity_type' => null,
+ 'detail' => $page->name,
+ ]);
+ }
+
+ public function test_auto_clear_functionality_works()
+ {
+ config()->set('app.recycle_bin_lifetime', 5);
+ $page = Page::query()->firstOrFail();
+ $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
+
+ $this->asEditor()->delete($page->getUrl());
+ $this->assertDatabaseHas('pages', ['id' => $page->id]);
+ $this->assertEquals(1, Deletion::query()->count());
+
+ Carbon::setTestNow(Carbon::now()->addDays(6));
+ $this->asEditor()->delete($otherPage->getUrl());
+ $this->assertEquals(1, Deletion::query()->count());
+
+ $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+ }
+
+ public function test_auto_clear_functionality_with_negative_time_keeps_forever()
+ {
+ config()->set('app.recycle_bin_lifetime', -1);
+ $page = Page::query()->firstOrFail();
+ $otherPage = Page::query()->where('id', '!=', $page->id)->firstOrFail();
+
+ $this->asEditor()->delete($page->getUrl());
+ $this->assertEquals(1, Deletion::query()->count());
+
+ Carbon::setTestNow(Carbon::now()->addDays(6000));
+ $this->asEditor()->delete($otherPage->getUrl());
+ $this->assertEquals(2, Deletion::query()->count());
+
+ $this->assertDatabaseHas('pages', ['id' => $page->id]);
+ }
+
+ public function test_auto_clear_functionality_with_zero_time_deletes_instantly()
+ {
+ config()->set('app.recycle_bin_lifetime', 0);
+ $page = Page::query()->firstOrFail();
+
+ $this->asEditor()->delete($page->getUrl());
+ $this->assertDatabaseMissing('pages', ['id' => $page->id]);
+ $this->assertEquals(0, Deletion::query()->count());
+ }
+
+ public function test_restore_flow_when_restoring_nested_delete_first()
+ {
+ $book = Book::query()->whereHas('pages')->whereHas('chapters')->with(['pages', 'chapters'])->firstOrFail();
+ $chapter = $book->chapters->first();
+ $this->asEditor()->delete($chapter->getUrl());
+ $this->asEditor()->delete($book->getUrl());
+
+ $bookDeletion = $book->deletions()->first();
+ $chapterDeletion = $chapter->deletions()->first();
+
+ $chapterRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$chapterDeletion->id}/restore");
+ $chapterRestoreView->assertStatus(200);
+ $chapterRestoreView->assertSeeText($chapter->name);
+
+ $chapterRestore = $this->post("/settings/recycle-bin/{$chapterDeletion->id}/restore");
+ $chapterRestore->assertRedirect("/settings/recycle-bin");
+ $this->assertDatabaseMissing("deletions", ["id" => $chapterDeletion->id]);
+
+ $chapter->refresh();
+ $this->assertNotNull($chapter->deleted_at);
+
+ $bookRestoreView = $this->asAdmin()->get("/settings/recycle-bin/{$bookDeletion->id}/restore");
+ $bookRestoreView->assertStatus(200);
+ $bookRestoreView->assertSeeText($chapter->name);
+
+ $this->post("/settings/recycle-bin/{$bookDeletion->id}/restore");
+ $chapter->refresh();
+ $this->assertNull($chapter->deleted_at);
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php namespace Tests;
+
+
+use Illuminate\Support\Str;
+
+class SecurityHeaderTest extends TestCase
+{
+
+ public function test_cookies_samesite_lax_by_default()
+ {
+ $resp = $this->get("/");
+ foreach ($resp->headers->getCookies() as $cookie) {
+ $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("/");
+ foreach ($resp->headers->getCookies() as $cookie) {
+ $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("/");
+ foreach ($resp->headers->getCookies() as $cookie) {
+ $this->assertFalse($cookie->isSecure());
+ }
+ });
+
+ $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("/");
+ $cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
+ $frameHeaders = $cspHeaders->filter(function ($val) {
+ return Str::startsWith($val, 'frame-ancestors');
+ });
+
+ $this->assertTrue($frameHeaders->count() === 1);
+ $this->assertEquals('frame-ancestors \'self\'', $frameHeaders->first());
+ }
+
+ 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("/");
+ $cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
+ $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;
use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
+use Illuminate\Http\Response;
use Illuminate\Support\Env;
use Illuminate\Support\Facades\Log;
use Mockery;
use Monolog\Handler\TestHandler;
use Monolog\Logger;
use Throwable;
+use Illuminate\Foundation\Testing\Assert as PHPUnit;
trait SharedTestHelpers
{
/**
* Give the given user some permissions.
- * @param User $user
- * @param array $permissions
*/
- protected function giveUserPermissions(User $user, $permissions = [])
+ protected function giveUserPermissions(User $user, array $permissions = [])
{
$newRole = $this->createNewRole($permissions);
$user->attachRole($newRole);
$user->load('roles');
- $user->permissions(false);
+ $user->clearPermissionCache();
}
/**
*/
protected function assertPermissionError($response)
{
- if ($response instanceof BrowserKitTest) {
- $response = \Illuminate\Foundation\Testing\TestResponse::fromBaseResponse($response->response);
- }
+ PHPUnit::assertTrue($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response contains a permission error.");
+ }
+
+ /**
+ * Assert a permission error has occurred.
+ */
+ protected function assertNotPermissionError($response)
+ {
+ PHPUnit::assertFalse($this->isPermissionError($response->baseResponse ?? $response->response), "Failed asserting the response does not contain a permission error.");
+ }
- $response->assertRedirect('/');
- $this->assertSessionHas('error');
- $error = session()->pull('error');
- $this->assertStringStartsWith('You do not have permission to access', $error);
+ /**
+ * Check if the given response is a permission error.
+ */
+ private function isPermissionError($response): bool
+ {
+ return $response->status() === 302
+ && $response->headers->get('Location') === url('/')
+ && strpos(session()->pull('error', ''), 'You do not have permission to access') === 0;
}
/**
<?php namespace Tests;
-use BookStack\Entities\Entity;
+use BookStack\Entities\Models\Entity;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
* Assert that an activity entry exists of the given key.
* Checks the activity belongs to the given entity if provided.
*/
- protected function assertActivityExists(string $key, Entity $entity = null)
+ protected function assertActivityExists(string $type, Entity $entity = null)
{
- $detailsToCheck = ['key' => $key];
+ $detailsToCheck = ['type' => $type];
if ($entity) {
$detailsToCheck['entity_type'] = $entity->getMorphClass();
/**
* Get the DOM Crawler for the response content.
- * @return Crawler
*/
- protected function crawler()
+ protected function crawler(): Crawler
{
if (!is_object($this->crawlerInstance)) {
$this->crawlerInstance = new Crawler($this->getContent());
/**
* Assert the response contains the specified element.
- * @param string $selector
* @return $this
*/
public function assertElementExists(string $selector)
/**
* Assert the response does not contain the specified element.
- * @param string $selector
* @return $this
*/
public function assertElementNotExists(string $selector)
/**
* Assert the response includes a specific element containing the given text.
- * @param string $selector
- * @param string $text
* @return $this
*/
public function assertElementContains(string $selector, string $text)
/**
* Assert the response does not include a specific element containing the given text.
- * @param string $selector
- * @param string $text
* @return $this
*/
public function assertElementNotContains(string $selector, string $text)
return $this;
}
+ /**
+ * Assert there's a notification within the view containing the given text.
+ * @return $this
+ */
+ public function assertNotificationContains(string $text)
+ {
+ return $this->assertElementContains('[notification]', $text);
+ }
+
/**
* Get the escaped text pattern for the constraint.
- * @param string $text
* @return string
*/
- protected function getEscapedPattern($text)
+ protected function getEscapedPattern(string $text)
{
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
<?php namespace Tests\Uploads;
+use BookStack\Entities\Tools\TrashCan;
+use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Attachment;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Uploads\AttachmentService;
+use Illuminate\Http\UploadedFile;
use Tests\TestCase;
+use Tests\TestResponse;
class AttachmentTest extends TestCase
{
/**
* Get a test file that can be uploaded
- * @param $fileName
- * @return \Illuminate\Http\UploadedFile
*/
- protected function getTestFile($fileName)
+ protected function getTestFile(string $fileName): UploadedFile
{
- return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
+ return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
}
/**
* Uploads a file with the given name.
- * @param $name
- * @param int $uploadedTo
- * @return \Illuminate\Foundation\Testing\TestResponse
*/
- protected function uploadFile($name, $uploadedTo = 0)
+ 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
+ */
+ protected function createAttachment(Page $page): Attachment
+ {
+ $this->post('attachments/link', [
+ 'attachment_link_url' => 'https://example.com',
+ 'attachment_link_name' => 'Example Attachment Link',
+ 'attachment_link_uploaded_to' => $page->id,
+ ]);
+
+ return Attachment::query()->latest()->first();
+ }
+
/**
* Delete all uploaded files.
* To assist with cleanup.
*/
protected function deleteUploads()
{
- $fileService = $this->app->make(\BookStack\Uploads\AttachmentService::class);
+ $fileService = $this->app->make(AttachmentService::class);
foreach (Attachment::all() as $file) {
$fileService->deleteFile($file);
}
$page = Page::first();
$this->asAdmin();
- $this->call('POST', 'attachments/link', [
- 'attachment_link_url' => 'https://example.com',
- 'attachment_link_name' => 'Example Attachment Link',
- 'attachment_link_uploaded_to' => $page->id,
- ]);
-
- $attachmentId = Attachment::first()->id;
-
- $update = $this->call('PUT', 'attachments/' . $attachmentId, [
+ $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'
]);
$expectedData = [
- 'id' => $attachmentId,
+ 'id' => $attachment->id,
'path' => 'https://test.example.com',
'name' => 'My new attachment name',
'uploaded_to' => $page->id
'name' => $fileName
]);
- $this->call('DELETE', $page->getUrl());
+ app(PageRepo::class)->destroy($page);
+ app(TrashCan::class)->empty();
$this->assertDatabaseMissing('attachments', [
'name' => $fileName
$this->deleteUploads();
}
+
+ public function test_data_and_js_links_cannot_be_attached_to_a_page()
+ {
+ $page = Page::first();
+ $this->asAdmin();
+
+ $badLinks = [
+ 'javascript:alert("bunny")',
+ ' 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>",
+ ];
+
+ foreach ($badLinks as $badLink) {
+ $linkReq = $this->post('attachments/link', [
+ 'attachment_link_url' => $badLink,
+ 'attachment_link_name' => 'Example Attachment Link',
+ 'attachment_link_uploaded_to' => $page->id,
+ ]);
+ $linkReq->assertStatus(422);
+ $this->assertDatabaseMissing('attachments', [
+ 'path' => $badLink,
+ ]);
+ }
+
+ $attachment = $this->createAttachment($page);
+
+ foreach ($badLinks as $badLink) {
+ $linkReq = $this->put('attachments/' . $attachment->id, [
+ 'attachment_edit_url' => $badLink,
+ 'attachment_edit_name' => 'Example Attachment Link',
+ ]);
+ $linkReq->assertStatus(422);
+ $this->assertDatabaseMissing('attachments', [
+ 'path' => $badLink,
+ ]);
+ }
+ }
}
<?php namespace Tests\Uploads;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Uploads\Image;
use Tests\TestCase;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Uploads\Image;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use BookStack\Uploads\ImageService;
use Illuminate\Support\Str;
use Tests\TestCase;
<?php namespace Tests\Uploads;
-use BookStack\Entities\Page;
+use BookStack\Entities\Models\Page;
use Illuminate\Http\UploadedFile;
trait UsesImages
-<?php namespace Test\User;
+<?php namespace Tests\User;
+use BookStack\Actions\ActivityType;
use BookStack\Api\ApiToken;
use Carbon\Carbon;
use Tests\TestCase;
$this->assertTrue(strlen($secret) === 32);
$this->assertSessionHas('success');
+ $this->assertActivityExists(ActivityType::API_TOKEN_CREATE);
}
public function test_create_with_no_expiry_sets_expiry_hundred_years_away()
$this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id]));
$this->assertSessionHas('success');
+ $this->assertActivityExists(ActivityType::API_TOKEN_UPDATE);
}
public function test_token_update_with_blank_expiry_sets_to_hundred_years_away()
$resp = $this->delete($tokenUrl);
$resp->assertRedirect($editor->getEditUrl('#api_tokens'));
$this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);
+ $this->assertActivityExists(ActivityType::API_TOKEN_DELETE);
}
public function test_user_manage_can_delete_token_without_api_permission_themselves()
--- /dev/null
+<?php namespace Tests\User;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class UserManagementTest extends TestCase
+{
+
+ public function test_delete()
+ {
+ $editor = $this->getEditor();
+ $resp = $this->asAdmin()->delete("settings/users/{$editor->id}");
+ $resp->assertRedirect("/settings/users");
+ $resp = $this->followRedirects($resp);
+
+ $resp->assertSee("User successfully removed");
+ $this->assertActivityExists(ActivityType::USER_DELETE);
+
+ $this->assertDatabaseMissing('users', ['id' => $editor->id]);
+ }
+
+ public function test_delete_offers_migrate_option()
+ {
+ $editor = $this->getEditor();
+ $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete");
+ $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();
+
+ $this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]);
+ $this->assertDatabaseHas('pages', [
+ 'id' => $page->id,
+ 'owned_by' => $newOwner->id,
+ ]);
+ }
+}
\ No newline at end of file
-<?php namespace Test\User;
+<?php namespace Tests\User;
use Tests\TestCase;
-<?php namespace Test\User;
+<?php namespace Tests\User;
use Activity;
+use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
-use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Models\Bookshelf;
use Tests\BrowserKitTest;
class UserProfileTest extends BrowserKitTest
$newUser = $this->getNewBlankUser();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
- Activity::add($entities['book'], 'book_update', $entities['book']->id);
- Activity::add($entities['page'], 'page_create', $entities['book']->id);
+ Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
+ Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
$this->asAdmin()->visit('/user/' . $newUser->id)
->seeInElement('#recent-user-activity', 'updated book')
$newUser = $this->getNewBlankUser();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
- Activity::add($entities['book'], 'book_update', $entities['book']->id);
- Activity::add($entities['page'], 'page_create', $entities['book']->id);
+ Activity::addForEntity($entities['book'], ActivityType::BOOK_UPDATE);
+ Activity::addForEntity($entities['page'], ActivityType::PAGE_CREATE);
$this->asAdmin()->visit('/')->clickInElement('#recent-activity', $newUser->name)
->seePageIs('/user/' . $newUser->id)