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\Entity;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
-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'),
'locale' => env('APP_LANG', 'en'),
// Locales available
- 'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
+ 'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
// Application Fallback Locale
'fallback_locale' => 'en',
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);
}
/**
-<?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();
}
];
$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\Entities\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;
+ }
+}
/**
* Array of right-to-left locales
- * @var array
*/
protected $rtlLocales = ['ar', 'he'];
/**
* Map of BookStack locale names to best-estimate system locale names.
- * @var array
*/
protected $localeMap = [
'ar' => 'ar',
'ja' => 'ja',
'ko' => 'ko_KR',
'nl' => 'nl_NL',
+ 'nb' => 'nb_NO',
'pl' => 'pl_PL',
'pt' => 'pl_PT',
'pt_BR' => 'pt_BR',
--- /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;
- });
-
- 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;
- });
-
// 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');
<?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;
/**
* 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);
"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": [
"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": {
<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"/>
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:
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)
+++ /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',
'ja' => '日本語',
'ko' => '한국어',
'nl' => 'Nederlands',
+ 'nb' => 'Norsk (Bokmål)',
'pl' => 'Polski',
'pt_BR' => 'Português do Brasil',
'ru' => 'Русский',
--- /dev/null
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+ // Pages
+ 'page_create' => 'opprettet side',
+ 'page_create_notification' => 'Siden ble opprettet',
+ 'page_update' => 'oppdaterte side',
+ 'page_update_notification' => 'Siden ble oppdatert',
+ 'page_delete' => 'slettet side',
+ 'page_delete_notification' => 'Siden ble slettet',
+ 'page_restore' => 'gjenopprettet side',
+ 'page_restore_notification' => 'Siden ble gjenopprettet',
+ 'page_move' => 'flyttet side',
+
+ // Chapters
+ 'chapter_create' => 'opprettet kapittel',
+ 'chapter_create_notification' => 'Kapittelet ble opprettet',
+ 'chapter_update' => 'oppdaterte kapittel',
+ 'chapter_update_notification' => 'Kapittelet ble oppdatert',
+ 'chapter_delete' => 'slettet kapittel',
+ 'chapter_delete_notification' => 'Kapittelet ble slettet',
+ 'chapter_move' => 'flyttet kapittel
+ ',
+
+ // Books
+ 'book_create' => 'opprettet bok',
+ 'book_create_notification' => 'Boken ble opprettet',
+ 'book_update' => 'oppdaterte bok',
+ 'book_update_notification' => 'Boken ble oppdatert',
+ 'book_delete' => 'slettet bok',
+ 'book_delete_notification' => 'Boken ble slettet',
+ 'book_sort' => 'sorterte bok',
+ 'book_sort_notification' => 'Boken ble omsortert',
+
+ // Bookshelves
+ 'bookshelf_create' => 'opprettet bokhylle',
+ 'bookshelf_create_notification' => 'Bokhyllen ble opprettet',
+ 'bookshelf_update' => 'oppdaterte bokhylle',
+ 'bookshelf_update_notification' => 'Bokhyllen ble oppdatert',
+ 'bookshelf_delete' => 'slettet bokhylle',
+ 'bookshelf_delete_notification' => 'Bokhyllen ble slettet',
+
+ // Other
+ 'commented_on' => 'kommenterte på',
+];
--- /dev/null
+<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
+return [
+
+ 'failed' => 'Disse detaljene samsvarer ikke med det vi har på bok.',
+ 'throttle' => 'For mange forsøk, prøv igjen om :seconds sekunder.',
+
+ // Login & Register
+ 'sign_up' => 'Registrer deg',
+ 'log_in' => 'Logg inn',
+ 'log_in_with' => 'Logg inn med :socialDriver',
+ 'sign_up_with' => 'Registrer med :socialDriver',
+ 'logout' => 'Logg ut',
+
+ 'name' => 'Navn',
+ 'username' => 'Brukernavn',
+ 'email' => 'E-post',
+ 'password' => 'Passord',
+ 'password_confirm' => 'Bekreft passord',
+ 'password_hint' => 'Må inneholde 7 tegn',
+ 'forgot_password' => 'Glemt passord?',
+ 'remember_me' => 'Husk meg',
+ 'ldap_email_hint' => 'Oppgi en e-post for denne kontoen.',
+ 'create_account' => 'Opprett konto',
+ 'already_have_account' => 'Har du allerede en konto?',
+ 'dont_have_account' => 'Mangler du en konto?',
+ 'social_login' => 'Sosiale kontoer',
+ 'social_registration' => 'Registrer via sosiale kontoer',
+ 'social_registration_text' => 'Bruk en annen tjeneste for å registrere deg.',
+
+ 'register_thanks' => 'Takk for at du registrerte deg!',
+ 'register_confirm' => 'Sjekk e-posten din for informasjon som gir deg tilgang til :appName.',
+ 'registrations_disabled' => 'Registrering er deaktivert.',
+ 'registration_email_domain_invalid' => 'Du kan ikke bruke det domenet for å registrere en konto.',
+ 'register_success' => 'Takk for registreringen! Du kan nå logge inn på tjenesten.',
+
+
+ // Password Reset
+ 'reset_password' => 'Nullstille passord',
+ 'reset_password_send_instructions' => 'Oppgi e-posten som er koblet til kontoen din, så sender vi en epost hvor du kan nullstille passordet.',
+ 'reset_password_send_button' => 'Send nullstillingslenke',
+ 'reset_password_sent' => 'En nullstillingslenke ble sendt til :email om den eksisterer i systemet.',
+ 'reset_password_success' => 'Passordet ble nullstilt.',
+ 'email_reset_subject' => 'Nullstill ditt :appName passord',
+ 'email_reset_text' => 'Du mottar denne eposten fordi det er blitt bedt om en nullstilling av passord på denne kontoen.',
+ 'email_reset_not_requested' => 'Om det ikke var deg, så trenger du ikke foreta deg noe.',
+
+
+ // Email Confirmation
+ 'email_confirm_subject' => 'Bekreft epost-adressen for :appName',
+ 'email_confirm_greeting' => 'Takk for at du registrerte deg for :appName!',
+ 'email_confirm_text' => 'Bekreft e-posten din ved å trykke på knappen nedenfor:',
+ 'email_confirm_action' => 'Bekreft e-post',
+ 'email_confirm_send_error' => 'Bekreftelse er krevd av systemet, men systemet kan ikke sende disse. Kontakt admin for å løse problemet.',
+ 'email_confirm_success' => 'E-posten din er bekreftet!',
+ 'email_confirm_resent' => 'Bekreftelsespost ble sendt, sjekk innboksen din.',
+
+ 'email_not_confirmed' => 'E-posten er ikke bekreftet.',
+ 'email_not_confirmed_text' => 'Epost-adressen er ennå ikke bekreftet.',
+ 'email_not_confirmed_click_link' => 'Trykk på lenken i e-posten du fikk vedrørende din registrering.',
+ 'email_not_confirmed_resend' => 'Om du ikke finner den i innboksen eller søppelboksen, kan du få tilsendt ny ved å trykke på knappen under.',
+ 'email_not_confirmed_resend_button' => 'Send bekreftelsespost på nytt',
+
+ // User Invite
+ 'user_invite_email_subject' => 'Du har blitt invitert til :appName!',
+ 'user_invite_email_greeting' => 'En konto har blitt opprettet for deg på :appName.',
+ 'user_invite_email_text' => 'Trykk på knappen under for å opprette et sikkert passord:',
+ 'user_invite_email_action' => 'Angi passord',
+ 'user_invite_page_welcome' => 'Velkommen til :appName!',
+ 'user_invite_page_text' => 'For å fullføre prosessen må du oppgi et passord som sikrer din konto på :appName for fremtidige besøk.',
+ 'user_invite_page_confirm_button' => 'Bekreft passord',
+ 'user_invite_success' => 'Passordet er angitt, du kan nå bruke :appName!'
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+ // Buttons
+ 'cancel' => 'Avbryt',
+ 'confirm' => 'Bekreft',
+ 'back' => 'Tilbake',
+ 'save' => 'Lagre',
+ 'continue' => 'Fortsett',
+ 'select' => 'Velg',
+ 'toggle_all' => 'Bytt alle',
+ 'more' => 'Mer',
+
+ // Form Labels
+ 'name' => 'Navn',
+ 'description' => 'Beskrivelse',
+ 'role' => 'Rolle',
+ 'cover_image' => 'Bokomslag',
+ 'cover_image_description' => 'Bildet bør være ca. 440x250px.',
+
+ // Actions
+ 'actions' => 'Handlinger',
+ 'view' => 'Vis',
+ 'view_all' => 'Vis alle',
+ 'create' => 'Opprett',
+ 'update' => 'Oppdater',
+ 'edit' => 'Rediger',
+ 'sort' => 'Sorter',
+ 'move' => 'Flytt',
+ 'copy' => 'Kopier',
+ 'reply' => 'Svar',
+ 'delete' => 'Slett',
+ 'delete_confirm' => 'Bekreft sletting',
+ 'search' => 'Søk',
+ 'search_clear' => 'Nullstill søk',
+ 'reset' => 'Nullstill',
+ 'remove' => 'Fjern',
+ 'add' => 'Legg til',
+ 'fullscreen' => 'Fullskjerm',
+
+ // Sort Options
+ 'sort_options' => 'Sorteringsalternativer',
+ 'sort_direction_toggle' => 'Sorteringsretning',
+ 'sort_ascending' => 'Stigende sortering',
+ 'sort_descending' => 'Synkende sortering',
+ 'sort_name' => 'Navn',
+ 'sort_created_at' => 'Dato opprettet',
+ 'sort_updated_at' => 'Dato oppdatert',
+
+ // Misc
+ 'deleted_user' => 'Slett bruker',
+ 'no_activity' => 'Ingen aktivitet å vise',
+ 'no_items' => 'Ingen ting å vise',
+ 'back_to_top' => 'Hopp til toppen',
+ 'toggle_details' => 'Vis/skjul detaljer',
+ 'toggle_thumbnails' => 'Vis/skjul miniatyrbilder',
+ 'details' => 'Detaljer',
+ 'grid_view' => 'Rutenettvisning',
+ 'list_view' => 'Listevisning',
+ 'default' => 'Standard',
+ 'breadcrumb' => 'Brødsmuler',
+
+ // Header
+ 'profile_menu' => 'Profilmeny',
+ 'view_profile' => 'Vis profil',
+ 'edit_profile' => 'Endre Profile',
+ 'dark_mode' => 'Kveldsmodus',
+ 'light_mode' => 'Dagmodus',
+
+ // Layout tabs
+ 'tab_info' => 'Informasjon',
+ 'tab_content' => 'Innhold',
+
+ // Email Content
+ 'email_action_help' => 'Om du har problemer med å trykke på «:actionText»-knappen, bruk nettadressen under for å gå direkte dit:',
+ 'email_rights' => 'Kopibeskyttet',
+];
--- /dev/null
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+ // Image Manager
+ 'image_select' => 'Velg bilde',
+ 'image_all' => 'Alle',
+ 'image_all_title' => 'Vis alle bilder',
+ 'image_book_title' => 'Vis bilder som er lastet opp i denne boken',
+ 'image_page_title' => 'Vis bilder lastet opp til denne siden',
+ 'image_search_hint' => 'Søk på bilder etter navn',
+ 'image_uploaded' => 'Opplastet :uploadedDate',
+ 'image_load_more' => 'Last in flere',
+ 'image_image_name' => 'Bildenavn',
+ 'image_delete_used' => 'Dette bildet er brukt på sidene nedenfor.',
+ 'image_delete_confirm_text' => 'Vil du slette dette bildet?',
+ 'image_select_image' => 'Velg bilde',
+ 'image_dropzone' => 'Dra og slipp eller trykk her for å laste opp bilder',
+ 'images_deleted' => 'Bilder slettet',
+ 'image_preview' => 'Hurtigvisning av bilder',
+ 'image_upload_success' => 'Bilde ble lastet opp',
+ 'image_update_success' => 'Bildedetaljer ble oppdatert',
+ 'image_delete_success' => 'Bilde ble slettet',
+ 'image_upload_remove' => 'Fjern',
+
+ // Code Editor
+ 'code_editor' => 'Endre kode',
+ 'code_language' => 'Kodespråk',
+ 'code_content' => 'Kodeinnhold',
+ 'code_session_history' => 'Sesjonshistorikk',
+ 'code_save' => 'Lagre kode',
+];
--- /dev/null
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+ // Shared
+ 'recently_created' => 'Nylig opprettet',
+ 'recently_created_pages' => 'Nylig opprettede sider',
+ 'recently_updated_pages' => 'Nylig oppdaterte sider',
+ 'recently_created_chapters' => 'Nylig opprettede kapitler',
+ 'recently_created_books' => 'Nylig opprettede bøker',
+ 'recently_created_shelves' => 'Nylig opprettede bokhyller',
+ 'recently_update' => 'Nylig oppdatert',
+ 'recently_viewed' => 'Nylig vist',
+ 'recent_activity' => 'Nylig aktivitet',
+ 'create_now' => 'Opprett en nå',
+ 'revisions' => 'Revisjoner',
+ 'meta_revision' => 'Revisjon #:revisionCount',
+ 'meta_created' => 'Opprettet :timeLength',
+ 'meta_created_name' => 'Opprettet :timeLength av :user',
+ 'meta_updated' => 'Oppdatert :timeLength',
+ 'meta_updated_name' => 'Oppdatert :timeLength av :user',
+ 'entity_select' => 'Velg entitet',
+ 'images' => 'Bilder',
+ 'my_recent_drafts' => 'Mine nylige utkast',
+ 'my_recently_viewed' => 'Mine nylige visninger',
+ 'no_pages_viewed' => 'Du har ikke sett på noen sider',
+ 'no_pages_recently_created' => 'Ingen sider har nylig blitt opprettet',
+ 'no_pages_recently_updated' => 'Ingen sider har nylig blitt oppdatert',
+ 'export' => 'Eksporter',
+ 'export_html' => 'Nettside med alt',
+ 'export_pdf' => 'PDF Fil',
+ 'export_text' => 'Tekstfil',
+
+ // Permissions and restrictions
+ 'permissions' => 'Tilganger',
+ 'permissions_intro' => 'Når disse er tillatt, vil disse tillatelsene ha prioritet over alle angitte rolletillatelser.',
+ 'permissions_enable' => 'Aktiver egendefinerte tillatelser',
+ 'permissions_save' => 'Lagre tillatelser',
+
+ // Search
+ 'search_results' => 'Søkeresultater',
+ 'search_total_results_found' => ':count resultater funnet|:count totalt',
+ 'search_clear' => 'Nullstill søk',
+ 'search_no_pages' => 'Ingen sider passer med søket',
+ 'search_for_term' => 'Søk etter :term',
+ 'search_more' => 'Flere resultater',
+ 'search_advanced' => 'Avansert søk',
+ 'search_terms' => 'Søkeord',
+ 'search_content_type' => 'Innholdstype',
+ 'search_exact_matches' => 'Eksakte ord',
+ 'search_tags' => 'Søk på merker',
+ 'search_options' => 'ALternativer',
+ 'search_viewed_by_me' => 'Sett av meg',
+ 'search_not_viewed_by_me' => 'Ikke sett av meg',
+ 'search_permissions_set' => 'Tilganger er angitt',
+ 'search_created_by_me' => 'Opprettet av meg',
+ 'search_updated_by_me' => 'Oppdatert av meg',
+ 'search_date_options' => 'Datoalternativer',
+ 'search_updated_before' => 'Oppdatert før',
+ 'search_updated_after' => 'Oppdatert etter',
+ 'search_created_before' => 'Opprettet før',
+ 'search_created_after' => 'Opprettet etter',
+ 'search_set_date' => 'Angi dato',
+ 'search_update' => 'Oppdater søk',
+
+ // Shelves
+ 'shelf' => 'Hylle',
+ 'shelves' => 'Hyller',
+ 'x_shelves' => ':count hylle|:count hyller',
+ 'shelves_long' => 'Bokhyller',
+ 'shelves_empty' => 'Ingen bokhyller er opprettet',
+ 'shelves_create' => 'Opprett ny bokhylle',
+ 'shelves_popular' => 'Populære bokhyller',
+ 'shelves_new' => 'Nye bokhyller',
+ 'shelves_new_action' => 'Ny bokhylle',
+ 'shelves_popular_empty' => 'De mest populære bokhyllene blir vist her.',
+ 'shelves_new_empty' => 'Nylig opprettede bokhyller vises her.',
+ 'shelves_save' => 'Lagre hylle',
+ 'shelves_books' => 'Bøker på denne hyllen',
+ 'shelves_add_books' => 'Legg til bøker på denne hyllen',
+ 'shelves_drag_books' => 'Dra bøker hit for å stable dem i denne hylla',
+ 'shelves_empty_contents' => 'INgen bøker er stablet i denne hylla',
+ 'shelves_edit_and_assign' => 'Endre hylla for å legge til bøker',
+ 'shelves_edit_named' => 'Endre hyllen :name',
+ 'shelves_edit' => 'Endre bokhylle',
+ 'shelves_delete' => 'Fjern bokhylle',
+ 'shelves_delete_named' => 'Fjern bokhyllen :name',
+ 'shelves_delete_explain' => "Dette vil fjerne bokhyllen ':name'. Bøkene vil ikke fjernes fra systemet.",
+ 'shelves_delete_confirmation' => 'Er du helt sikker på at du vil skru ned hylla?',
+ 'shelves_permissions' => 'Tilganger til hylla',
+ 'shelves_permissions_updated' => 'Hyllas tilganger er oppdatert',
+ 'shelves_permissions_active' => 'Hyllas tilganger er aktive',
+ 'shelves_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',
+ 'shelves_copy_permissions' => 'Kopier tilganger',
+ 'shelves_copy_permissions_explain' => 'Dette vil angi gjeldende tillatelsesinnstillinger for denne bokhyllen på alle bøkene som finnes på den. Før du aktiverer, må du forsikre deg om at endringer i tillatelsene til denne bokhyllen er lagret.',
+ 'shelves_copy_permission_success' => 'Tilgangene ble overført til :count bøker',
+
+ // Books
+ 'book' => 'Bok',
+ 'books' => 'Bøker',
+ 'x_books' => ':count bok|:count bøker',
+ 'books_empty' => 'Ingen bøker er skrevet',
+ 'books_popular' => 'Populære bøker',
+ 'books_recent' => 'Nylige bøker',
+ 'books_new' => 'Nye bøker',
+ 'books_new_action' => 'Ny bok',
+ 'books_popular_empty' => 'De mest populære bøkene',
+ 'books_new_empty' => 'Siste utgivelser vises her.',
+ 'books_create' => 'Skriv ny bok',
+ 'books_delete' => 'Brenn bok',
+ 'books_delete_named' => 'Brenn boken :bookName',
+ 'books_delete_explain' => 'Dette vil brenne boken «:bookName». Alle sider i boken vil fordufte for godt.',
+ 'books_delete_confirmation' => 'Er du sikker på at du vil brenne boken?',
+ 'books_edit' => 'Endre bok',
+ 'books_edit_named' => 'Endre boken :bookName',
+ 'books_form_book_name' => 'Boktittel',
+ 'books_save' => 'Lagre bok',
+ 'books_permissions' => 'Boktilganger',
+ 'books_permissions_updated' => 'Boktilganger oppdatert',
+ 'books_empty_contents' => 'Ingen sider eller kapitler finnes i denne boken.',
+ 'books_empty_create_page' => 'Skriv en ny side',
+ 'books_empty_sort_current_book' => 'Sorter innholdet i boken',
+ 'books_empty_add_chapter' => 'Start på nytt kapittel',
+ 'books_permissions_active' => 'Boktilganger er aktive',
+ 'books_search_this' => 'Søk i boken',
+ 'books_navigation' => 'Boknavigasjon',
+ 'books_sort' => 'Sorter bokinnhold',
+ 'books_sort_named' => 'Sorter boken :bookName',
+ 'books_sort_name' => 'Sorter på navn',
+ 'books_sort_created' => 'Sorter på opprettet dato',
+ 'books_sort_updated' => 'Sorter på oppdatert dato',
+ 'books_sort_chapters_first' => 'Kapitler først',
+ 'books_sort_chapters_last' => 'Kapitler sist',
+ 'books_sort_show_other' => 'Vis andre bøker',
+ 'books_sort_save' => 'Lagre sortering',
+
+ // Chapters
+ 'chapter' => 'Kapittel',
+ 'chapters' => 'Kapitler',
+ 'x_chapters' => ':count Kapittel|:count Kapitler',
+ 'chapters_popular' => 'Populære kapittler',
+ 'chapters_new' => 'Nytt kapittel',
+ 'chapters_create' => 'Skriv nytt kapittel',
+ 'chapters_delete' => 'Riv ut kapittel',
+ 'chapters_delete_named' => 'Riv ut kapittelet :chapterName',
+ 'chapters_delete_explain' => 'Du ønsker å rive ut kapittelet «:chapterName». Alle sidene vil bli flyttet ut av kapittelet og vil ligge direkte i boka.',
+ 'chapters_delete_confirm' => 'Er du sikker på at du vil rive ut dette kapittelet?',
+ 'chapters_edit' => 'Endre kapittel',
+ 'chapters_edit_named' => 'Endre kapittelet :chapterName',
+ 'chapters_save' => 'Lagre kapittel',
+ 'chapters_move' => 'Flytt kapittel',
+ 'chapters_move_named' => 'Flytt kapittelet :chapterName',
+ 'chapter_move_success' => 'Kapittelet ble flyttet til :bookName',
+ 'chapters_permissions' => 'Kapitteltilganger',
+ 'chapters_empty' => 'Det finnes ingen sider i dette kapittelet.',
+ 'chapters_permissions_active' => 'Kapitteltilganger er aktivert',
+ 'chapters_permissions_success' => 'Kapitteltilgager er oppdatert',
+ 'chapters_search_this' => 'Søk i dette kapittelet',
+
+ // Pages
+ 'page' => 'Side',
+ 'pages' => 'Sider',
+ 'x_pages' => ':count side|:count sider',
+ 'pages_popular' => 'Populære sider',
+ 'pages_new' => 'Ny side',
+ 'pages_attachments' => 'Vedlegg',
+ 'pages_navigation' => 'Sidenavigasjon',
+ 'pages_delete' => 'Riv ut side',
+ 'pages_delete_named' => 'Riv ut siden :pageName',
+ 'pages_delete_draft_named' => 'Kast sideutkast :pageName',
+ 'pages_delete_draft' => 'Kast sideutkast',
+ 'pages_delete_success' => 'Siden er revet ut',
+ 'pages_delete_draft_success' => 'Sideutkast er kastet',
+ 'pages_delete_confirm' => 'Er du sikker på at du vil rive ut siden?',
+ 'pages_delete_draft_confirm' => 'Er du sikker på at du vil forkaste utkastet?',
+ 'pages_editing_named' => 'Endrer :pageName',
+ 'pages_edit_draft_options' => 'Utkastsalternativer',
+ 'pages_edit_save_draft' => 'Lagre utkast',
+ 'pages_edit_draft' => 'Endre utkast',
+ 'pages_editing_draft' => 'Redigerer utkast',
+ 'pages_editing_page' => 'Redigerer side',
+ 'pages_edit_draft_save_at' => 'Ukast lagret under ',
+ 'pages_edit_delete_draft' => 'Forkast utkast',
+ 'pages_edit_discard_draft' => 'Gi opp utkast',
+ 'pages_edit_set_changelog' => 'Angi endringslogg',
+ 'pages_edit_enter_changelog_desc' => 'Gi en kort beskrivelse av endringene dine',
+ 'pages_edit_enter_changelog' => 'Se endringslogg',
+ 'pages_save' => 'Lagre side',
+ 'pages_title' => 'Sidetittel',
+ 'pages_name' => 'Sidenavn',
+ 'pages_md_editor' => 'Tekstbehandler',
+ 'pages_md_preview' => 'Forhåndsvisning',
+ 'pages_md_insert_image' => 'Lim inn bilde',
+ 'pages_md_insert_link' => 'Lim in lenke',
+ 'pages_md_insert_drawing' => 'Lim inn tegning',
+ 'pages_not_in_chapter' => 'Siden tilhører ingen kapittel',
+ 'pages_move' => 'Flytt side',
+ 'pages_move_success' => 'Siden ble flyttet til ":parentName"',
+ 'pages_copy' => 'Kopier side',
+ 'pages_copy_desination' => 'Destinasjon',
+ 'pages_copy_success' => 'Siden ble flyttet',
+ 'pages_permissions' => 'Sidetilganger',
+ 'pages_permissions_success' => 'Sidens tilganger ble endret',
+ 'pages_revision' => 'Revisjon',
+ 'pages_revisions' => 'Sidens revisjoner',
+ 'pages_revisions_named' => 'Revisjoner for :pageName',
+ 'pages_revision_named' => 'Revisjoner for :pageName',
+ 'pages_revisions_created_by' => 'Skrevet av',
+ 'pages_revisions_date' => 'Revideringsdato',
+ 'pages_revisions_number' => '#',
+ 'pages_revisions_numbered' => 'Revisjon #:id',
+ 'pages_revisions_numbered_changes' => 'Endringer på revisjon #:id',
+ 'pages_revisions_changelog' => 'Endringslogg',
+ 'pages_revisions_changes' => 'Endringer',
+ 'pages_revisions_current' => 'Siste versjon',
+ 'pages_revisions_preview' => 'Forhåndsvisning',
+ 'pages_revisions_restore' => 'Gjenopprett',
+ 'pages_revisions_none' => 'Denne siden har ingen revisjoner',
+ 'pages_copy_link' => 'Kopier lenke',
+ 'pages_edit_content_link' => 'Endre innhold',
+ 'pages_permissions_active' => 'Sidetilganger er aktive',
+ 'pages_initial_revision' => 'Første publisering',
+ 'pages_initial_name' => 'Ny side',
+ 'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
+ 'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
+ 'pages_draft_edit_active' => [
+ 'start_a' => ':count forfattere har begynt å endre denne siden.',
+ 'start_b' => ':userName skriver på siden for øyeblikket',
+ 'time_a' => 'siden sist siden ble oppdatert',
+ 'time_b' => 'i løpet av de siste :minCount minuttene',
+ 'message' => ':start :time. Prøv å ikke overskriv hverandres endringer!',
+ ],
+ 'pages_draft_discarded' => 'Forkastet, viser nå siste endringer fra siden slik den er lagret.',
+ 'pages_specific' => 'Bestemt side',
+ 'pages_is_template' => 'Sidemal',
+
+ // Editor Sidebar
+ 'page_tags' => 'Sidemerker',
+ 'chapter_tags' => 'Kapittelmerker',
+ 'book_tags' => 'Bokmerker',
+ 'shelf_tags' => 'Hyllemerker',
+ 'tag' => 'Merke',
+ 'tags' => 'Merker',
+ 'tag_name' => 'Merketittel',
+ 'tag_value' => 'Merkeverdi (Valgfritt)',
+ 'tags_explain' => "Legg til merker for å kategorisere innholdet ditt. \n Du kan legge til merkeverdier for å beskrive dem ytterligere.",
+ 'tags_add' => 'Legg til flere merker',
+ 'tags_remove' => 'Fjern merke',
+ 'attachments' => 'Vedlegg',
+ 'attachments_explain' => 'Last opp vedlegg eller legg til lenker for å berike innholdet. Disse vil vises i sidestolpen på siden.',
+ 'attachments_explain_instant_save' => 'Endringer her blir lagret med en gang.',
+ 'attachments_items' => 'Vedlegg',
+ 'attachments_upload' => 'Last opp vedlegg',
+ 'attachments_link' => 'Fest lenke',
+ 'attachments_set_link' => 'Angi lenke',
+ 'attachments_delete' => 'Er du sikker på at du vil fjerne vedlegget?',
+ 'attachments_dropzone' => 'Dra og slipp eller trykk her for å feste vedlegg',
+ 'attachments_no_files' => 'Ingen vedlegg er lastet opp',
+ 'attachments_explain_link' => 'Du kan feste lenker til denne. Det kan være henvisning til andre sider, bøker etc. eller lenker fra nettet.',
+ 'attachments_link_name' => 'Lenkenavn',
+ 'attachment_link' => 'Vedleggslenke',
+ 'attachments_link_url' => 'Lenke til vedlegg',
+ 'attachments_link_url_hint' => 'Adresse til lenke eller vedlegg',
+ 'attach' => 'Fest',
+ 'attachments_insert_link' => 'Fest vedleggslenke',
+ 'attachments_edit_file' => 'Endre vedlegg',
+ 'attachments_edit_file_name' => 'Vedleggsnavn',
+ 'attachments_edit_drop_upload' => 'Dra og slipp eller trykk her for å oppdatere eller overskrive',
+ 'attachments_order_updated' => 'Vedleggssortering endret',
+ 'attachments_updated_success' => 'Vedleggsdetaljer endret',
+ 'attachments_deleted' => 'Vedlegg fjernet',
+ 'attachments_file_uploaded' => 'Vedlegg ble lastet opp',
+ 'attachments_file_updated' => 'Vedlegget ble oppdatert',
+ 'attachments_link_attached' => 'Lenken ble festet til siden',
+ 'templates' => 'Maler',
+ 'templates_set_as_template' => 'Siden er en mal',
+ 'templates_explain_set_as_template' => 'Du kan angi denne siden som en mal slik at innholdet kan brukes når du oppretter andre sider. Andre brukere vil kunne bruke denne malen hvis de har visningstillatelser for denne siden.',
+ 'templates_replace_content' => 'Bytt sideinnhold',
+ 'templates_append_content' => 'Legg til neders på siden',
+ 'templates_prepend_content' => 'Legg til øverst på siden',
+
+ // Profile View
+ 'profile_user_for_x' => 'Medlem i :time',
+ 'profile_created_content' => 'Har skrevet',
+ 'profile_not_created_pages' => ':userName har ikke forfattet noen sider',
+ 'profile_not_created_chapters' => ':userName har ikke opprettet noen kapitler',
+ 'profile_not_created_books' => ':userName har ikke laget noen bøker',
+ 'profile_not_created_shelves' => ':userName har ikke hengt opp noen hyller',
+
+ // Comments
+ 'comment' => 'Kommentar',
+ 'comments' => 'Kommentarer',
+ 'comment_add' => 'Skriv kommentar',
+ 'comment_placeholder' => 'Skriv en kommentar her',
+ 'comment_count' => '{0} Ingen kommentarer|{1} 1 kommentar|[2,*] :count kommentarer',
+ 'comment_save' => 'Publiser kommentar',
+ 'comment_saving' => 'Publiserer ...',
+ 'comment_deleting' => 'Fjerner...',
+ 'comment_new' => 'Ny kommentar',
+ 'comment_created' => 'kommenterte :createDiff',
+ 'comment_updated' => 'Oppdatert :updateDiff av :username',
+ 'comment_deleted_success' => 'Kommentar fjernet',
+ 'comment_created_success' => 'Kommentar skrevet',
+ 'comment_updated_success' => 'Kommentar endret',
+ 'comment_delete_confirm' => 'Er du sikker på at du vil fjerne kommentaren?',
+ 'comment_in_reply_to' => 'Som svar til :commentId',
+
+ // Revision
+ 'revision_delete_confirm' => 'Vil du slette revisjonen?',
+ 'revision_restore_confirm' => 'Vil du gjenopprette revisjonen? Innholdet på siden vil bli overskrevet med denne revisjonen.',
+ 'revision_delete_success' => 'Revisjonen ble slettet',
+ 'revision_cannot_delete_latest' => 'CKan ikke slette siste revisjon.'
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+ // Permissions
+ 'permission' => 'Du har ikke tilgang til å se denne siden.',
+ 'permissionJson' => 'Du har ikke tilgang til å utføre denne handlingen.',
+
+ // Auth
+ 'error_user_exists_different_creds' => 'En konto med :email finnes allerede, men har andre detaljer.',
+ 'email_already_confirmed' => 'E-posten er allerede bekreftet, du kan forsøke å logge inn.',
+ 'email_confirmation_invalid' => 'Denne bekreftelseskoden er allerede benyttet eller utgått. Prøv å registrere på nytt.',
+ 'email_confirmation_expired' => 'Bekreftelseskoden er allerede utgått, en ny e-post er sendt.',
+ 'email_confirmation_awaiting' => 'Du må bekrefte e-posten for denne kontoen.',
+ 'ldap_fail_anonymous' => 'LDAP kan ikke benyttes med anonym tilgang for denne tjeneren.',
+ 'ldap_fail_authed' => 'LDAP tilgang feilet med angitt DN',
+ 'ldap_extension_not_installed' => 'LDAP PHP modulen er ikke installert.',
+ 'ldap_cannot_connect' => 'Klarer ikke koble til LDAP på denne adressen',
+ 'saml_already_logged_in' => 'Allerede logget inn',
+ 'saml_user_not_registered' => 'Kontoen med navn :name er ikke registert, registrering er også deaktivert.',
+ 'saml_no_email_address' => 'Denne kontoinformasjonen finnes ikke i det eksterne autentiseringssystemet.',
+ 'saml_invalid_response_id' => 'Forespørselen fra det eksterne autentiseringssystemet gjenkjennes ikke av en prosess som startes av dette programmet. Å navigere tilbake etter pålogging kan forårsake dette problemet.',
+ 'saml_fail_authed' => 'Innlogging gjennom :system feilet. Fikk ikke kontakt med autentiseringstjeneren.',
+ 'social_no_action_defined' => 'Ingen handlinger er definert',
+ 'social_login_bad_response' => "Feilmelding mottat fra :socialAccount innloggingstjeneste: \n:error",
+ 'social_account_in_use' => 'Denne :socialAccount kontoen er allerede registrert, Prøv å logge inn med :socialAccount alternativet.',
+ 'social_account_email_in_use' => 'E-posten :email er allerede i bruk. Har du allerede en konto hos :socialAccount kan dette angis fra profilsiden din.',
+ 'social_account_existing' => 'Denne :socialAccount er allerede koblet til din konto.',
+ 'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
+ 'social_account_not_used' => 'Denne :socialAccount konten er ikke koblet til noen konto, angi denne i profilinnstillingene dine. ',
+ 'social_account_register_instructions' => 'Har du ikke en konto her ennå, kan du benytte :socialAccount alternativet for å registrere deg.',
+ 'social_driver_not_found' => 'Autentiseringstjeneste fra sosiale medier er ikke installert',
+ 'social_driver_not_configured' => 'Dine :socialAccount innstilliner er ikke angitt.',
+ 'invite_token_expired' => 'Invitasjonslenken har utgått, du kan forsøke å be om nytt passord istede.',
+
+ // System
+ 'path_not_writable' => 'Filstien :filePath aksepterer ikke filer, du må sjekke filstitilganger i systemet.',
+ 'cannot_get_image_from_url' => 'Kan ikke hente bilde fra :url',
+ 'cannot_create_thumbs' => 'Kan ikke opprette miniatyrbilder. GD PHP er ikke installert.',
+ 'server_upload_limit' => 'Vedlegget er for stort, forsøk med et mindre vedlegg.',
+ 'uploaded' => 'Tjenesten aksepterer ikke vedlegg som er så stor.',
+ 'image_upload_error' => 'Bildet kunne ikke lastes opp, forsøk igjen.',
+ 'image_upload_type_error' => 'Bildeformatet støttes ikke, forsøk med et annet format.',
+ 'file_upload_timeout' => 'Opplastingen gikk ut på tid.',
+
+ // Attachments
+ 'attachment_not_found' => 'Vedlegget ble ikke funnet',
+
+ // Pages
+ 'page_draft_autosave_fail' => 'Kunne ikke lagre utkastet, forsikre deg om at du er tilkoblet tjeneren (Har du nettilgang?)',
+ 'page_custom_home_deletion' => 'Kan ikke slette en side som er satt som forside.',
+
+ // Entities
+ 'entity_not_found' => 'Entitet ble ikke funnet',
+ 'bookshelf_not_found' => 'Bokhyllen ble ikke funnet',
+ 'book_not_found' => 'Boken ble ikke funnet',
+ 'page_not_found' => 'Siden ble ikke funnet',
+ 'chapter_not_found' => 'Kapittel ble ikke funnet',
+ 'selected_book_not_found' => 'Den valgte boken eksisterer ikke',
+ 'selected_book_chapter_not_found' => 'Den valgte boken eller kapittelet eksisterer ikke',
+ 'guests_cannot_save_drafts' => 'Gjester kan ikke lagre utkast',
+
+ // Users
+ 'users_cannot_delete_only_admin' => 'Du kan ikke kaste ut den eneste administratoren',
+ 'users_cannot_delete_guest' => 'Du kan ikke slette gjestebrukeren (Du kan deaktivere offentlig visning istede)',
+
+ // Roles
+ 'role_cannot_be_edited' => 'Denne rollen kan ikke endres',
+ 'role_system_cannot_be_deleted' => 'Denne systemrollen kan ikke slettes',
+ 'role_registration_default_cannot_delete' => 'Du kan ikke slette en rolle som er satt som registreringsrolle (rollen nye kontoer får når de registrerer seg)',
+ 'role_cannot_remove_only_admin' => 'Denne brukeren er den eneste brukeren som er tildelt administratorrollen. Tilordne administratorrollen til en annen bruker før du prøver å fjerne den her.',
+
+ // Comments
+ 'comment_list' => 'Det oppstod en feil under henting av kommentarene.',
+ 'cannot_add_comment_to_draft' => 'Du kan ikke legge til kommentarer i et utkast.',
+ 'comment_add' => 'Det oppsto en feil da kommentaren skulle legges til / oppdateres.',
+ 'comment_delete' => 'Det oppstod en feil under sletting av kommentaren.',
+ 'empty_comment' => 'Kan ikke legge til en tom kommentar.',
+
+ // Error pages
+ '404_page_not_found' => 'Siden finnes ikke',
+ 'sorry_page_not_found' => 'Beklager, siden du leter etter ble ikke funnet.',
+ 'sorry_page_not_found_permission_warning' => 'Hvis du forventet at denne siden skulle eksistere, har du kanskje ikke tillatelse til å se den.',
+ 'return_home' => 'Gå til hovedside',
+ 'error_occurred' => 'En feil oppsto',
+ 'app_down' => ':appName er nede for øyeblikket',
+ 'back_soon' => 'Den vil snart komme tilbake.',
+
+ // API errors
+ 'api_no_authorization_found' => 'Ingen autorisasjonstoken ble funnet på forespørselen',
+ 'api_bad_authorization_format' => 'Det ble funnet et autorisasjonstoken på forespørselen, men formatet virket feil',
+ 'api_user_token_not_found' => 'Ingen samsvarende API-token ble funnet for det angitte autorisasjonstokenet',
+ 'api_incorrect_token_secret' => 'Hemmeligheten som er gitt for det gitte brukte API-tokenet er feil',
+ 'api_user_no_api_permission' => 'Eieren av det brukte API-tokenet har ikke tillatelse til å ringe API-samtaler',
+ 'api_user_token_expired' => 'Autorisasjonstokenet som er brukt, har utløpt',
+
+ // Settings & Maintenance
+ 'maintenance_test_email_failure' => 'Feil kastet når du sendte en test-e-post:',
+
+];
--- /dev/null
+<?php
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
+return [
+
+ 'previous' => '« Forrige',
+ 'next' => 'Neste »',
+
+];
--- /dev/null
+<?php
+/**
+ * Password Reminder Language Lines
+ * The following language lines are the default lines which match reasons
+ * that are given by the password broker for a password update attempt has failed.
+ */
+return [
+
+ 'password' => 'Passord må inneholde minst åtte tegn og samsvarer med bekreftelsen.',
+ 'user' => "Vi finner ikke en bruker med den e-postadressen.",
+ 'token' => 'Passordet for tilbakestilling av passord er ugyldig for denne e-postadressen.',
+ 'sent' => 'Vi har sendt e-postadressen til tilbakestilling av passordet ditt!',
+ 'reset' => 'Passordet ditt har blitt tilbakestilt!',
+
+];
--- /dev/null
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+ // Common Messages
+ 'settings' => 'Innstillinger',
+ 'settings_save' => 'Lagre innstillinger',
+ 'settings_save_success' => 'Innstillinger lagret',
+
+ // App Settings
+ 'app_customization' => 'Tilpassing',
+ 'app_features_security' => 'Funksjoner og sikkerhet',
+ 'app_name' => 'Applikasjonsnavn',
+ 'app_name_desc' => 'Dette navnet vises i overskriften og i alle e-postmeldinger som sendes av systemet.',
+ 'app_name_header' => 'Vis navn i topptekst',
+ 'app_public_access' => 'Offentlig tilgang',
+ 'app_public_access_desc' => 'Hvis du aktiverer dette alternativet, kan besøkende, som ikke er logget på, få tilgang til innhold i din BookStack-forekomst.',
+ 'app_public_access_desc_guest' => 'Tilgang for offentlige besøkende kan kontrolleres gjennom "Gjest" -brukeren.',
+ 'app_public_access_toggle' => 'Tillat offentlig tilgang',
+ 'app_public_viewing' => 'Tillat offentlig visning?',
+ 'app_secure_images' => 'Høyere sikkerhet på bildeopplastinger',
+ 'app_secure_images_toggle' => 'Enable høyere sikkerhet på bildeopplastinger',
+ 'app_secure_images_desc' => 'Av ytelsesgrunner er alle bilder offentlige. Dette alternativet legger til en tilfeldig streng som er vanskelig å gjette foran bildets nettadresser. Forsikre deg om at katalogindekser ikke er aktivert for å forhindre enkel tilgang.',
+ 'app_editor' => 'Tekstbehandler',
+ 'app_editor_desc' => 'Velg hvilken tekstbehandler som skal brukes av alle brukere til å redigere sider.',
+ 'app_custom_html' => 'Tilpasset HTML-hodeinnhold',
+ 'app_custom_html_desc' => 'Alt innhold som legges til her, blir satt inn i bunnen av <head> -delen på hver side. Dette er praktisk for å overstyre stiler eller legge til analysekode.',
+ 'app_custom_html_disabled_notice' => 'Tilpasset HTML-hodeinnhold er deaktivert på denne innstillingssiden for å sikre at eventuelle endringer ødelegger noe, kan tilbakestilles.',
+ 'app_logo' => 'Applikasjonslogo',
+ 'app_logo_desc' => 'Dette bildet skal være 43 px høyt. <br> Store bilder blir nedskalert.',
+ 'app_primary_color' => 'Applikasjonens primærfarge',
+ 'app_primary_color_desc' => 'Angir primærfargen for applikasjonen inkludert banner, knapper og lenker.',
+ 'app_homepage' => 'Applikasjonens hjemmeside',
+ 'app_homepage_desc' => 'Velg en visning som skal vises på hjemmesiden i stedet for standardvisningen. Sidetillatelser ignoreres for utvalgte sider.',
+ 'app_homepage_select' => 'Velg en side',
+ 'app_disable_comments' => 'Deaktiver kommentarer',
+ 'app_disable_comments_toggle' => 'Deaktiver kommentarer',
+ 'app_disable_comments_desc' => 'Deaktiver kommentarer på tvers av alle sidene i applikasjonen. <br> Eksisterende kommentarer vises ikke.',
+
+ // Color settings
+ 'content_colors' => 'Innholdsfarger',
+ 'content_colors_desc' => 'Angir farger for alle elementene i sideorganisasjonshierarkiet. Det anbefales å lese farger med en lignende lysstyrke som standardfargene for lesbarhet.',
+ 'bookshelf_color' => 'Hyllefarge',
+ 'book_color' => 'Bokfarge',
+ 'chapter_color' => 'Kapittelfarge',
+ 'page_color' => 'Sidefarge',
+ 'page_draft_color' => 'Sideutkastsfarge',
+
+ // Registration Settings
+ 'reg_settings' => 'Registrering',
+ 'reg_enable' => 'Tillat registrering',
+ 'reg_enable_toggle' => 'Tillat registrering',
+ 'reg_enable_desc' => 'Når registrering er aktivert vil brukeren kunne registrere seg som applikasjonsbruker. Ved registrering får de en standard brukerrolle.',
+ 'reg_default_role' => 'Standard brukerrolle etter registrering',
+ 'reg_enable_external_warning' => 'Alternativet ovenfor ignoreres mens ekstern LDAP- eller SAML-autentisering er aktiv. Brukerkontoer for ikke-eksisterende medlemmer blir automatisk opprettet hvis autentisering mot det eksterne systemet i bruk lykkes.',
+ 'reg_email_confirmation' => 'E-postbekreftelse',
+ 'reg_email_confirmation_toggle' => 'Krev e-postbekreftelse',
+ 'reg_confirm_email_desc' => 'Hvis domenebegrensning brukes, vil e-postbekreftelse være nødvendig, og dette alternativet vil bli ignorert.',
+ 'reg_confirm_restrict_domain' => 'Domenebegrensning',
+ 'reg_confirm_restrict_domain_desc' => 'Skriv inn en kommaseparert liste over e-postdomener du vil begrense registreringen til. Brukerne vil bli sendt en e-post for å bekrefte adressen deres før de får lov til å kommunisere med applikasjonen. <br> Vær oppmerksom på at brukere vil kunne endre e-postadressene sine etter vellykket registrering.',
+ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt',
+
+ // Maintenance settings
+ 'maint' => 'Maintenance',
+ 'maint_image_cleanup' => 'Bildeopprydding',
+ 'maint_image_cleanup_desc' => "Skanner side og revisjonsinnhold for å sjekke hvilke bilder og tegninger som for øyeblikket er i bruk, og hvilke bilder som er overflødige. Forsikre deg om at du lager en full database og sikkerhetskopiering av bilder før du kjører denne.",
+ 'maint_image_cleanup_ignore_revisions' => 'Ignorer bilder i revisjoner',
+ 'maint_image_cleanup_run' => 'Kjør opprydding',
+ 'maint_image_cleanup_warning' => ':count potensielt ubrukte bilder ble funnet. Er du sikker på at du vil slette disse bildene?',
+ 'maint_image_cleanup_success' => ':count potensielt ubrukte bilder funnet og slettet!',
+ 'maint_image_cleanup_nothing_found' => 'Ingen ubrukte bilder funnet, ingenting slettet!',
+ 'maint_send_test_email' => 'Send en test-e-post',
+ 'maint_send_test_email_desc' => 'Dette sender en test-e-post til din e-postadresse som er angitt i profilen din.',
+ 'maint_send_test_email_run' => 'Send en test-e-post',
+ 'maint_send_test_email_success' => 'Send en test-e-post til :address',
+ 'maint_send_test_email_mail_subject' => 'Test-e-post',
+ 'maint_send_test_email_mail_greeting' => 'E-postsending ser ut til å fungere!',
+ 'maint_send_test_email_mail_text' => 'Gratulerer! Da du mottok dette e-postvarselet, ser det ut til at e-postinnstillingene dine er konfigurert riktig.',
+
+ // Audit Log
+ 'audit' => 'Revisjonslogg',
+ 'audit_desc' => 'Denne revisjonsloggen viser en liste over aktiviteter som spores i systemet. Denne listen er ufiltrert i motsetning til lignende aktivitetslister i systemet der tillatelsesfiltre brukes.',
+ 'audit_event_filter' => 'Hendelsesfilter',
+ 'audit_event_filter_no_filter' => 'Ingen filter',
+ 'audit_deleted_item' => 'Slettet ting',
+ 'audit_deleted_item_name' => 'Navn: :name',
+ 'audit_table_user' => 'Kontoholder',
+ 'audit_table_event' => 'Hendelse',
+ 'audit_table_item' => 'Relatert ting',
+ 'audit_table_date' => 'Aktivitetsdato',
+ 'audit_date_from' => 'Datoperiode fra',
+ 'audit_date_to' => 'Datoperiode til',
+
+ // Role Settings
+ 'roles' => 'Roller',
+ 'role_user_roles' => 'Kontoroller',
+ 'role_create' => 'Opprett ny rolle',
+ 'role_create_success' => 'Rolle opprettet',
+ 'role_delete' => 'Rolle slettet',
+ 'role_delete_confirm' => 'Dette vil slette rollen «:roleName».',
+ 'role_delete_users_assigned' => 'Denne rollen har :userCount kontoer koblet opp mot seg. Velg hvilke rolle du vil flytte disse til.',
+ 'role_delete_no_migration' => "Ikke flytt kontoer",
+ 'role_delete_sure' => 'Er du sikker på at du vil slette rollen?',
+ 'role_delete_success' => 'Rollen ble slettet',
+ 'role_edit' => 'Endre rolle',
+ 'role_details' => 'Rolledetaljer',
+ 'role_name' => 'Rollenavn',
+ 'role_desc' => 'Kort beskrivelse av rolle',
+ 'role_external_auth_id' => 'Ekstern godkjennings-ID',
+ 'role_system' => 'Systemtilganger',
+ 'role_manage_users' => 'Behandle kontoer',
+ 'role_manage_roles' => 'Behandle roller og rolletilganger',
+ 'role_manage_entity_permissions' => 'Behandle bok-, kapittel- og sidetilganger',
+ 'role_manage_own_entity_permissions' => 'Behandle tilganger på egne verk',
+ 'role_manage_page_templates' => 'Behandle sidemaler',
+ 'role_access_api' => 'Systemtilgang API',
+ 'role_manage_settings' => 'Behandle applikasjonsinnstillinger',
+ 'role_asset' => 'Eiendomstillatelser',
+ 'roles_system_warning' => 'Vær oppmerksom på at tilgang til noen av de ovennevnte tre tillatelsene kan tillate en bruker å endre sine egne rettigheter eller rettighetene til andre i systemet. Bare tildel roller med disse tillatelsene til pålitelige brukere.',
+ 'role_asset_desc' => 'Disse tillatelsene kontrollerer standard tilgang til eiendelene i systemet. Tillatelser til bøker, kapitler og sider overstyrer disse tillatelsene.',
+ 'role_asset_admins' => 'Administratorer får automatisk tilgang til alt innhold, men disse alternativene kan vise eller skjule UI-alternativer.',
+ 'role_all' => 'Alle',
+ 'role_own' => 'Egne',
+ 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til',
+ 'role_save' => 'Lagre rolle',
+ 'role_update_success' => 'Rollen ble oppdatert',
+ 'role_users' => 'Kontoholdere med denne rollen',
+ 'role_users_none' => 'Ingen kontoholdere er gitt denne rollen',
+
+ // Users
+ 'users' => 'Users',
+ 'user_profile' => 'Profil',
+ 'users_add_new' => 'Register ny konto',
+ 'users_search' => 'Søk i kontoer',
+ 'users_details' => 'Kontodetaljer',
+ 'users_details_desc' => 'Angi et visningsnavn og en e-postadresse for denne kontoholderen. E-postadressen vil bli brukt til å logge på applikasjonen.',
+ 'users_details_desc_no_email' => 'Angi et visningsnavn for denne kontoholderen slik at andre kan gjenkjenne dem.',
+ 'users_role' => 'Roller',
+ 'users_role_desc' => 'Velg hvilke roller denne kontoholderen vil bli tildelt. Hvis en kontoholderen er tildelt flere roller, vil tillatelsene fra disse rollene stable seg, og de vil motta alle evnene til de tildelte rollene.',
+ 'users_password' => 'Passord',
+ 'users_password_desc' => 'Angi et passord som brukes til å logge på applikasjonen. Dette må bestå av minst 6 tegn.',
+ 'users_send_invite_text' => 'Du kan velge å sende denne kontoholderen en invitasjons-e-post som lar dem angi sitt eget passord, ellers kan du selv angi passordet.',
+ 'users_send_invite_option' => 'Send invitasjonsmelding',
+ 'users_external_auth_id' => 'Ekstern godkjennings-ID',
+ 'users_external_auth_id_desc' => 'Dette er ID-en som brukes til å matche denne kontoholderen når de kommuniserer med det eksterne autentiseringssystemet.',
+ 'users_password_warning' => 'Fyll bare ut nedenfor hvis du vil endre passordet ditt.',
+ 'users_system_public' => 'Denne brukeren representerer alle gjester som besøker appliaksjonen din. Den kan ikke brukes til å logge på, men tildeles automatisk.',
+ 'users_delete' => 'Slett konto',
+ 'users_delete_named' => 'Slett kontoen :userName',
+ 'users_delete_warning' => 'Dette vil fullstendig slette denne brukeren med navnet «:userName» fra systemet.',
+ 'users_delete_confirm' => 'Er du sikker på at du vil slette denne kontoen?',
+ 'users_delete_success' => 'Konto slettet',
+ 'users_edit' => 'Rediger konto',
+ 'users_edit_profile' => 'Rediger profil',
+ 'users_edit_success' => 'Kontoen ble oppdatert',
+ 'users_avatar' => 'Kontobilde',
+ 'users_avatar_desc' => 'Velg et bilde for å representere denne kontoholderen. Dette skal være omtrent 256px kvadrat.',
+ 'users_preferred_language' => 'Foretrukket språk',
+ 'users_preferred_language_desc' => 'Dette alternativet vil endre språket som brukes til brukergrensesnittet til applikasjonen. Dette påvirker ikke noe brukeropprettet innhold.',
+ 'users_social_accounts' => 'Sosiale kontoer',
+ 'users_social_accounts_info' => 'Her kan du koble andre kontoer for raskere og enklere pålogging. Hvis du frakobler en konto her, tilbakekaller ikke dette tidligere autorisert tilgang. Tilbakekall tilgang fra profilinnstillingene dine på den tilkoblede sosiale kontoen.',
+ 'users_social_connect' => 'Koble til konto',
+ 'users_social_disconnect' => 'Koble fra konto',
+ 'users_social_connected' => ':socialAccount ble lagt til din konto.',
+ 'users_social_disconnected' => ':socialAccount ble koblet fra din konto.',
+ 'users_api_tokens' => 'API-nøkler',
+ 'users_api_tokens_none' => 'Ingen API-nøkler finnes for denne kontoen',
+ 'users_api_tokens_create' => 'Opprett nøkkel',
+ 'users_api_tokens_expires' => 'Utløper',
+ 'users_api_tokens_docs' => 'API-dokumentasjon',
+
+ // API Tokens
+ 'user_api_token_create' => 'Opprett API-nøkkel',
+ 'user_api_token_name' => 'Navn',
+ 'user_api_token_name_desc' => 'Gi nøkkelen et lesbart navn som en fremtidig påminnelse om det tiltenkte formålet.',
+ 'user_api_token_expiry' => 'Utløpsdato',
+ 'user_api_token_expiry_desc' => 'Angi en dato da denne nøkkelen utløper. Etter denne datoen vil forespørsler som er gjort med denne nøkkelen ikke lenger fungere. Å la dette feltet stå tomt vil sette utløpsdato 100 år inn i fremtiden.',
+ 'user_api_token_create_secret_message' => 'Umiddelbart etter å ha opprettet denne nøkkelen vil en identifikator og hemmelighet bli generert og vist. Hemmeligheten vil bare vises en gang, så husk å kopiere verdien til et trygt sted før du fortsetter.',
+ 'user_api_token_create_success' => 'API-nøkkel ble opprettet',
+ 'user_api_token_update_success' => 'API-nøkkel ble oppdatert',
+ 'user_api_token' => 'API-nøkkel',
+ 'user_api_token_id' => 'Identifikator',
+ 'user_api_token_id_desc' => 'Dette er en ikke-redigerbar systemgenerert identifikator for denne nøkkelen som må oppgis i API-forespørsler.',
+ 'user_api_token_secret' => 'Hemmelighet',
+ 'user_api_token_secret_desc' => 'Dette er en systemgenerert hemmelighet for denne nøkkelen som må leveres i API-forespørsler. Dette vises bare denne gangen, så kopier denne verdien til et trygt sted.',
+ 'user_api_token_created' => 'Nøkkel opprettet :timeAgo',
+ 'user_api_token_updated' => 'Nøkkel oppdatert :timeAgo',
+ 'user_api_token_delete' => 'Slett nøkkel',
+ 'user_api_token_delete_warning' => 'Dette vil slette API-nøkkelen \':tokenName\' fra systemet.',
+ 'user_api_token_delete_confirm' => 'Sikker på at du vil slette nøkkelen?',
+ 'user_api_token_delete_success' => 'API-nøkkelen ble slettet',
+
+ //! If editing translations files directly please ignore this in all
+ //! languages apart from en. Content will be auto-copied from en.
+ //!////////////////////////////////
+ 'language_select' => [
+ 'en' => 'English',
+ 'ar' => 'العربية',
+ 'bg' => 'Bǎlgarski',
+ 'cs' => 'Česky',
+ 'da' => 'Dansk',
+ 'de' => 'Deutsch (Sie)',
+ 'de_informal' => 'Deutsch (Du)',
+ 'es' => 'Español',
+ 'es_AR' => 'Español Argentina',
+ 'fr' => 'Français',
+ 'he' => 'עברית',
+ 'hu' => 'Magyar',
+ 'it' => 'Italian',
+ 'ja' => '日本語',
+ 'ko' => '한국어',
+ 'nl' => 'Nederlands',
+ 'pl' => 'Polski',
+ 'pt_BR' => 'Português do Brasil',
+ 'ru' => 'Русский',
+ 'sk' => 'Slovensky',
+ 'sl' => 'Slovenščina',
+ 'sv' => 'Svenska',
+ 'tr' => 'Türkçe',
+ 'uk' => 'Українська',
+ 'vi' => 'Tiếng Việt',
+ 'zh_CN' => '简体中文',
+ 'zh_TW' => '繁體中文',
+ ]
+ //!////////////////////////////////
+];
--- /dev/null
+<?php
+/**
+ * Validation Lines
+ * following language lines contain default error messages used by
+ * validator class. Some of these rules have multiple versions such
+ * as size rules. Feel free to tweak each of these messages here.
+ */
+return [
+
+ // Standard laravel validation lines
+ 'accepted' => ':attribute må aksepteres.',
+ 'active_url' => ':attribute er ikke en godkjent URL.',
+ 'after' => ':attribute må være en dato etter :date.',
+ 'alpha' => ':attribute kan kun inneholde bokstaver.',
+ 'alpha_dash' => ':attribute kan kunne inneholde bokstaver, tall, bindestreker eller understreker.',
+ 'alpha_num' => ':attribute kan kun inneholde bokstaver og tall.',
+ 'array' => ':attribute må være en liste.',
+ 'before' => ':attribute må være en dato før :date.',
+ 'between' => [
+ 'numeric' => ':attribute må være mellom :min og :max.',
+ 'file' => ':attribute må være mellom :min og :max kilobytes.',
+ 'string' => ':attribute må være mellom :min og :max tegn.',
+ 'array' => ':attribute må være mellom :min og :max ting.',
+ ],
+ 'boolean' => ':attribute feltet kan bare være sann eller falsk.',
+ 'confirmed' => ':attribute bekreftelsen samsvarer ikke.',
+ 'date' => ':attribute er ikke en gyldig dato.',
+ 'date_format' => ':attribute samsvarer ikke med :format.',
+ 'different' => ':attribute og :other må være forskjellige.',
+ 'digits' => ':attribute må være :digits tall.',
+ 'digits_between' => ':attribute må være mellomg :min og :max tall.',
+ 'email' => ':attribute må være en gyldig e-post.',
+ 'ends_with' => ':attribute må slutte med en av verdiene: :values',
+ 'filled' => ':attribute feltet er påkrevd.',
+ 'gt' => [
+ 'numeric' => ':attribute må være større enn :value.',
+ 'file' => ':attribute må være større enn :value kilobytes.',
+ 'string' => ':attribute må være større enn :value tegn.',
+ 'array' => ':attribute må ha mer en :value ting.',
+ ],
+ 'gte' => [
+ 'numeric' => ':attribute må være større enn eller lik :value.',
+ 'file' => ':attribute må være større enn eller lik :value kilobytes.',
+ 'string' => ':attribute må være større enn eller lik :value tegn.',
+ 'array' => ':attribute må ha :value eller flere ting.',
+ ],
+ 'exists' => 'Den valgte :attribute er ugyldig.',
+ 'image' => ':attribute må være et bilde.',
+ 'image_extension' => ':attribute må ha støttet formattype.',
+ 'in' => 'Den valgte :attribute er ugyldig.',
+ 'integer' => ':attribute må være et heltall',
+ 'ip' => ':attribute må være en gyldig IP adresse.',
+ 'ipv4' => ':attribute må være en gyldig IPv4 adresse.',
+ 'ipv6' => ':attribute må være en gyldig IPv6 adresse.',
+ 'json' => ':attribute må være en gyldig JSON tekststreng.',
+ 'lt' => [
+ 'numeric' => ':attribute må være mindre enn :value.',
+ 'file' => ':attribute må være mindre enn :value kilobytes.',
+ 'string' => ':attribute må være mindre enn :value tegn.',
+ 'array' => ':attribute må ha mindre enn :value ting.',
+ ],
+ 'lte' => [
+ 'numeric' => ':attribute må være mindre enn eller lik :value.',
+ 'file' => ':attribute må være mindre enn eller lik :value kilobytes.',
+ 'string' => ':attribute må være mindre enn eller lik :value characters.',
+ 'array' => ':attribute må ha mindre enn eller lik :value ting.',
+ ],
+ 'max' => [
+ 'numeric' => ':attribute kan ikke være større enn :max.',
+ 'file' => ':attribute kan ikke være større enn :max kilobytes.',
+ 'string' => ':attribute kan ikke være større enn :max tegn.',
+ 'array' => ':attribute kan ikke inneholde mer enn :max ting.',
+ ],
+ 'mimes' => ':attribute må være en fil av typen: :values.',
+ 'min' => [
+ 'numeric' => ':attribute må være på minst :min.',
+ 'file' => ':attribute må være på minst :min kilobytes.',
+ 'string' => ':attribute må være på minst :min tegn.',
+ 'array' => ':attribute må minst ha :min ting.',
+ ],
+ 'no_double_extension' => ':attribute kan bare ha en formattype spesifisert.',
+ 'not_in' => 'Den valgte :attribute er ugyldig.',
+ 'not_regex' => ':attribute format er ugyldig.',
+ 'numeric' => ':attribute må være et nummer.',
+ 'regex' => ':attribute format er ugyldig.',
+ 'required' => ':attribute feltet er påkrevt.',
+ 'required_if' => ':attribute feltet er påkrevt når :other er :value.',
+ 'required_with' => ':attribute feltet er påkrevt når :values er tilgjengelig.',
+ 'required_with_all' => ':attribute feltet er påkrevt når :values er tilgjengelig',
+ 'required_without' => ':attribute feltet er påkrevt når :values ikke er tilgjengelig.',
+ 'required_without_all' => ':attribute feltet er påkrevt når ingen av :values er tilgjengelig.',
+ 'same' => ':attribute og :other må samsvare.',
+ 'size' => [
+ 'numeric' => ':attribute må være :size.',
+ 'file' => ':attribute må være :size kilobytes.',
+ 'string' => ':attribute må være :size tegn.',
+ 'array' => ':attribute må inneholde :size ting.',
+ ],
+ 'string' => ':attribute må være en tekststreng.',
+ 'timezone' => ':attribute må være en tidssone.',
+ 'unique' => ':attribute har allerede blitt tatt.',
+ 'url' => ':attribute format er ugyldig.',
+ 'uploaded' => 'kunne ikke lastes opp, tjeneren støtter ikke filer av denne størrelsen.',
+
+ // Custom validation lines
+ 'custom' => [
+ 'password-confirm' => [
+ 'required_with' => 'passordbekreftelse er påkrevd',
+ ],
+ ],
+
+ // Custom validation attributes
+ 'attributes' => [],
+];
.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
<?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
{
/**
->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;
'name' => $fileName
]);
- $this->call('DELETE', $page->getUrl());
+ app(PageRepo::class)->destroy($page);
+ app(TrashCan::class)->empty();
$this->assertDatabaseMissing('attachments', [
'name' => $fileName
<?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 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 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)