# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
-QUEUE_DRIVER=sync
+QUEUE_CONNECTION=sync
# Storage system to use
# Can be 'local', 'local_secure' or 's3'
--- /dev/null
+name: phpunit
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: [7.2, 7.3]
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup Database
+ run: |
+ mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
+ mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
+ mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
+ mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
+ - name: Install composer dependencies & Test
+ run: composer install --prefer-dist --no-interaction --ansi
+ - name: Migrate and seed the database
+ run: |
+ php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
+ php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
+ - name: phpunit
+ run: php${{ matrix.php }} ./vendor/bin/phpunit
.buildpath
.project
.settings/
-webpack-stats.json
\ No newline at end of file
+webpack-stats.json
+.phpunit.result.cache
\ No newline at end of file
+++ /dev/null
-dist: trusty
-sudo: false
-language: php
-php:
- - 7.0.20
- - 7.1.9
-
-cache:
- directories:
- - $HOME/.composer/cache
-
-before_script:
- - mysql -u root -e 'create database `bookstack-test`;'
- - mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- - mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- - mysql -u root -e "FLUSH PRIVILEGES;"
- - phpenv config-rm xdebug.ini
- - composer install --prefer-dist --no-interaction
- - php artisan clear-compiled -n
- - php artisan optimize -n
- - php artisan migrate --force -n --database=mysql_testing
- - php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
-
-after_failure:
- - cat storage/logs/laravel.log
-
-script:
- - phpunit
namespace BookStack\Actions;
use BookStack\Auth\User;
+use BookStack\Entities\Entity;
use BookStack\Model;
/**
- * @property string key
- * @property \User user
- * @property \Entity entity
- * @property string extra
+ * @property string $key
+ * @property User $user
+ * @property Entity $entity
+ * @property string $extra
+ * @property string $entity_type
+ * @property int $entity_id
+ * @property int $user_id
+ * @property int $book_id
*/
class Activity extends Model
{
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
use BookStack\Entities\Entity;
-use Session;
class ActivityService
{
/**
* ActivityService constructor.
- * @param \BookStack\Actions\Activity $activity
+ * @param Activity $activity
* @param PermissionService $permissionService
*/
public function __construct(Activity $activity, PermissionService $permissionService)
/**
* Add activity data to database.
- * @param Entity $entity
- * @param $activityKey
+ * @param \BookStack\Entities\Entity $entity
+ * @param string $activityKey
* @param int $bookId
- * @param bool $extra
*/
- public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
+ public function add(Entity $entity, string $activityKey, int $bookId = null)
{
- $activity = $this->activity->newInstance();
- $activity->user_id = $this->user->id;
- $activity->book_id = $bookId;
- $activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $activity->extra = $extra;
- }
+ $activity = $this->newActivityForUser($activityKey, $bookId);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
}
/**
- * Adds a activity history with a message & without binding to a entity.
- * @param $activityKey
+ * Adds a activity history with a message, without binding to a entity.
+ * @param string $activityKey
+ * @param string $message
* @param int $bookId
- * @param bool|false $extra
*/
- public function addMessage($activityKey, $bookId = 0, $extra = false)
+ public function addMessage(string $activityKey, string $message, int $bookId = null)
{
- $this->activity->user_id = $this->user->id;
- $this->activity->book_id = $bookId;
- $this->activity->key = strtolower($activityKey);
- if ($extra !== false) {
- $this->activity->extra = $extra;
- }
- $this->activity->save();
+ $this->newActivityForUser($activityKey, $bookId)->forceFill([
+ 'extra' => $message
+ ])->save();
+
$this->setNotification($activityKey);
}
+ /**
+ * Get a new activity instance for the current user.
+ * @param string $key
+ * @param int|null $bookId
+ * @return Activity
+ */
+ protected function newActivityForUser(string $key, int $bookId = null)
+ {
+ return $this->activity->newInstance()->forceFill([
+ 'key' => strtolower($key),
+ 'user_id' => $this->user->id,
+ 'book_id' => $bookId ?? 0,
+ ]);
+ }
/**
* Removes the entity attachment from each of its activities
* and instead uses the 'extra' field with the entities name.
* Used when an entity is deleted.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @return mixed
*/
public function removeEntity(Entity $entity)
{
+ // TODO - Rewrite to db query.
$activities = $entity->activity;
foreach ($activities as $activity) {
$activity->extra = $entity->name;
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
- ->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
+ ->orderBy('created_at', 'desc')
+ ->with('user', 'entity')
+ ->skip($count * $page)
+ ->take($count)
+ ->get();
return $this->filterSimilar($activityList);
}
/**
* Gets the latest activity for an entity, Filtering out similar
* items to prevent a message activity list.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @param int $count
* @param int $page
* @return array
$notificationTextKey = 'activities.' . $activityKey . '_notification';
if (trans()->has($notificationTextKey)) {
$message = trans($notificationTextKey);
- Session::flash('success', $message);
+ session()->flash('success', $message);
}
}
}
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Book;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
+use DB;
use Illuminate\Support\Collection;
class ViewService
/**
* ViewService constructor.
- * @param \BookStack\Actions\View $view
- * @param \BookStack\Auth\Permissions\PermissionService $permissionService
+ * @param View $view
+ * @param PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
/**
* Add a view to the given entity.
- * @param Entity $entity
+ * @param \BookStack\Entities\Entity $entity
* @return int
*/
public function add(Entity $entity)
}
// Otherwise create new view count
- $entity->views()->save($this->view->create([
+ $entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
* @param string $action - used for permission checking
* @return Collection
*/
- public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
+ public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
- ->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
+ ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
*/
public function configPath($path = '')
{
- return $this->basePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
+ return $this->basePath
+ . DIRECTORY_SEPARATOR
+ . 'app'
+ . DIRECTORY_SEPARATOR
+ . 'Config'
+ . ($path ? DIRECTORY_SEPARATOR.$path : $path);
}
-
-}
\ No newline at end of file
+}
<?php namespace BookStack\Auth\Access;
use BookStack\Auth\User;
-use BookStack\Auth\UserRepo;
use BookStack\Exceptions\ConfirmationEmailException;
-use BookStack\Exceptions\UserRegistrationException;
use BookStack\Notifications\ConfirmEmail;
-use Carbon\Carbon;
-use Illuminate\Database\Connection as Database;
-class EmailConfirmationService
+class EmailConfirmationService extends UserTokenService
{
- protected $db;
- protected $users;
-
- /**
- * EmailConfirmationService constructor.
- * @param Database $db
- * @param \BookStack\Auth\UserRepo $users
- */
- public function __construct(Database $db, UserRepo $users)
- {
- $this->db = $db;
- $this->users = $users;
- }
+ protected $tokenTable = 'email_confirmations';
+ protected $expiryTime = 24;
/**
* Create new confirmation for a user,
* Also removes any existing old ones.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @throws ConfirmationEmailException
*/
public function sendConfirmation(User $user)
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
}
- $this->deleteConfirmationsByUser($user);
- $token = $this->createEmailConfirmation($user);
+ $this->deleteByUser($user);
+ $token = $this->createTokenForUser($user);
$user->notify(new ConfirmEmail($token));
}
/**
- * Creates a new email confirmation in the database and returns the token.
- * @param User $user
- * @return string
- */
- public function createEmailConfirmation(User $user)
- {
- $token = $this->getToken();
- $this->db->table('email_confirmations')->insert([
- 'user_id' => $user->id,
- 'token' => $token,
- 'created_at' => Carbon::now(),
- 'updated_at' => Carbon::now()
- ]);
- return $token;
- }
-
- /**
- * Gets an email confirmation by looking up the token,
- * Ensures the token has not expired.
- * @param string $token
- * @return array|null|\stdClass
- * @throws UserRegistrationException
- */
- public function getEmailConfirmationFromToken($token)
- {
- $emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
-
- // If not found show error
- if ($emailConfirmation === null) {
- throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
- }
-
- // If more than a day old
- if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
- $user = $this->users->getById($emailConfirmation->user_id);
- $this->sendConfirmation($user);
- throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
- }
-
- $emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
- return $emailConfirmation;
- }
-
- /**
- * Delete all email confirmations that belong to a user.
- * @param \BookStack\Auth\User $user
- * @return mixed
+ * Check if confirmation is required in this instance.
+ * @return bool
*/
- public function deleteConfirmationsByUser(User $user)
+ public function confirmationRequired() : bool
{
- return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
- }
-
- /**
- * Creates a unique token within the email confirmation database.
- * @return string
- */
- protected function getToken()
- {
- $token = str_random(24);
- while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
- $token = str_random(25);
- }
- return $token;
+ return setting('registration-confirmation')
+ || setting('registration-restrict');
}
}
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
+use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\User as SocialUser;
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$isLoggedIn = auth()->check();
$currentUser = user();
+ $titleCaseDriver = Str::title($socialDriver);
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
- session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
- session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in, A social account exists but the users do not match.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
- session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
// Otherwise let the user know this social account is not used by anyone.
- $message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
+ $message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
if (setting('registration-enabled')) {
- $message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
+ $message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
}
throw new SocialSignInAccountNotUsed($message, '/login');
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
- throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
+ throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
}
return $driver;
public function detachSocialAccount($socialDriver)
{
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
- session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
+ session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
return redirect(user()->getEditUrl());
}
--- /dev/null
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Notifications\UserInvite;
+
+class UserInviteService extends UserTokenService
+{
+ protected $tokenTable = 'user_invites';
+ protected $expiryTime = 336; // Two weeks
+
+ /**
+ * Send an invitation to a user to sign into BookStack
+ * Removes existing invitation tokens.
+ * @param User $user
+ */
+ public function sendInvitation(User $user)
+ {
+ $this->deleteByUser($user);
+ $token = $this->createTokenForUser($user);
+ $user->notify(new UserInvite($token));
+ }
+}
--- /dev/null
+<?php namespace BookStack\Auth\Access;
+
+use BookStack\Auth\User;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use Carbon\Carbon;
+use Illuminate\Database\Connection as Database;
+use Illuminate\Support\Str;
+use stdClass;
+
+class UserTokenService
+{
+
+ /**
+ * Name of table where user tokens are stored.
+ * @var string
+ */
+ protected $tokenTable = 'user_tokens';
+
+ /**
+ * Token expiry time in hours.
+ * @var int
+ */
+ protected $expiryTime = 24;
+
+ protected $db;
+
+ /**
+ * UserTokenService constructor.
+ * @param Database $db
+ */
+ public function __construct(Database $db)
+ {
+ $this->db = $db;
+ }
+
+ /**
+ * Delete all email confirmations that belong to a user.
+ * @param User $user
+ * @return mixed
+ */
+ public function deleteByUser(User $user)
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('user_id', '=', $user->id)
+ ->delete();
+ }
+
+ /**
+ * Get the user id from a token, while check the token exists and has not expired.
+ * @param string $token
+ * @return int
+ * @throws UserTokenNotFoundException
+ * @throws UserTokenExpiredException
+ */
+ public function checkTokenAndGetUserId(string $token) : int
+ {
+ $entry = $this->getEntryByToken($token);
+
+ if (is_null($entry)) {
+ throw new UserTokenNotFoundException('Token "' . $token . '" not found');
+ }
+
+ if ($this->entryExpired($entry)) {
+ throw new UserTokenExpiredException("Token of id {$entry->id} has expired.", $entry->user_id);
+ }
+
+ return $entry->user_id;
+ }
+
+ /**
+ * Creates a unique token within the email confirmation database.
+ * @return string
+ */
+ protected function generateToken() : string
+ {
+ $token = Str::random(24);
+ while ($this->tokenExists($token)) {
+ $token = Str::random(25);
+ }
+ return $token;
+ }
+
+ /**
+ * Generate and store a token for the given user.
+ * @param User $user
+ * @return string
+ */
+ protected function createTokenForUser(User $user) : string
+ {
+ $token = $this->generateToken();
+ $this->db->table($this->tokenTable)->insert([
+ 'user_id' => $user->id,
+ 'token' => $token,
+ 'created_at' => Carbon::now(),
+ 'updated_at' => Carbon::now()
+ ]);
+ return $token;
+ }
+
+ /**
+ * Check if the given token exists.
+ * @param string $token
+ * @return bool
+ */
+ protected function tokenExists(string $token) : bool
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('token', '=', $token)->exists();
+ }
+
+ /**
+ * Get a token entry for the given token.
+ * @param string $token
+ * @return object|null
+ */
+ protected function getEntryByToken(string $token)
+ {
+ return $this->db->table($this->tokenTable)
+ ->where('token', '=', $token)
+ ->first();
+ }
+
+ /**
+ * Check if the given token entry has expired.
+ * @param stdClass $tokenEntry
+ * @return bool
+ */
+ protected function entryExpired(stdClass $tokenEntry) : bool
+ {
+ return Carbon::now()->subHours($this->expiryTime)
+ ->gt(new Carbon($tokenEntry->created_at));
+ }
+}
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
- * @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
}
/**
- * Get the children of a book in an efficient single query, Filtered by the permission system.
- * @param integer $book_id
- * @param bool $filterDrafts
- * @param bool $fetchPageContent
- * @return QueryBuilder
+ * Limited the given entity query so that the query will only
+ * return items that the user has permission for the given ability.
*/
- public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
+ public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
{
- $entities = $this->entityProvider;
- $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
- ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
- $query->where('draft', '=', 0);
- if (!$filterDrafts) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
+ $this->clean();
+ return $query->where(function (Builder $parentQuery) use ($ability) {
+ $parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
+ $permissionQuery->whereIn('role_id', $this->getRoles())
+ ->where('action', '=', $ability)
+ ->where(function (Builder $query) {
+ $query->where('has_permission', '=', true)
+ ->orWhere(function (Builder $query) {
+ $query->where('has_permission_own', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
+ });
});
- }
- });
- $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
- $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
- ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
-
- // Add joint permission filter
- $whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
- ->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
- ->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
- ->where(function ($query) {
- $query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
- $query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
- });
});
- $query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
+ });
+ }
- $query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
- $this->clean();
- return $query;
+ /**
+ * Extend the given page query to ensure draft items are not visible
+ * unless created by the given user.
+ */
+ public function enforceDraftVisiblityOnQuery(Builder $query): Builder
+ {
+ return $query->where(function (Builder $query) {
+ $query->where('draft', '=', false)
+ ->orWhere(function (Builder $query) {
+ $query->where('draft', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
+ });
+ });
}
/**
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
- $query->where('draft', '=', false);
- if ($this->currentUser()) {
- $query->orWhere(function ($query) {
- $query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
+ $query->where('draft', '=', false)
+ ->orWhere(function ($query) {
+ $query->where('draft', '=', true)
+ ->where('created_by', '=', $this->currentUser()->id);
});
- }
});
}
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
+use Illuminate\Support\Str;
class PermissionsRepo
{
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
- $role->name .= strtolower(str_random(2));
+ $role->name .= strtolower(Str::random(2));
}
$role->save();
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
throw new PermissionsException(trans('errors.role_system_cannot_be_deleted'));
- } else if ($role->id == setting('registration-role')) {
+ } else if ($role->id === intval(setting('registration-role'))) {
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
*/
public static function getRole($roleName)
{
- return static::where('name', '=', $roleName)->first();
+ return static::query()->where('name', '=', $roleName)->first();
}
/**
*/
public static function getSystemRole($roleName)
{
- return static::where('system_name', '=', $roleName)->first();
+ return static::query()->where('system_name', '=', $roleName)->first();
}
/**
*/
public static function visible()
{
- return static::where('hidden', '=', false)->orderBy('name')->get();
+ return static::query()->where('hidden', '=', false)->orderBy('name')->get();
+ }
+
+ /**
+ * Get the roles that can be restricted.
+ * @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
+ */
+ public static function restrictable()
+ {
+ return static::query()->where('system_name', '!=', 'admin')->get();
}
}
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
+use Carbon\Carbon;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
+/**
+ * Class User
+ * @package BookStack\Auth
+ * @property string $id
+ * @property string $name
+ * @property string $email
+ * @property string $password
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ * @property bool $email_confirmed
+ * @property int $image_id
+ * @property string $external_auth_id
+ * @property string $system_name
+ */
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword, Notifiable;
*/
protected $permissions;
+ /**
+ * This holds the default user when loaded.
+ * @var null|User
+ */
+ protected static $defaultUser = null;
+
/**
* Returns the default public user.
* @return User
*/
public static function getDefault()
{
- return static::where('system_name', '=', 'public')->first();
+ if (!is_null(static::$defaultUser)) {
+ return static::$defaultUser;
+ }
+
+ static::$defaultUser = static::where('system_name', '=', 'public')->first();
+ return static::$defaultUser;
}
/**
<?php namespace BookStack\Auth;
use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Images;
+use Log;
class UserRepo
{
protected $user;
protected $role;
- protected $entityRepo;
/**
* UserRepo constructor.
- * @param User $user
- * @param Role $role
- * @param EntityRepo $entityRepo
*/
- public function __construct(User $user, Role $role, EntityRepo $entityRepo)
+ public function __construct(User $user, Role $role)
{
$this->user = $user;
$this->role = $role;
- $this->entityRepo = $entityRepo;
}
/**
* Creates a new user and attaches a role to them.
* @param array $data
* @param boolean $verifyEmail
- * @return \BookStack\Auth\User
+ * @return User
*/
public function registerNew(array $data, $verifyEmail = false)
{
/**
* Checks if the give user is the only admin.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @return bool
*/
public function isOnlyAdmin(User $user)
* Create a new basic instance of user.
* @param array $data
* @param boolean $verifyEmail
- * @return \BookStack\Auth\User
+ * @return User
*/
public function create(array $data, $verifyEmail = false)
{
/**
* Remove the given user from storage, Delete all related content.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @throws Exception
*/
public function destroy(User $user)
/**
* Get the latest activity for a user.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @param int $count
* @param int $page
* @return array
/**
* Get the recently created content for this given user.
- * @param \BookStack\Auth\User $user
- * @param int $count
- * @return mixed
*/
- public function getRecentlyCreated(User $user, $count = 20)
+ public function getRecentlyCreated(User $user, int $count = 20): array
{
- $createdByUserQuery = function (Builder $query) use ($user) {
- $query->where('created_by', '=', $user->id);
+ $query = function (Builder $query) use ($user, $count) {
+ return $query->orderBy('created_at', 'desc')
+ ->where('created_by', '=', $user->id)
+ ->take($count)
+ ->get();
};
return [
- 'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
- 'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
- 'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
- 'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
+ 'pages' => $query(Page::visible()->where('draft', '=', false)),
+ 'chapters' => $query(Chapter::visible()),
+ 'books' => $query(Book::visible()),
+ 'shelves' => $query(Bookshelf::visible()),
];
}
/**
* Get asset created counts for the give user.
- * @param \BookStack\Auth\User $user
- * @return array
*/
- public function getAssetCounts(User $user)
+ public function getAssetCounts(User $user): array
{
+ $createdBy = ['created_by' => $user->id];
return [
- 'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
- 'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
- 'books' => $this->entityRepo->getUserTotalCreated('book', $user),
- 'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
+ 'pages' => Page::visible()->where($createdBy)->count(),
+ 'chapters' => Chapter::visible()->where($createdBy)->count(),
+ 'books' => Book::visible()->where($createdBy)->count(),
+ 'shelves' => Bookshelf::visible()->where($createdBy)->count(),
];
}
return $this->role->newQuery()->orderBy('name', 'asc')->get();
}
- /**
- * Get all the roles which can be given restricted access to
- * other entities in the system.
- * @return mixed
- */
- public function getRestrictableRoles()
- {
- return $this->role->where('system_name', '!=', 'admin')->get();
- }
-
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
$user->save();
return true;
} catch (Exception $e) {
- \Log::error('Failed to save user avatar image');
+ Log::error('Failed to save user avatar image');
return false;
}
}
'locale' => env('APP_LANG', 'en'),
// Locales available
- 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
+ 'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW', 'tr'],
// Application Fallback Locale
'fallback_locale' => 'en',
+ // Faker Locale
+ 'faker_locale' => 'en_GB',
+
// Enable right-to-left text control.
'rtl' => false,
// Encryption cipher
'cipher' => 'AES-256-CBC',
- // Logging configuration
- // Options: single, daily, syslog, errorlog
- 'log' => env('APP_LOGGING', 'single'),
-
// Application Services Provides
'providers' => [
// Laravel
'App' => Illuminate\Support\Facades\App::class,
+ 'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
+ 'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
+ 'Permissions' => BookStack\Facades\Permissions::class,
],
'api' => [
'driver' => 'token',
'provider' => 'users',
+ 'hash' => false,
],
],
],
],
-];
\ No newline at end of file
+];
'pusher' => [
'driver' => 'pusher',
- 'key' => env('PUSHER_KEY'),
- 'secret' => env('PUSHER_SECRET'),
+ 'key' => env('PUSHER_APP_KEY'),
+ 'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
+ 'options' => [
+ 'cluster' => env('PUSHER_APP_CLUSTER'),
+ 'useTLS' => true,
+ ],
],
'redis' => [
'driver' => 'log',
],
+ 'null' => [
+ 'driver' => 'null',
+ ],
+
+
],
];
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
foreach ($memcachedServers as $index => $memcachedServer) {
$memcachedServerDetails = explode(':', $memcachedServer);
- if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
- if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
+ if (count($memcachedServerDetails) < 2) {
+ $memcachedServerDetails[] = '11211';
+ }
+ if (count($memcachedServerDetails) < 3) {
+ $memcachedServerDetails[] = '100';
+ }
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
}
}
// Cache key prefix
// Used to prevent collisions in shared cache systems.
- 'prefix' => env('CACHE_PREFIX', 'bookstack'),
+ 'prefix' => env('CACHE_PREFIX', 'bookstack_cache'),
];
// REDIS
// Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
-
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
- $redisConfig = [];
+ $redisConfig = ['client' => 'predis'];
$cluster = count($redisServers) > 1;
if ($cluster) {
// Many of those shown here are unsupported by BookStack.
'connections' => [
- 'sqlite' => [
- 'driver' => 'sqlite',
- 'database' => storage_path('database.sqlite'),
- 'prefix' => '',
- ],
-
'mysql' => [
'driver' => 'mysql',
+ 'url' => env('DATABASE_URL'),
'host' => $mysql_host,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
+ 'prefix_indexes' => true,
'strict' => false,
'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
],
'mysql_testing' => [
'driver' => 'mysql',
+ 'url' => env('TEST_DATABASE_URL'),
'host' => '127.0.0.1',
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
- 'charset' => 'utf8',
- 'collation' => 'utf8_unicode_ci',
+ 'charset' => 'utf8mb4',
+ 'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
+ 'prefix_indexes' => true,
'strict' => false,
],
- 'pgsql' => [
- 'driver' => 'pgsql',
- 'host' => env('DB_HOST', 'localhost'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- 'schema' => 'public',
- ],
-
- 'sqlsrv' => [
- 'driver' => 'sqlsrv',
- 'host' => env('DB_HOST', 'localhost'),
- 'database' => env('DB_DATABASE', 'forge'),
- 'username' => env('DB_USERNAME', 'forge'),
- 'password' => env('DB_PASSWORD', ''),
- 'charset' => 'utf8',
- 'prefix' => '',
- ],
-
],
// Migration Repository Table
'files' => false, // Show the included files
'config' => false, // Display config settings
'cache' => false, // Display cache events
+ 'models' => true, // Display models
],
// Configure some DataCollectors
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
- * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
+ * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
"DOMPDF_CHROOT" => realpath(base_path()),
--- /dev/null
+<?php
+
+/**
+ * Hashing configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+ // Default Hash Driver
+ // This option controls the default hash driver that will be used to hash
+ // passwords for your application. By default, the bcrypt algorithm is used.
+ // Supported: "bcrypt", "argon", "argon2id"
+ 'driver' => 'bcrypt',
+
+ // Bcrypt Options
+ // Here you may specify the configuration options that should be used when
+ // passwords are hashed using the Bcrypt algorithm. This will allow you
+ // to control the amount of time it takes to hash the given password.
+ 'bcrypt' => [
+ 'rounds' => env('BCRYPT_ROUNDS', 10),
+ ],
+
+ // Argon Options
+ // Here you may specify the configuration options that should be used when
+ // passwords are hashed using the Argon algorithm. These will allow you
+ // to control the amount of time it takes to hash the given password.
+ 'argon' => [
+ 'memory' => 1024,
+ 'threads' => 2,
+ 'time' => 2,
+ ],
+
+];
--- /dev/null
+<?php
+
+use Monolog\Handler\NullHandler;
+use Monolog\Handler\StreamHandler;
+
+/**
+ * Logging configuration options.
+ *
+ * Changes to these config files are not supported by BookStack and may break upon updates.
+ * Configuration should be altered via the `.env` file or environment variables.
+ * Do not edit this file unless you're happy to maintain any changes yourself.
+ */
+
+return [
+
+ // Default Log Channel
+ // This option defines the default log channel that gets used when writing
+ // messages to the logs. The name specified in this option should match
+ // one of the channels defined in the "channels" configuration array.
+ 'default' => env('LOG_CHANNEL', 'single'),
+
+ // Log Channels
+ // Here you may configure the log channels for your application. Out of
+ // the box, Laravel uses the Monolog PHP logging library. This gives
+ // you a variety of powerful log handlers / formatters to utilize.
+ // Available Drivers: "single", "daily", "slack", "syslog",
+ // "errorlog", "monolog",
+ // "custom", "stack"
+ 'channels' => [
+ 'stack' => [
+ 'driver' => 'stack',
+ 'channels' => ['daily'],
+ 'ignore_exceptions' => false,
+ ],
+
+ 'single' => [
+ 'driver' => 'single',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => 'debug',
+ 'days' => 14,
+ ],
+
+ 'daily' => [
+ 'driver' => 'daily',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => 'debug',
+ 'days' => 7,
+ ],
+
+ 'slack' => [
+ 'driver' => 'slack',
+ 'url' => env('LOG_SLACK_WEBHOOK_URL'),
+ 'username' => 'Laravel Log',
+ 'emoji' => ':boom:',
+ 'level' => 'critical',
+ ],
+
+ 'stderr' => [
+ 'driver' => 'monolog',
+ 'handler' => StreamHandler::class,
+ 'with' => [
+ 'stream' => 'php://stderr',
+ ],
+ ],
+
+ 'syslog' => [
+ 'driver' => 'syslog',
+ 'level' => 'debug',
+ ],
+
+ 'errorlog' => [
+ 'driver' => 'errorlog',
+ 'level' => 'debug',
+ ],
+
+ 'null' => [
+ 'driver' => 'monolog',
+ 'handler' => NullHandler::class,
+ ],
+ ],
+
+];
// Global "From" address & name
'from' => [
- 'name' => env('MAIL_FROM_NAME','BookStack')
+ 'name' => env('MAIL_FROM_NAME', 'BookStack')
],
// Email encryption protocol
],
],
+ // Log Channel
+ // If you are using the "log" driver, you may specify the logging channel
+ // if you prefer to keep mail messages separate from other log entries
+ // for simpler reading. Otherwise, the default channel will be used.
+ 'log_channel' => env('MAIL_LOG_CHANNEL'),
+
];
// Default driver to use for the queue
// Options: null, sync, redis
- 'default' => env('QUEUE_DRIVER', 'sync'),
+ 'default' => env('QUEUE_CONNECTION', 'sync'),
// Queue connection configuration
'connections' => [
+
'sync' => [
'driver' => 'sync',
],
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
- 'expire' => 60,
- ],
-
- 'beanstalkd' => [
- 'driver' => 'beanstalkd',
- 'host' => 'localhost',
- 'queue' => 'default',
- 'ttr' => 60,
- ],
-
- 'sqs' => [
- 'driver' => 'sqs',
- 'key' => 'your-public-key',
- 'secret' => 'your-secret-key',
- 'queue' => 'your-queue-url',
- 'region' => 'us-east-1',
- ],
-
- 'iron' => [
- 'driver' => 'iron',
- 'host' => 'mq-aws-us-east-1.iron.io',
- 'token' => 'your-token',
- 'project' => 'your-project-id',
- 'queue' => 'your-queue-name',
- 'encrypt' => true,
+ 'retry_after' => 90,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
- 'queue' => 'default',
- 'expire' => 60,
+ 'queue' => env('REDIS_QUEUE', 'default'),
+ 'retry_after' => 90,
+ 'block_for' => null,
],
],
// Callback URL for social authentication methods
'callback_url' => env('APP_URL', false),
- 'mailgun' => [
- 'domain' => '',
- 'secret' => '',
- ],
-
- 'ses' => [
- 'key' => '',
- 'secret' => '',
- 'region' => 'us-east-1',
- ],
-
- 'stripe' => [
- 'model' => \BookStack\Auth\User::class,
- 'key' => '',
- 'secret' => '',
- ],
-
'github' => [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),
'okta' => [
'client_id' => env('OKTA_APP_ID'),
'client_secret' => env('OKTA_APP_SECRET'),
- 'redirect' => env('APP_URL') . '/login/service/okta/callback',
- 'base_url' => env('OKTA_BASE_URL'),
+ 'redirect' => env('APP_URL') . '/login/service/okta/callback',
+ 'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
'auto_register' => env('OKTA_AUTO_REGISTER', false),
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
- 'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
- 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
- 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
- 'tls_insecure' => env('LDAP_TLS_INSECURE', false),
- ]
+ 'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
+ 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
+ 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
+ 'tls_insecure' => env('LDAP_TLS_INSECURE', false),
+ ]
];
// Session database table, if database driver is in use
'table' => 'sessions',
+ // Session Cache Store
+ // When using the "apc" or "memcached" session drivers, you may specify a
+ // cache store that should be used for these sessions. This value must
+ // correspond with one of the application's configured cache stores.
+ 'store' => null,
+
// Session Sweeping Lottery
// Some session drivers must manually sweep their storage location to get
// rid of old sessions from storage. Here are the chances that it will
// happen on a given request. By default, the odds are 2 out of 100.
'lottery' => [2, 100],
-
// Session Cookie Name
// Here you may change the name of the cookie used to identify a session
// instance by ID. The name specified here will get used every time a
'app-logo' => '',
'app-name-header' => true,
'app-editor' => 'wysiwyg',
- 'app-color' => '#0288D1',
- 'app-color-light' => 'rgba(21, 101, 192, 0.15)',
+ 'app-color' => '#206ea7',
+ 'app-color-light' => 'rgba(32,110,167,0.15)',
'app-custom-head' => false,
'registration-enabled' => false,
-];
\ No newline at end of file
+];
<?php namespace BookStack\Entities;
use BookStack\Uploads\Image;
+use Exception;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Support\Collection;
-class Book extends Entity
+/**
+ * Class Book
+ * @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', 'image_id'];
- /**
- * Get the morph class for this model.
- * @return string
- */
- public function getMorphClass()
- {
- return 'BookStack\\Book';
- }
-
/**
* Get the url for this book.
* @param string|bool $path
try {
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
- } catch (\Exception $err) {
+ } catch (Exception $err) {
$cover = $default;
}
return $cover;
/**
* Get the cover image of the book
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function cover()
+ public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
+ /**
+ * Get the type of the image model that is used when storing a cover image.
+ */
+ public function coverImageTypeKey(): string
+ {
+ return 'cover_book';
+ }
+
/**
* Get all pages within this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function pages()
{
/**
* Get the direct child pages of this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function directPages()
{
/**
* Get all chapters within this book.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function chapters()
{
/**
* Get the shelves this book is contained within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
+ * @return BelongsToMany
*/
public function shelves()
{
}
/**
- * Get an excerpt of this book's description to the specified length or less.
- * @param int $length
- * @return string
+ * Get the direct child items within this book.
+ * @return Collection
*/
- public function getExcerpt(int $length = 100)
+ public function getDirectChildren(): Collection
{
- $description = $this->description;
- return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
+ $pages = $this->directPages()->visible()->get();
+ $chapters = $this->chapters()->visible()->get();
+ return $pages->contact($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get an excerpt of this book's description to the specified length or less.
+ * @param int $length
* @return string
*/
- public function entityRawQuery()
+ public function getExcerpt(int $length = 100)
{
- return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ $description = $this->description;
+ return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
}
--- /dev/null
+<?php namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * Class BookChild
+ * @property int $book_id
+ * @property int $priority
+ * @property Book $book
+ * @method Builder whereSlugs(string $bookSlug, string $childSlug)
+ */
+class BookChild extends Entity
+{
+
+ /**
+ * Scope a query to find items where the the child has the given childSlug
+ * where its parent has the bookSlug.
+ */
+ public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
+ {
+ return $query->with('book')
+ ->whereHas('book', function (Builder $query) use ($bookSlug) {
+ $query->where('slug', '=', $bookSlug);
+ })
+ ->where('slug', '=', $childSlug);
+ }
+
+ /**
+ * Get the book this page sits in.
+ * @return BelongsTo
+ */
+ public function book(): BelongsTo
+ {
+ return $this->belongsTo(Book::class);
+ }
+
+ /**
+ * Change the book that this entity belongs to.
+ */
+ public function changeBook(int $newBookId): Entity
+ {
+ $this->book_id = $newBookId;
+ $this->refreshSlug();
+ $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) {
+ $page->changeBook($newBookId);
+ }
+ }
+
+ return $this;
+ }
+}
<?php namespace BookStack\Entities;
use BookStack\Uploads\Image;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
-class Bookshelf extends Entity
+class Bookshelf extends Entity implements HasCoverImage
{
protected $table = 'bookshelves';
protected $fillable = ['name', 'description', 'image_id'];
- /**
- * Get the morph class for this model.
- * @return string
- */
- public function getMorphClass()
- {
- return 'BookStack\\Bookshelf';
- }
-
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.
->orderBy('order', 'asc');
}
+ /**
+ * Related books that are visible to the current user.
+ */
+ public function visibleBooks(): BelongsToMany
+ {
+ return $this->books()->visible();
+ }
+
/**
* Get the url for this bookshelf.
* @param string|bool $path
/**
* Get the cover image of the shelf
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function cover()
+ public function cover(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
+ /**
+ * Get the type of the image model that is used when storing a cover image.
+ */
+ public function coverImageTypeKey(): string
+ {
+ return 'cover_shelf';
+ }
+
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this shelf contains the given book.
+ * @param Book $book
+ * @return bool
*/
- public function entityRawQuery()
+ public function contains(Book $book): bool
{
- return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ return $this->books()->where('id', '=', $book->id)->count() > 0;
}
/**
- * Check if this shelf contains the given book.
+ * Add a book to the end of this shelf.
* @param Book $book
- * @return bool
*/
- public function contains(Book $book)
+ public function appendBook(Book $book)
{
- return $this->books()->where('id', '=', $book->id)->count() > 0;
+ if ($this->contains($book)) {
+ return;
+ }
+
+ $maxOrder = $this->books()->max('order');
+ $this->books()->attach($book->id, ['order' => $maxOrder + 1]);
}
}
<?php namespace BookStack\Entities;
+use BookStack\Entities\Managers\EntityContext;
use Illuminate\View\View;
class BreadcrumbsViewComposer
/**
* BreadcrumbsViewComposer constructor.
- * @param EntityContextManager $entityContextManager
+ * @param EntityContext $entityContextManager
*/
- public function __construct(EntityContextManager $entityContextManager)
+ public function __construct(EntityContext $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
- if (array_first($crumbs) instanceof Book) {
- $shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
+ $firstCrumb = $crumbs[0] ?? null;
+ if ($firstCrumb instanceof Book) {
+ $shelf = $this->entityContextManager->getContextualShelfForBook($firstCrumb);
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
<?php namespace BookStack\Entities;
-class Chapter extends Entity
+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'];
- /**
- * Get the morph class for this model.
- * @return string
- */
- public function getMorphClass()
- {
- return 'BookStack\\Chapter';
- }
-
- /**
- * Get the book this chapter is within.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function book()
- {
- return $this->belongsTo(Book::class);
- }
-
/**
* Get the pages that this chapter contains.
* @param string $dir
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @return string
+ * Check if this chapter has any child pages.
+ * @return bool
*/
- public function entityRawQuery()
+ public function hasChildren()
{
- return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
+ return count($this->pages) > 0;
}
/**
- * Check if this chapter has any child pages.
- * @return bool
+ * Get the visible pages in this chapter.
*/
- public function hasChildren()
+ public function getVisiblePages(): Collection
{
- return count($this->pages) > 0;
+ return $this->pages()->visible()
+ ->orderBy('draft', 'desc')
+ ->orderBy('priority', 'asc')
+ ->get();
}
}
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Facades\Permissions;
use BookStack\Ownable;
use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
/**
* The base class for book-like items such as pages, chapters & books.
* This is not a database model in itself but extended.
*
- * @property integer $id
+ * @property int $id
* @property string $name
* @property string $slug
* @property Carbon $created_at
* @property int $created_by
* @property int $updated_by
* @property boolean $restricted
+ * @property Collection $tags
+ * @method static Entity|Builder visible()
+ * @method static Entity|Builder hasPermission(string $permission)
+ * @method static Builder withLastView()
+ * @method static Builder withViewCount()
*
* @package BookStack\Entities
*/
public $searchFactor = 1.0;
/**
- * Get the morph class for this model.
- * Set here since, due to folder changes, the namespace used
- * in the database no longer matches the class namespace.
- * @return string
+ * Get the entities that are visible to the current user.
+ */
+ public function scopeVisible(Builder $query)
+ {
+ return $this->scopeHasPermission($query, 'view');
+ }
+
+ /**
+ * Scope the query to those entities that the current user has the given permission for.
+ */
+ public function scopeHasPermission(Builder $query, string $permission)
+ {
+ return Permissions::restrictEntityQuery($query, $permission);
+ }
+
+ /**
+ * Query scope to get the last view from the current user.
*/
- public function getMorphClass()
+ public function scopeWithLastView(Builder $query)
{
- return 'BookStack\\Entity';
+ $viewedAtQuery = View::query()->select('updated_at')
+ ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+ ->where('viewable_type', '=', $this->getMorphClass())
+ ->where('user_id', '=', user()->id)
+ ->take(1);
+
+ return $query->addSelect(['last_viewed_at' => $viewedAtQuery]);
+ }
+
+ /**
+ * Query scope to get the total view count of the entities.
+ */
+ public function scopeWithViewCount(Builder $query)
+ {
+ $viewCountQuery = View::query()->selectRaw('SUM(views) as view_count')
+ ->whereColumn('viewable_id', '=', $this->getTable() . '.id')
+ ->where('viewable_type', '=', $this->getMorphClass())->take(1);
+
+ $query->addSelect(['view_count' => $viewCountQuery]);
}
/**
/**
* Gets the activity objects for this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function activity()
{
- return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
+ return $this->morphMany(Activity::class, 'entity')
+ ->orderBy('created_at', 'desc');
}
/**
return $this->morphMany(View::class, 'viewable');
}
- public function viewCountQuery()
- {
- return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
- }
-
/**
* Get the Tag models that have been user assigned to this entity.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function tags()
{
/**
* Get the related search terms.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function searchTerms()
{
/**
* Get the entity jointPermissions this is connected to.
- * @return \Illuminate\Database\Eloquent\Relations\MorphMany
+ * @return MorphMany
*/
public function jointPermissions()
{
}
/**
- * Return a generalised, common raw query that can be 'unioned' across entities.
+ * Get the url of this entity
+ * @param $path
* @return string
*/
- public function entityRawQuery()
+ public function getUrl($path = '/')
{
- return '';
+ return $path;
}
/**
- * Get the url of this entity
- * @param $path
- * @return string
+ * Rebuild the permissions for this entity.
*/
- public function getUrl($path = '/')
+ public function rebuildPermissions()
{
- return $path;
+ /** @noinspection PhpUnhandledExceptionInspection */
+ Permissions::buildJointPermissionsForEntity($this);
+ }
+
+ /**
+ * Index the current entity for search
+ */
+ public function indexForSearch()
+ {
+ $searchService = app()->make(SearchService::class);
+ $searchService->indexEntity($this);
+ }
+
+ /**
+ * Generate and set a new URL slug for this model.
+ */
+ public function refreshSlug(): string
+ {
+ $generator = new SlugGenerator($this);
+ $this->slug = $generator->generate();
+ return $this->slug;
}
}
/**
* EntityProvider constructor.
- * @param Bookshelf $bookshelf
- * @param Book $book
- * @param Chapter $chapter
- * @param Page $page
- * @param PageRevision $pageRevision
*/
public function __construct(
Bookshelf $bookshelf,
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
- * @return Entity[]
*/
- public function all()
+ public function all(): array
{
return [
'bookshelf' => $this->bookshelf,
/**
* Get an entity instance by it's basic name.
- * @param string $type
- * @return Entity
*/
- public function get(string $type)
+ public function get(string $type): Entity
{
$type = strtolower($type);
return $this->all()[$type];
/**
* Get the morph classes, as an array, for a single or multiple types.
- * @param string|array $types
- * @return array<string>
*/
- public function getMorphClasses($types)
+ public function getMorphClasses(array $types): array
{
- if (is_string($types)) {
- $types = [$types];
- }
-
$morphClasses = [];
foreach ($types as $type) {
$model = $this->get($type);
<?php namespace BookStack\Entities;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
use BookStack\Uploads\ImageService;
+use DomPDF;
+use Exception;
+use SnappyPDF;
+use Throwable;
class ExportService
{
- protected $entityRepo;
protected $imageService;
/**
* ExportService constructor.
- * @param EntityRepo $entityRepo
- * @param ImageService $imageService
*/
- public function __construct(EntityRepo $entityRepo, ImageService $imageService)
+ public function __construct(ImageService $imageService)
{
- $this->entityRepo = $entityRepo;
$this->imageService = $imageService;
}
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
- * @param \BookStack\Entities\Page $page
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function pageToContainedHtml(Page $page)
{
- $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
$pageHtml = view('pages/export', [
'page' => $page
])->render();
/**
* Convert a chapter to a self-contained HTML file.
- * @param \BookStack\Entities\Chapter $chapter
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function chapterToContainedHtml(Chapter $chapter)
{
- $pages = $this->entityRepo->getChapterChildren($chapter);
+ $pages = $chapter->getVisiblePages();
$pages->each(function ($page) {
- $page->html = $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
});
$html = view('chapters/export', [
'chapter' => $chapter,
/**
* Convert a book to a self-contained HTML file.
- * @param Book $book
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function bookToContainedHtml(Book $book)
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
/**
* Convert a page to a PDF file.
- * @param Page $page
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function pageToPdf(Page $page)
{
- $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
$html = view('pages/pdf', [
'page' => $page
])->render();
/**
* Convert a chapter to a PDF file.
- * @param \BookStack\Entities\Chapter $chapter
- * @return mixed|string
- * @throws \Throwable
+ * @throws Throwable
*/
public function chapterToPdf(Chapter $chapter)
{
- $pages = $this->entityRepo->getChapterChildren($chapter);
+ $pages = $chapter->getVisiblePages();
$pages->each(function ($page) {
- $page->html = $this->entityRepo->renderPage($page);
+ $page->html = (new PageContent($page))->render();
});
+
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
+
return $this->htmlToPdf($html);
}
/**
- * Convert a book to a PDF file
- * @param \BookStack\Entities\Book $book
- * @return string
- * @throws \Throwable
+ * Convert a book to a PDF file.
+ * @throws Throwable
*/
public function bookToPdf(Book $book)
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
}
/**
- * Convert normal webpage HTML to a PDF.
- * @param $html
- * @return string
- * @throws \Exception
+ * Convert normal web-page HTML to a PDF.
+ * @throws Exception
*/
- protected function htmlToPdf($html)
+ protected function htmlToPdf(string $html): string
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
- $pdf = \SnappyPDF::loadHTML($containedHtml);
+ $pdf = SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
- $pdf = \DomPDF::loadHTML($containedHtml);
+ $pdf = DomPDF::loadHTML($containedHtml);
}
return $pdf->output();
}
/**
* Bundle of the contents of a html file to be self-contained.
- * @param $htmlContent
- * @return mixed|string
- * @throws \Exception
+ * @throws Exception
*/
- protected function containHtml($htmlContent)
+ protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to provide a nice final output.
- * @param Page $page
- * @return mixed
*/
- public function pageToPlainText(Page $page)
+ public function pageToPlainText(Page $page): string
{
- $html = $this->entityRepo->renderPage($page);
+ $html = (new PageContent($page))->render();
$text = strip_tags($html);
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
/**
* Convert a chapter into a plain text string.
- * @param \BookStack\Entities\Chapter $chapter
- * @return string
*/
- public function chapterToPlainText(Chapter $chapter)
+ public function chapterToPlainText(Chapter $chapter): string
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
/**
* Convert a book into a plain text string.
- * @param Book $book
- * @return string
*/
- public function bookToPlainText(Book $book)
+ public function bookToPlainText(Book $book): string
{
- $bookTree = $this->entityRepo->getBookChildren($book, true, true);
+ $bookTree = (new BookContents($book))->getTree(false, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
--- /dev/null
+<?php
+
+
+namespace BookStack\Entities;
+
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+interface HasCoverImage
+{
+
+ /**
+ * Get the cover image for this item.
+ */
+ public function cover(): BelongsTo;
+
+ /**
+ * Get the type of the image model that is used when storing a cover image.
+ */
+ public function coverImageTypeKey(): string;
+}
--- /dev/null
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\BookChild;
+use BookStack\Entities\Chapter;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Page;
+use BookStack\Exceptions\SortOperationException;
+use Illuminate\Support\Collection;
+
+class BookContents
+{
+
+ /**
+ * @var Book
+ */
+ protected $book;
+
+ /**
+ * BookContents constructor.
+ * @param $book
+ */
+ public function __construct(Book $book)
+ {
+ $this->book = $book;
+ }
+
+ /**
+ * Get the current priority of the last item
+ * at the top-level of the book.
+ */
+ public function getLastPriority(): int
+ {
+ $maxPage = Page::visible()->where('book_id', '=', $this->book->id)
+ ->where('draft', '=', false)
+ ->where('chapter_id', '=', 0)->max('priority');
+ $maxChapter = Chapter::visible()->where('book_id', '=', $this->book->id)
+ ->max('priority');
+ return max($maxChapter, $maxPage, 1);
+ }
+
+ /**
+ * Get the contents as a sorted collection tree.
+ * TODO - Support $renderPages option
+ */
+ public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
+ {
+ $pages = $this->getPages($showDrafts);
+ $chapters = Chapter::visible()->where('book_id', '=', $this->book->id)->get();
+ $all = collect()->concat($pages)->concat($chapters);
+ $chapterMap = $chapters->keyBy('id');
+ $lonePages = collect();
+
+ $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()));
+ } else {
+ $lonePages = $lonePages->concat($pages);
+ }
+ });
+
+ $all->each(function (Entity $entity) {
+ $entity->setRelation('book', $this->book);
+ });
+
+ return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
+ }
+
+ /**
+ * Function for providing a sorting score for an entity in relation to the
+ * other items within the book.
+ */
+ protected function bookChildSortFunc(): callable
+ {
+ return function (Entity $entity) {
+ if (isset($entity['draft']) && $entity['draft']) {
+ return -100;
+ }
+ return $entity['priority'] ?? 0;
+ };
+ }
+
+ /**
+ * Get the visible pages within this book.
+ */
+ protected function getPages(bool $showDrafts = false): Collection
+ {
+ $query = Page::visible()->where('book_id', '=', $this->book->id);
+
+ if (!$showDrafts) {
+ $query->where('draft', '=', false);
+ }
+
+ return $query->get();
+ }
+
+ /**
+ * Sort the books content using the given map.
+ * The map is a single-dimension collection of objects in the following format:
+ * {
+ * +"id": "294" (ID of item)
+ * +"sort": 1 (Sort order index)
+ * +"parentChapter": false (ID of parent chapter, as string, or false)
+ * +"type": "page" (Entity type of item)
+ * +"book": "1" (Id of book to place item in)
+ * }
+ *
+ * Returns a list of books that were involved in the operation.
+ * @throws SortOperationException
+ */
+ public function sortUsingMap(Collection $sortMap): Collection
+ {
+ // Load models into map
+ $this->loadModelsIntoSortMap($sortMap);
+ $booksInvolved = $this->getBooksInvolvedInSort($sortMap);
+
+ // Perform the sort
+ $sortMap->each(function ($mapItem) {
+ $this->applySortUpdates($mapItem);
+ });
+
+ // Update permissions and activity.
+ $booksInvolved->each(function (Book $book) {
+ $book->rebuildPermissions();
+ });
+
+ return $booksInvolved;
+ }
+
+ /**
+ * Using the given sort map item, detect changes for the related model
+ * and update it if required.
+ */
+ protected function applySortUpdates(\stdClass $sortMapItem)
+ {
+ /** @var BookChild $model */
+ $model = $sortMapItem->model;
+
+ $priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
+ $bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
+ $chapterChanged = ($sortMapItem->type === 'page') && intval($model->chapter_id) !== $sortMapItem->parentChapter;
+
+ if ($bookChanged) {
+ $model->changeBook($sortMapItem->book);
+ }
+
+ if ($chapterChanged) {
+ $model->chapter_id = intval($sortMapItem->parentChapter);
+ $model->save();
+ }
+
+ if ($priorityChanged) {
+ $model->priority = intval($sortMapItem->sort);
+ $model->save();
+ }
+ }
+
+ /**
+ * Load models from the database into the given sort map.
+ */
+ protected function loadModelsIntoSortMap(Collection $sortMap): void
+ {
+ $keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
+ return $sortMapItem->type . ':' . $sortMapItem->id;
+ });
+ $pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
+ $chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
+
+ $pages = Page::visible()->whereIn('id', $pageIds)->get();
+ $chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
+
+ foreach ($pages as $page) {
+ $sortItem = $keyMap->get('page:' . $page->id);
+ $sortItem->model = $page;
+ }
+
+ foreach ($chapters as $chapter) {
+ $sortItem = $keyMap->get('chapter:' . $chapter->id);
+ $sortItem->model = $chapter;
+ }
+ }
+
+ /**
+ * Get the books involved in a sort.
+ * The given sort map should have its models loaded first.
+ * @throws SortOperationException
+ */
+ protected function getBooksInvolvedInSort(Collection $sortMap): Collection
+ {
+ $bookIdsInvolved = collect([$this->book->id]);
+ $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
+ $bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
+ $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
+
+ $books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
+
+ if (count($books) !== count($bookIdsInvolved)) {
+ throw new SortOperationException("Could not find all books requested in sort operation");
+ }
+
+ return $books;
+ }
+}
-<?php namespace BookStack\Entities;
+<?php namespace BookStack\Entities\Managers;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
use Illuminate\Session\Store;
-class EntityContextManager
+class EntityContext
{
protected $session;
- protected $entityRepo;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
- * @param Store $session
- * @param EntityRepo $entityRepo
*/
- public function __construct(Store $session, EntityRepo $entityRepo)
+ public function __construct(Store $session)
{
$this->session = $session;
- $this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
- * @param Book $book
- * @return Bookshelf|null
*/
- public function getContextualShelfForBook(Book $book)
+ public function getContextualShelfForBook(Book $book): ?Bookshelf
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
- if (is_int($contextBookshelfId)) {
- /** @var Bookshelf $shelf */
- $shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
-
- if ($shelf && $shelf->contains($book)) {
- return $shelf;
- }
+ if (!is_int($contextBookshelfId)) {
+ return null;
}
- return null;
+
+ $shelf = Bookshelf::visible()->find($contextBookshelfId);
+ $shelfContainsBook = $shelf && $shelf->contains($book);
+
+ return $shelfContainsBook ? $shelf : null;
}
/**
--- /dev/null
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use DOMDocument;
+use DOMElement;
+use DOMNodeList;
+use DOMXPath;
+
+class PageContent
+{
+
+ protected $page;
+
+ /**
+ * PageContent constructor.
+ */
+ public function __construct(Page $page)
+ {
+ $this->page = $page;
+ }
+
+ /**
+ * Update the content of the page with new provided HTML.
+ */
+ public function setNewHTML(string $html)
+ {
+ $this->page->html = $this->formatHtml($html);
+ $this->page->text = $this->toPlainText();
+ }
+
+ /**
+ * Formats a page's html to be tagged correctly within the system.
+ */
+ protected function formatHtml(string $htmlText): string
+ {
+ if ($htmlText == '') {
+ return $htmlText;
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
+
+ $container = $doc->documentElement;
+ $body = $container->childNodes->item(0);
+ $childNodes = $body->childNodes;
+
+ // Set ids on top-level nodes
+ $idMap = [];
+ foreach ($childNodes as $index => $childNode) {
+ $this->setUniqueId($childNode, $idMap);
+ }
+
+ // Ensure no duplicate ids within child items
+ $xPath = new DOMXPath($doc);
+ $idElems = $xPath->query('//body//*//*[@id]');
+ foreach ($idElems as $domElem) {
+ $this->setUniqueId($domElem, $idMap);
+ }
+
+ // Generate inner html as a string
+ $html = '';
+ foreach ($childNodes as $childNode) {
+ $html .= $doc->saveHTML($childNode);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Set a unique id on the given DOMElement.
+ * A map for existing ID's should be passed in to check for current existence.
+ * @param DOMElement $element
+ * @param array $idMap
+ */
+ protected function setUniqueId($element, array &$idMap)
+ {
+ if (get_class($element) !== 'DOMElement') {
+ return;
+ }
+
+ // Overwrite id if not a BookStack custom id
+ $existingId = $element->getAttribute('id');
+ if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
+ $idMap[$existingId] = true;
+ return;
+ }
+
+ // Create an unique id for the element
+ // Uses the content as a basis to ensure output is the same every time
+ // the same content is passed through.
+ $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
+ $newId = urlencode($contentId);
+ $loopIndex = 0;
+
+ while (isset($idMap[$newId])) {
+ $newId = urlencode($contentId . '-' . $loopIndex);
+ $loopIndex++;
+ }
+
+ $element->setAttribute('id', $newId);
+ $idMap[$newId] = true;
+ }
+
+ /**
+ * Get a plain-text visualisation of this page.
+ */
+ protected function toPlainText(): string
+ {
+ $html = $this->render(true);
+ return strip_tags($html);
+ }
+
+ /**
+ * Render the page for viewing
+ */
+ public function render(bool $blankIncludes = false) : string
+ {
+ $content = $this->page->html;
+
+ if (!config('app.allow_content_scripts')) {
+ $content = $this->escapeScripts($content);
+ }
+
+ if ($blankIncludes) {
+ $content = $this->blankPageIncludes($content);
+ } else {
+ $content = $this->parsePageIncludes($content);
+ }
+
+ return $content;
+ }
+
+ /**
+ * Parse the headers on the page to get a navigation menu
+ */
+ public function getNavigation(string $htmlContent): array
+ {
+ if (empty($htmlContent)) {
+ return [];
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
+ $xPath = new DOMXPath($doc);
+ $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
+
+ return $headers ? $this->headerNodesToLevelList($headers) : [];
+ }
+
+ /**
+ * Convert a DOMNodeList into an array of readable header attributes
+ * with levels normalised to the lower header level.
+ */
+ protected function headerNodesToLevelList(DOMNodeList $nodeList): array
+ {
+ $tree = collect($nodeList)->map(function ($header) {
+ $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
+ $text = mb_substr($text, 0, 100);
+
+ return [
+ 'nodeName' => strtolower($header->nodeName),
+ 'level' => intval(str_replace('h', '', $header->nodeName)),
+ 'link' => '#' . $header->getAttribute('id'),
+ 'text' => $text,
+ ];
+ })->filter(function ($header) {
+ return mb_strlen($header['text']) > 0;
+ });
+
+ // Shift headers if only smaller headers have been used
+ $levelChange = ($tree->pluck('level')->min() - 1);
+ $tree = $tree->map(function ($header) use ($levelChange) {
+ $header['level'] -= ($levelChange);
+ return $header;
+ });
+
+ return $tree->toArray();
+ }
+
+ /**
+ * Remove any page include tags within the given HTML.
+ */
+ protected function blankPageIncludes(string $html) : string
+ {
+ return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
+ }
+
+ /**
+ * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
+ */
+ protected function parsePageIncludes(string $html) : string
+ {
+ $matches = [];
+ preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
+
+ foreach ($matches[1] as $index => $includeId) {
+ $fullMatch = $matches[0][$index];
+ $splitInclude = explode('#', $includeId, 2);
+
+ // Get page id from reference
+ $pageId = intval($splitInclude[0]);
+ if (is_nan($pageId)) {
+ continue;
+ }
+
+ // Find page and skip this if page not found
+ $matchedPage = Page::visible()->find($pageId);
+ if ($matchedPage === null) {
+ $html = str_replace($fullMatch, '', $html);
+ continue;
+ }
+
+ // If we only have page id, just insert all page html and continue.
+ if (count($splitInclude) === 1) {
+ $html = str_replace($fullMatch, $matchedPage->html, $html);
+ continue;
+ }
+
+ // Create and load HTML into a document
+ $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
+ $html = str_replace($fullMatch, trim($innerContent), $html);
+ }
+
+ return $html;
+ }
+
+
+ /**
+ * Fetch the content from a specific section of the given page.
+ */
+ protected function fetchSectionOfPage(Page $page, string $sectionId): string
+ {
+ $topLevelTags = ['table', 'ul', 'ol'];
+ $doc = new DOMDocument();
+ libxml_use_internal_errors(true);
+ $doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
+
+ // Search included content for the id given and blank out if not exists.
+ $matchingElem = $doc->getElementById($sectionId);
+ if ($matchingElem === null) {
+ return '';
+ }
+
+ // Otherwise replace the content with the found content
+ // Checks if the top-level wrapper should be included by matching on tag types
+ $innerContent = '';
+ $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
+ if ($isTopLevel) {
+ $innerContent .= $doc->saveHTML($matchingElem);
+ } else {
+ foreach ($matchingElem->childNodes as $childNode) {
+ $innerContent .= $doc->saveHTML($childNode);
+ }
+ }
+ libxml_clear_errors();
+
+ return $innerContent;
+ }
+
+ /**
+ * Escape script tags within HTML content.
+ */
+ protected function escapeScripts(string $html) : string
+ {
+ if (empty($html)) {
+ return $html;
+ }
+
+ libxml_use_internal_errors(true);
+ $doc = new DOMDocument();
+ $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
+ $xPath = new DOMXPath($doc);
+
+ // Remove standard script tags
+ $scriptElems = $xPath->query('//script');
+ foreach ($scriptElems as $scriptElem) {
+ $scriptElem->parentNode->removeChild($scriptElem);
+ }
+
+ // Remove data or JavaScript iFrames
+ $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
+ foreach ($badIframes as $badIframe) {
+ $badIframe->parentNode->removeChild($badIframe);
+ }
+
+ // Remove 'on*' attributes
+ $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
+ foreach ($onAttributes as $attr) {
+ /** @var \DOMAttr $attr*/
+ $attrName = $attr->nodeName;
+ $attr->parentNode->removeAttribute($attrName);
+ }
+
+ $html = '';
+ $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
+ foreach ($topElems as $child) {
+ $html .= $doc->saveHTML($child);
+ }
+
+ return $html;
+ }
+}
--- /dev/null
+<?php namespace BookStack\Entities\Managers;
+
+use BookStack\Entities\Page;
+use BookStack\Entities\PageRevision;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Builder;
+
+class PageEditActivity
+{
+
+ protected $page;
+
+ /**
+ * PageEditActivity constructor.
+ */
+ public function __construct(Page $page)
+ {
+ $this->page = $page;
+ }
+
+ /**
+ * Check if there's active editing being performed on this page.
+ * @return bool
+ */
+ public function hasActiveEditing(): bool
+ {
+ return $this->activePageEditingQuery(60)->count() > 0;
+ }
+
+ /**
+ * Get a notification message concerning the editing activity on the page.
+ */
+ public function activeEditingMessage(): string
+ {
+ $pageDraftEdits = $this->activePageEditingQuery(60)->get();
+ $count = $pageDraftEdits->count();
+
+ $userMessage = $count > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $count]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
+ $timeMessage = trans('entities.pages_draft_edit_active.time_b', ['minCount'=> 60]);
+ return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+ }
+
+ /**
+ * Get the message to show when the user will be editing one of their drafts.
+ * @param PageRevision $draft
+ * @return string
+ */
+ public function getEditingActiveDraftMessage(PageRevision $draft): string
+ {
+ $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
+ if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
+ return $message;
+ }
+ return $message . "\n" . trans('entities.pages_draft_edited_notification');
+ }
+
+ /**
+ * A query to check for active update drafts on a particular page
+ * within the last given many minutes.
+ */
+ protected function activePageEditingQuery(int $withinMinutes): Builder
+ {
+ $checkTime = Carbon::now()->subMinutes($withinMinutes);
+ $query = PageRevision::query()
+ ->where('type', '=', 'update_draft')
+ ->where('page_id', '=', $this->page->id)
+ ->where('updated_at', '>', $this->page->updated_at)
+ ->where('created_by', '!=', user()->id)
+ ->where('updated_at', '>=', $checkTime)
+ ->with('createdBy');
+
+ return $query;
+ }
+}
--- /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;
use BookStack\Uploads\Attachment;
-
-class Page extends Entity
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Permissions;
+
+/**
+ * Class Page
+ * @property int $chapter_id
+ * @property string $html
+ * @property string $markdown
+ * @property string $text
+ * @property bool $template
+ * @property bool $draft
+ * @property int $revision_count
+ * @property Chapter $chapter
+ * @property Collection $attachments
+ */
+class Page extends BookChild
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
public $textField = 'text';
/**
- * Get the morph class for this model.
- * @return string
+ * Get the entities that are visible to the current user.
*/
- public function getMorphClass()
+ public function scopeVisible(Builder $query)
{
- return 'BookStack\\Page';
+ $query = Permissions::enforceDraftVisiblityOnQuery($query);
+ return parent::scopeVisible($query);
}
/**
return $array;
}
- /**
- * Get the book this page sits in.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function book()
- {
- return $this->belongsTo(Book::class);
- }
-
/**
* Get the parent item
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
- public function parent()
+ public function parent(): Entity
{
- return $this->chapter_id ? $this->chapter() : $this->book();
+ return $this->chapter_id ? $this->chapter : $this->book;
}
/**
* Get the chapter that this page is in, If applicable.
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ * @return BelongsTo
*/
public function chapter()
{
*/
public function revisions()
{
- return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
+ return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
}
/**
* Get the attachments assigned to this page.
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ * @return HasMany
*/
public function attachments()
{
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
+ $url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
if ($path !== false) {
- return url('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
+ $url .= '/' . trim($path, '/');
}
- return url('/books/' . urlencode($bookSlug) . $midText . $idComponent);
- }
-
- /**
- * Return a generalised, common raw query that can be 'unioned' across entities.
- * @param bool $withContent
- * @return string
- */
- public function entityRawQuery($withContent = false)
- {
- $htmlQuery = $withContent ? 'html' : "'' as html";
- return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
+ return url($url);
}
/**
* Get the current revision for the page if existing
- * @return \BookStack\Entities\PageRevision|null
+ * @return PageRevision|null
*/
public function getCurrentRevision()
{
use BookStack\Auth\User;
use BookStack\Model;
+use Carbon\Carbon;
+/**
+ * Class PageRevision
+ * @property int $page_id
+ * @property string $slug
+ * @property string $book_slug
+ * @property int $created_by
+ * @property Carbon $created_at
+ * @property string $type
+ * @property string $summary
+ * @property string $markdown
+ * @property string $html
+ * @property int $revision_number
+ */
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
/**
* Get the previous revision for the same page if existing
- * @return \BookStack\PageRevision|null
+ * @return \BookStack\Entities\PageRevision|null
*/
public function getPrevious()
{
- if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
- return static::find($id);
+ $id = static::newQuery()->where('page_id', '=', $this->page_id)
+ ->where('id', '<', $this->id)
+ ->max('id');
+
+ if ($id) {
+ return static::query()->find($id);
}
+
return null;
}
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Entity;
+use BookStack\Entities\HasCoverImage;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Uploads\ImageRepo;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BaseRepo
+{
+
+ protected $tagRepo;
+ protected $imageRepo;
+
+
+ /**
+ * BaseRepo constructor.
+ * @param $tagRepo
+ */
+ public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
+ {
+ $this->tagRepo = $tagRepo;
+ $this->imageRepo = $imageRepo;
+ }
+
+ /**
+ * Create a new entity in the system
+ */
+ public function create(Entity $entity, array $input)
+ {
+ $entity->fill($input);
+ $entity->forceFill([
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ ]);
+ $entity->refreshSlug();
+ $entity->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+ }
+
+ $entity->rebuildPermissions();
+ $entity->indexForSearch();
+ }
+
+ /**
+ * Update the given entity.
+ */
+ public function update(Entity $entity, array $input)
+ {
+ $entity->fill($input);
+ $entity->updated_by = user()->id;
+
+ if ($entity->isDirty('name')) {
+ $entity->refreshSlug();
+ }
+
+ $entity->save();
+
+ if (isset($input['tags'])) {
+ $this->tagRepo->saveTagsToEntity($entity, $input['tags']);
+ }
+
+ $entity->rebuildPermissions();
+ $entity->indexForSearch();
+ }
+
+ /**
+ * Update the given items' cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws \Exception
+ */
+ public function updateCoverImage(HasCoverImage $entity, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ if ($coverImage) {
+ $this->imageRepo->destroyImage($entity->cover);
+ $image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
+ $entity->cover()->associate($image);
+ }
+
+ if ($removeImage) {
+ $this->imageRepo->destroyImage($entity->cover);
+ $entity->image_id = 0;
+ $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();
+ }
+}
--- /dev/null
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Actions\TagRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Uploads\ImageRepo;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookRepo
+{
+
+ protected $baseRepo;
+ protected $tagRepo;
+ protected $imageRepo;
+
+ /**
+ * BookRepo constructor.
+ * @param $tagRepo
+ */
+ public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
+ {
+ $this->baseRepo = $baseRepo;
+ $this->tagRepo = $tagRepo;
+ $this->imageRepo = $imageRepo;
+ }
+
+ /**
+ * Get all books in a paginated format.
+ */
+ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+ {
+ return Book::visible()->orderBy($sort, $order)->paginate($count);
+ }
+
+ /**
+ * Get the books that were most recently viewed by this user.
+ */
+ public function getRecentlyViewed(int $count = 20): Collection
+ {
+ return Book::visible()->withLastView()
+ ->having('last_viewed_at', '>', 0)
+ ->orderBy('last_viewed_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most popular books in the system.
+ */
+ public function getPopular(int $count = 20): Collection
+ {
+ return Book::visible()->withViewCount()
+ ->having('view_count', '>', 0)
+ ->orderBy('view_count', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most recently created books from the system.
+ */
+ public function getRecentlyCreated(int $count = 20): Collection
+ {
+ return Book::visible()->orderBy('created_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get a book by its slug.
+ */
+ public function getBySlug(string $slug): Book
+ {
+ $book = Book::visible()->where('slug', '=', $slug)->first();
+
+ if ($book === null) {
+ throw new NotFoundException(trans('errors.book_not_found'));
+ }
+
+ return $book;
+ }
+
+ /**
+ * Create a new book in the system
+ */
+ public function create(array $input): Book
+ {
+ $book = new Book();
+ $this->baseRepo->create($book, $input);
+ return $book;
+ }
+
+ /**
+ * Update the given book.
+ */
+ public function update(Book $book, array $input): Book
+ {
+ $this->baseRepo->update($book, $input);
+ return $book;
+ }
+
+ /**
+ * Update the given book's cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws Exception
+ */
+ public function updateCoverImage(Book $book, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ $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
+ */
+ public function destroy(Book $book)
+ {
+ $trashCan = new TrashCan();
+ $trashCan->destroyBook($book);
+ }
+}
--- /dev/null
+<?php namespace BookStack\Entities\Repos;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\TrashCan;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
+use Exception;
+use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Http\UploadedFile;
+use Illuminate\Support\Collection;
+
+class BookshelfRepo
+{
+ protected $baseRepo;
+
+ /**
+ * BookshelfRepo constructor.
+ * @param $baseRepo
+ */
+ public function __construct(BaseRepo $baseRepo)
+ {
+ $this->baseRepo = $baseRepo;
+ }
+
+ /**
+ * Get all bookshelves in a paginated format.
+ */
+ public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
+ {
+ return Bookshelf::visible()->with('visibleBooks')
+ ->orderBy($sort, $order)->paginate($count);
+ }
+
+ /**
+ * Get the bookshelves that were most recently viewed by this user.
+ */
+ public function getRecentlyViewed(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->withLastView()
+ ->having('last_viewed_at', '>', 0)
+ ->orderBy('last_viewed_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most popular bookshelves in the system.
+ */
+ public function getPopular(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->withViewCount()
+ ->having('view_count', '>', 0)
+ ->orderBy('view_count', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get the most recently created bookshelves from the system.
+ */
+ public function getRecentlyCreated(int $count = 20): Collection
+ {
+ return Bookshelf::visible()->orderBy('created_at', 'desc')
+ ->take($count)->get();
+ }
+
+ /**
+ * Get a shelf by its slug.
+ */
+ public function getBySlug(string $slug): Bookshelf
+ {
+ $shelf = Bookshelf::visible()->where('slug', '=', $slug)->first();
+
+ if ($shelf === null) {
+ throw new NotFoundException(trans('errors.bookshelf_not_found'));
+ }
+
+ return $shelf;
+ }
+
+ /**
+ * Create a new shelf in the system.
+ */
+ public function create(array $input, array $bookIds): Bookshelf
+ {
+ $shelf = new Bookshelf();
+ $this->baseRepo->create($shelf, $input);
+ $this->updateBooks($shelf, $bookIds);
+ return $shelf;
+ }
+
+ /**
+ * Create a new shelf in the system.
+ */
+ public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
+ {
+ $this->baseRepo->update($shelf, $input);
+ $this->updateBooks($shelf, $bookIds);
+ return $shelf;
+ }
+
+ /**
+ * Update which books are assigned to this shelf by
+ * syncing the given book ids.
+ * Function ensures the books are visible to the current user and existing.
+ */
+ protected function updateBooks(Bookshelf $shelf, array $bookIds)
+ {
+ $numericIDs = collect($bookIds)->map(function ($id) {
+ return intval($id);
+ });
+
+ $syncData = Book::visible()
+ ->whereIn('id', $bookIds)
+ ->get(['id'])->pluck('id')->mapWithKeys(function ($bookId) use ($numericIDs) {
+ return [$bookId => ['order' => $numericIDs->search($bookId)]];
+ });
+
+ $shelf->books()->sync($syncData);
+ }
+
+ /**
+ * Update the given shelf cover image, or clear it.
+ * @throws ImageUploadException
+ * @throws Exception
+ */
+ public function updateCoverImage(Bookshelf $shelf, UploadedFile $coverImage = null, bool $removeImage = false)
+ {
+ $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 copyDownPermissions(Bookshelf $shelf): int
+ {
+ $shelfPermissions = $shelf->permissions()->get(['role_id', 'action'])->toArray();
+ $shelfBooks = $shelf->books()->get();
+ $updatedBookCount = 0;
+
+ /** @var Book $book */
+ foreach ($shelfBooks as $book) {
+ if (!userCan('restrictions-manage', $book)) {
+ continue;
+ }
+ $book->permissions()->delete();
+ $book->restricted = $shelf->restricted;
+ $book->permissions()->createMany($shelfPermissions);
+ $book->save();
+ $book->rebuildPermissions();
+ $updatedBookCount++;
+ }
+
+ return $updatedBookCount;
+ }
+
+ /**
+ * Remove a bookshelf from the system.
+ * @throws Exception
+ */
+ public function destroy(Bookshelf $shelf)
+ {
+ $trashCan = new TrashCan();
+ $trashCan->destroyShelf($shelf);
+ }
+}
--- /dev/null
+<?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\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use Exception;
+use Illuminate\Contracts\Container\BindingResolutionException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Support\Collection;
+
+class ChapterRepo
+{
+
+ protected $baseRepo;
+
+ /**
+ * ChapterRepo constructor.
+ * @param $baseRepo
+ */
+ public function __construct(BaseRepo $baseRepo)
+ {
+ $this->baseRepo = $baseRepo;
+ }
+
+ /**
+ * Get a chapter via the slug.
+ * @throws NotFoundException
+ */
+ public function getBySlug(string $bookSlug, string $chapterSlug): Chapter
+ {
+ $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->first();
+
+ if ($chapter === null) {
+ throw new NotFoundException(trans('errors.chapter_not_found'));
+ }
+
+ return $chapter;
+ }
+
+ /**
+ * Create a new chapter in the system.
+ */
+ public function create(array $input, Book $parentBook): Chapter
+ {
+ $chapter = new Chapter();
+ $chapter->book_id = $parentBook->id;
+ $chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
+ $this->baseRepo->create($chapter, $input);
+ return $chapter;
+ }
+
+ /**
+ * Update the given chapter.
+ */
+ public function update(Chapter $chapter, array $input): Chapter
+ {
+ $this->baseRepo->update($chapter, $input);
+ 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);
+ }
+
+ /**
+ * Move the given chapter into a new parent book.
+ * The $parentIdentifier must be a string of the following format:
+ * 'book:<id>' (book:5)
+ * @throws MoveOperationException
+ */
+ public function move(Chapter $chapter, string $parentIdentifier): Book
+ {
+ $stringExploded = explode(':', $parentIdentifier);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
+
+ if ($entityType !== 'book') {
+ throw new MoveOperationException('Chapters can only be moved into books');
+ }
+
+ $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();
+ return $parent;
+ }
+}
+++ /dev/null
-<?php namespace BookStack\Entities\Repos;
-
-use Activity;
-use BookStack\Actions\TagRepo;
-use BookStack\Actions\ViewService;
-use BookStack\Auth\Permissions\PermissionService;
-use BookStack\Auth\User;
-use BookStack\Entities\Book;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\Chapter;
-use BookStack\Entities\Entity;
-use BookStack\Entities\EntityProvider;
-use BookStack\Entities\Page;
-use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
-use BookStack\Uploads\AttachmentService;
-use DOMDocument;
-use DOMNode;
-use DOMXPath;
-use Illuminate\Contracts\Pagination\LengthAwarePaginator;
-use Illuminate\Database\Eloquent\Builder;
-use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
-use Throwable;
-
-class EntityRepo
-{
-
- /**
- * @var EntityProvider
- */
- protected $entityProvider;
-
- /**
- * @var PermissionService
- */
- protected $permissionService;
-
- /**
- * @var ViewService
- */
- protected $viewService;
-
- /**
- * @var TagRepo
- */
- protected $tagRepo;
-
- /**
- * @var SearchService
- */
- protected $searchService;
-
- /**
- * EntityRepo constructor.
- * @param EntityProvider $entityProvider
- * @param ViewService $viewService
- * @param PermissionService $permissionService
- * @param TagRepo $tagRepo
- * @param SearchService $searchService
- */
- public function __construct(
- EntityProvider $entityProvider,
- ViewService $viewService,
- PermissionService $permissionService,
- TagRepo $tagRepo,
- SearchService $searchService
- ) {
- $this->entityProvider = $entityProvider;
- $this->viewService = $viewService;
- $this->permissionService = $permissionService;
- $this->tagRepo = $tagRepo;
- $this->searchService = $searchService;
- }
-
- /**
- * Base query for searching entities via permission system
- * @param string $type
- * @param bool $allowDrafts
- * @param string $permission
- * @return \Illuminate\Database\Query\Builder
- */
- protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
- {
- $q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
- if (strtolower($type) === 'page' && !$allowDrafts) {
- $q = $q->where('draft', '=', false);
- }
- return $q;
- }
-
- /**
- * Check if an entity with the given id exists.
- * @param $type
- * @param $id
- * @return bool
- */
- public function exists($type, $id)
- {
- return $this->entityQuery($type)->where('id', '=', $id)->exists();
- }
-
- /**
- * Get an entity by ID
- * @param string $type
- * @param integer $id
- * @param bool $allowDrafts
- * @param bool $ignorePermissions
- * @return Entity
- */
- public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
- {
- $query = $this->entityQuery($type, $allowDrafts);
-
- if ($ignorePermissions) {
- $query = $this->entityProvider->get($type)->newQuery();
- }
-
- return $query->find($id);
- }
-
- /**
- * @param string $type
- * @param []int $ids
- * @param bool $allowDrafts
- * @param bool $ignorePermissions
- * @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
- */
- public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
- {
- $query = $this->entityQuery($type, $allowDrafts);
-
- if ($ignorePermissions) {
- $query = $this->entityProvider->get($type)->newQuery();
- }
-
- return $query->whereIn('id', $ids)->get();
- }
-
- /**
- * Get an entity by its url slug.
- * @param string $type
- * @param string $slug
- * @param string|bool $bookSlug
- * @return Entity
- * @throws NotFoundException
- */
- public function getBySlug($type, $slug, $bookSlug = false)
- {
- $q = $this->entityQuery($type)->where('slug', '=', $slug);
-
- if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
- $q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
- $query->select('id')
- ->from($this->entityProvider->book->getTable())
- ->where('slug', '=', $bookSlug)->limit(1);
- });
- }
- $entity = $q->first();
- if ($entity === null) {
- throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
- }
- return $entity;
- }
-
-
- /**
- * Get all entities of a type with the given permission, limited by count unless count is false.
- * @param string $type
- * @param integer|bool $count
- * @param string $permission
- * @return Collection
- */
- public function getAll($type, $count = 20, $permission = 'view')
- {
- $q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
- if ($count !== false) {
- $q = $q->take($count);
- }
- return $q->get();
- }
-
- /**
- * Get all entities in a paginated format
- * @param $type
- * @param int $count
- * @param string $sort
- * @param string $order
- * @param null|callable $queryAddition
- * @return LengthAwarePaginator
- */
- public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
- {
- $query = $this->entityQuery($type);
- $query = $this->addSortToQuery($query, $sort, $order);
- if ($queryAddition) {
- $queryAddition($query);
- }
- return $query->paginate($count);
- }
-
- /**
- * Add sorting operations to an entity query.
- * @param Builder $query
- * @param string $sort
- * @param string $order
- * @return Builder
- */
- protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
- {
- $order = ($order === 'asc') ? 'asc' : 'desc';
- $propertySorts = ['name', 'created_at', 'updated_at'];
-
- if (in_array($sort, $propertySorts)) {
- return $query->orderBy($sort, $order);
- }
-
- return $query;
- }
-
- /**
- * Get the most recently created entities of the given type.
- * @param string $type
- * @param int $count
- * @param int $page
- * @param bool|callable $additionalQuery
- * @return Collection
- */
- public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
- {
- $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
- ->orderBy('created_at', 'desc');
- if (strtolower($type) === 'page') {
- $query = $query->where('draft', '=', false);
- }
- if ($additionalQuery !== false && is_callable($additionalQuery)) {
- $additionalQuery($query);
- }
- return $query->skip($page * $count)->take($count)->get();
- }
-
- /**
- * Get the most recently updated entities of the given type.
- * @param string $type
- * @param int $count
- * @param int $page
- * @param bool|callable $additionalQuery
- * @return Collection
- */
- public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
- {
- $query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
- ->orderBy('updated_at', 'desc');
- if (strtolower($type) === 'page') {
- $query = $query->where('draft', '=', false);
- }
- if ($additionalQuery !== false && is_callable($additionalQuery)) {
- $additionalQuery($query);
- }
- return $query->skip($page * $count)->take($count)->get();
- }
-
- /**
- * Get the most recently viewed entities.
- * @param string|bool $type
- * @param int $count
- * @param int $page
- * @return mixed
- */
- public function getRecentlyViewed($type, $count = 10, $page = 0)
- {
- $filter = is_bool($type) ? false : $this->entityProvider->get($type);
- return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
- }
-
- /**
- * Get the latest pages added to the system with pagination.
- * @param string $type
- * @param int $count
- * @return mixed
- */
- public function getRecentlyCreatedPaginated($type, $count = 20)
- {
- return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
- }
-
- /**
- * Get the latest pages added to the system with pagination.
- * @param string $type
- * @param int $count
- * @return mixed
- */
- public function getRecentlyUpdatedPaginated($type, $count = 20)
- {
- return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
- }
-
- /**
- * Get the most popular entities base on all views.
- * @param string $type
- * @param int $count
- * @param int $page
- * @return mixed
- */
- public function getPopular(string $type, int $count = 10, int $page = 0)
- {
- return $this->viewService->getPopular($count, $page, $type);
- }
-
- /**
- * Get draft pages owned by the current user.
- * @param int $count
- * @param int $page
- * @return Collection
- */
- public function getUserDraftPages($count = 20, $page = 0)
- {
- return $this->entityProvider->page->where('draft', '=', true)
- ->where('created_by', '=', user()->id)
- ->orderBy('updated_at', 'desc')
- ->skip($count * $page)->take($count)->get();
- }
-
- /**
- * Get the number of entities the given user has created.
- * @param string $type
- * @param User $user
- * @return int
- */
- public function getUserTotalCreated(string $type, User $user)
- {
- return $this->entityProvider->get($type)
- ->where('created_by', '=', $user->id)->count();
- }
-
- /**
- * Get the child items for a chapter sorted by priority but
- * with draft items floated to the top.
- * @param Bookshelf $bookshelf
- * @return \Illuminate\Database\Eloquent\Collection|static[]
- */
- public function getBookshelfChildren(Bookshelf $bookshelf)
- {
- return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
- }
-
- /**
- * Get the direct children of a book.
- * @param Book $book
- * @return \Illuminate\Database\Eloquent\Collection
- */
- public function getBookDirectChildren(Book $book)
- {
- $pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
- $chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
- return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
- }
-
- /**
- * Get all child objects of a book.
- * Returns a sorted collection of Pages and Chapters.
- * Loads the book slug onto child elements to prevent access database access for getting the slug.
- * @param Book $book
- * @param bool $filterDrafts
- * @param bool $renderPages
- * @return mixed
- */
- public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
- {
- $q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
- $entities = [];
- $parents = [];
- $tree = [];
-
- foreach ($q as $index => $rawEntity) {
- if ($rawEntity->entity_type === $this->entityProvider->page->getMorphClass()) {
- $entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
- if ($renderPages) {
- $entities[$index]->html = $rawEntity->html;
- $entities[$index]->html = $this->renderPage($entities[$index]);
- };
- } else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
- $entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
- $key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
- $parents[$key] = $entities[$index];
- $parents[$key]->setAttribute('pages', collect());
- }
- if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
- $tree[] = $entities[$index];
- }
- $entities[$index]->book = $book;
- }
-
- foreach ($entities as $entity) {
- if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
- continue;
- }
- $parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
- if (!isset($parents[$parentKey])) {
- $tree[] = $entity;
- continue;
- }
- $chapter = $parents[$parentKey];
- $chapter->pages->push($entity);
- }
-
- return collect($tree);
- }
-
- /**
- * Get the child items for a chapter sorted by priority but
- * with draft items floated to the top.
- * @param Chapter $chapter
- * @return \Illuminate\Database\Eloquent\Collection|static[]
- */
- public function getChapterChildren(Chapter $chapter)
- {
- return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
- ->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
- }
-
-
- /**
- * Get the next sequential priority for a new child element in the given book.
- * @param Book $book
- * @return int
- */
- public function getNewBookPriority(Book $book)
- {
- $lastElem = $this->getBookChildren($book)->pop();
- return $lastElem ? $lastElem->priority + 1 : 0;
- }
-
- /**
- * Get a new priority for a new page to be added to the given chapter.
- * @param Chapter $chapter
- * @return int
- */
- public function getNewChapterPriority(Chapter $chapter)
- {
- $lastPage = $chapter->pages('DESC')->first();
- return $lastPage !== null ? $lastPage->priority + 1 : 0;
- }
-
- /**
- * Find a suitable slug for an entity.
- * @param string $type
- * @param string $name
- * @param bool|integer $currentId
- * @param bool|integer $bookId Only pass if type is not a book
- * @return string
- */
- public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
- {
- $slug = $this->nameToSlug($name);
- while ($this->slugExists($type, $slug, $currentId, $bookId)) {
- $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
- }
- return $slug;
- }
-
- /**
- * Check if a slug already exists in the database.
- * @param string $type
- * @param string $slug
- * @param bool|integer $currentId
- * @param bool|integer $bookId
- * @return bool
- */
- protected function slugExists($type, $slug, $currentId = false, $bookId = false)
- {
- $query = $this->entityProvider->get($type)->where('slug', '=', $slug);
- if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
- $query = $query->where('book_id', '=', $bookId);
- }
- if ($currentId) {
- $query = $query->where('id', '!=', $currentId);
- }
- return $query->count() > 0;
- }
-
- /**
- * Updates entity restrictions from a request
- * @param Request $request
- * @param Entity $entity
- * @throws Throwable
- */
- public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
- {
- $entity->restricted = $request->get('restricted', '') === 'true';
- $entity->permissions()->delete();
-
- if ($request->filled('restrictions')) {
- foreach ($request->get('restrictions') as $roleId => $restrictions) {
- foreach ($restrictions as $action => $value) {
- $entity->permissions()->create([
- 'role_id' => $roleId,
- 'action' => strtolower($action)
- ]);
- }
- }
- }
-
- $entity->save();
- $this->permissionService->buildJointPermissionsForEntity($entity);
- }
-
-
-
- /**
- * Create a new entity from request input.
- * Used for books and chapters.
- * @param string $type
- * @param array $input
- * @param bool|Book $book
- * @return Entity
- */
- public function createFromInput($type, $input = [], $book = false)
- {
- $isChapter = strtolower($type) === 'chapter';
- $entityModel = $this->entityProvider->get($type)->newInstance($input);
- $entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
- $entityModel->created_by = user()->id;
- $entityModel->updated_by = user()->id;
- $isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $this->searchService->indexEntity($entityModel);
- return $entityModel;
- }
-
- /**
- * Update entity details from request input.
- * Used for books and chapters
- * @param string $type
- * @param Entity $entityModel
- * @param array $input
- * @return Entity
- */
- public function updateFromInput($type, Entity $entityModel, $input = [])
- {
- if ($entityModel->name !== $input['name']) {
- $entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
- }
- $entityModel->fill($input);
- $entityModel->updated_by = user()->id;
- $entityModel->save();
-
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
- }
-
- $this->permissionService->buildJointPermissionsForEntity($entityModel);
- $this->searchService->indexEntity($entityModel);
- return $entityModel;
- }
-
- /**
- * Sync the books assigned to a shelf from a comma-separated list
- * of book IDs.
- * @param Bookshelf $shelf
- * @param string $books
- */
- public function updateShelfBooks(Bookshelf $shelf, string $books)
- {
- $ids = explode(',', $books);
-
- // Check books exist and match ordering
- $bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
- $syncData = [];
- foreach ($ids as $index => $id) {
- if ($bookIds->contains($id)) {
- $syncData[$id] = ['order' => $index];
- }
- }
-
- $shelf->books()->sync($syncData);
- }
-
- /**
- * Append a Book to a BookShelf.
- * @param Bookshelf $shelf
- * @param Book $book
- */
- public function appendBookToShelf(Bookshelf $shelf, Book $book)
- {
- if ($shelf->contains($book)) {
- return;
- }
-
- $maxOrder = $shelf->books()->max('order');
- $shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
- }
-
- /**
- * Change the book that an entity belongs to.
- * @param string $type
- * @param integer $newBookId
- * @param Entity $entity
- * @param bool $rebuildPermissions
- * @return Entity
- */
- public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
- {
- $entity->book_id = $newBookId;
- // Update related activity
- foreach ($entity->activity as $activity) {
- $activity->book_id = $newBookId;
- $activity->save();
- }
- $entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
- $entity->save();
-
- // Update all child pages if a chapter
- if (strtolower($type) === 'chapter') {
- foreach ($entity->pages as $page) {
- $this->changeBook('page', $newBookId, $page, false);
- }
- }
-
- // Update permissions if applicable
- if ($rebuildPermissions) {
- $entity->load('book');
- $this->permissionService->buildJointPermissionsForEntity($entity->book);
- }
-
- return $entity;
- }
-
- /**
- * Alias method to update the book jointPermissions in the PermissionService.
- * @param Book $book
- */
- public function buildJointPermissionsForBook(Book $book)
- {
- $this->permissionService->buildJointPermissionsForEntity($book);
- }
-
- /**
- * Format a name as a url slug.
- * @param $name
- * @return string
- */
- protected function nameToSlug($name)
- {
- $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
- $slug = preg_replace('/\s{2,}/', ' ', $slug);
- $slug = str_replace(' ', '-', $slug);
- if ($slug === "") {
- $slug = substr(md5(rand(1, 500)), 0, 5);
- }
- return $slug;
- }
-
- /**
- * Render the page for viewing
- * @param Page $page
- * @param bool $blankIncludes
- * @return string
- */
- public function renderPage(Page $page, bool $blankIncludes = false) : string
- {
- $content = $page->html;
-
- if (!config('app.allow_content_scripts')) {
- $content = $this->escapeScripts($content);
- }
-
- if ($blankIncludes) {
- $content = $this->blankPageIncludes($content);
- } else {
- $content = $this->parsePageIncludes($content);
- }
-
- return $content;
- }
-
- /**
- * Remove any page include tags within the given HTML.
- * @param string $html
- * @return string
- */
- protected function blankPageIncludes(string $html) : string
- {
- return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
- }
-
- /**
- * Parse any include tags "{{@<page_id>#section}}" to be part of the page.
- * @param string $html
- * @return mixed|string
- */
- protected function parsePageIncludes(string $html) : string
- {
- $matches = [];
- preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
-
- $topLevelTags = ['table', 'ul', 'ol'];
- foreach ($matches[1] as $index => $includeId) {
- $splitInclude = explode('#', $includeId, 2);
- $pageId = intval($splitInclude[0]);
- if (is_nan($pageId)) {
- continue;
- }
-
- $matchedPage = $this->getById('page', $pageId);
- if ($matchedPage === null) {
- $html = str_replace($matches[0][$index], '', $html);
- continue;
- }
-
- if (count($splitInclude) === 1) {
- $html = str_replace($matches[0][$index], $matchedPage->html, $html);
- continue;
- }
-
- $doc = new DOMDocument();
- libxml_use_internal_errors(true);
- $doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
- $matchingElem = $doc->getElementById($splitInclude[1]);
- if ($matchingElem === null) {
- $html = str_replace($matches[0][$index], '', $html);
- continue;
- }
- $innerContent = '';
- $isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
- if ($isTopLevel) {
- $innerContent .= $doc->saveHTML($matchingElem);
- } else {
- foreach ($matchingElem->childNodes as $childNode) {
- $innerContent .= $doc->saveHTML($childNode);
- }
- }
- libxml_clear_errors();
- $html = str_replace($matches[0][$index], trim($innerContent), $html);
- }
-
- return $html;
- }
-
- /**
- * Escape script tags within HTML content.
- * @param string $html
- * @return string
- */
- protected function escapeScripts(string $html) : string
- {
- if ($html == '') {
- return $html;
- }
-
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
- $xPath = new DOMXPath($doc);
-
- // Remove standard script tags
- $scriptElems = $xPath->query('//script');
- foreach ($scriptElems as $scriptElem) {
- $scriptElem->parentNode->removeChild($scriptElem);
- }
-
- // Remove data or JavaScript iFrames
- $badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')]');
- foreach ($badIframes as $badIframe) {
- $badIframe->parentNode->removeChild($badIframe);
- }
-
- // Remove 'on*' attributes
- $onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
- foreach ($onAttributes as $attr) {
- /** @var \DOMAttr $attr*/
- $attrName = $attr->nodeName;
- $attr->parentNode->removeAttribute($attrName);
- }
-
- $html = '';
- $topElems = $doc->documentElement->childNodes->item(0)->childNodes;
- foreach ($topElems as $child) {
- $html .= $doc->saveHTML($child);
- }
-
- return $html;
- }
-
- /**
- * Search for image usage within page content.
- * @param $imageString
- * @return mixed
- */
- public function searchForImage($imageString)
- {
- $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
- foreach ($pages as $page) {
- $page->url = $page->getUrl();
- $page->html = '';
- $page->text = '';
- }
- return count($pages) > 0 ? $pages : false;
- }
-
- /**
- * Destroy a bookshelf instance
- * @param Bookshelf $shelf
- * @throws Throwable
- */
- public function destroyBookshelf(Bookshelf $shelf)
- {
- $this->destroyEntityCommonRelations($shelf);
- $shelf->delete();
- }
-
- /**
- * Destroy the provided book and all its child entities.
- * @param Book $book
- * @throws NotifyException
- * @throws Throwable
- */
- public function destroyBook(Book $book)
- {
- foreach ($book->pages as $page) {
- $this->destroyPage($page);
- }
- foreach ($book->chapters as $chapter) {
- $this->destroyChapter($chapter);
- }
- $this->destroyEntityCommonRelations($book);
- $book->delete();
- }
-
- /**
- * Destroy a chapter and its relations.
- * @param Chapter $chapter
- * @throws Throwable
- */
- public function destroyChapter(Chapter $chapter)
- {
- if (count($chapter->pages) > 0) {
- foreach ($chapter->pages as $page) {
- $page->chapter_id = 0;
- $page->save();
- }
- }
- $this->destroyEntityCommonRelations($chapter);
- $chapter->delete();
- }
-
- /**
- * Destroy a given page along with its dependencies.
- * @param Page $page
- * @throws NotifyException
- * @throws Throwable
- */
- 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->destroyEntityCommonRelations($page);
-
- // Delete Attached Files
- $attachmentService = app(AttachmentService::class);
- foreach ($page->attachments as $attachment) {
- $attachmentService->deleteFile($attachment);
- }
-
- $page->delete();
- }
-
- /**
- * Destroy or handle the common relations connected to an entity.
- * @param Entity $entity
- * @throws Throwable
- */
- protected function destroyEntityCommonRelations(Entity $entity)
- {
- Activity::removeEntity($entity);
- $entity->views()->delete();
- $entity->permissions()->delete();
- $entity->tags()->delete();
- $entity->comments()->delete();
- $this->permissionService->deleteJointPermissionsForEntity($entity);
- $this->searchService->deleteEntityTerms($entity);
- }
-
- /**
- * Copy the permissions of a bookshelf to all child books.
- * Returns the number of books that had permissions updated.
- * @param Bookshelf $bookshelf
- * @return int
- * @throws Throwable
- */
- public function copyBookshelfPermissions(Bookshelf $bookshelf)
- {
- $shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
- $shelfBooks = $bookshelf->books()->get();
- $updatedBookCount = 0;
-
- foreach ($shelfBooks as $book) {
- if (!userCan('restrictions-manage', $book)) {
- continue;
- }
- $book->permissions()->delete();
- $book->restricted = $bookshelf->restricted;
- $book->permissions()->createMany($shelfPermissions);
- $book->save();
- $this->permissionService->buildJointPermissionsForEntity($book);
- $updatedBookCount++;
- }
-
- return $updatedBookCount;
- }
-}
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 Carbon\Carbon;
-use DOMDocument;
-use DOMElement;
-use DOMXPath;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
-class PageRepo extends EntityRepo
+class PageRepo
{
+ protected $baseRepo;
+
/**
- * Get page by slug.
- * @param string $pageSlug
- * @param string $bookSlug
- * @return Page
- * @throws \BookStack\Exceptions\NotFoundException
+ * PageRepo constructor.
*/
- public function getPageBySlug(string $pageSlug, string $bookSlug)
+ public function __construct(BaseRepo $baseRepo)
{
- return $this->getBySlug('page', $pageSlug, $bookSlug);
+ $this->baseRepo = $baseRepo;
}
/**
- * Search through page revisions and retrieve the last page in the
- * current book that has a slug equal to the one given.
- * @param string $pageSlug
- * @param string $bookSlug
- * @return null|Page
+ * Get a page by ID.
+ * @throws NotFoundException
*/
- public function getPageByOldSlug(string $pageSlug, string $bookSlug)
+ public function getById(int $id): Page
{
- $revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
- ->whereHas('page', function ($query) {
- $this->permissionService->enforceEntityRestrictions('page', $query);
+ $page = Page::visible()->with(['book'])->find($id);
+
+ if (!$page) {
+ throw new NotFoundException(trans('errors.page_not_found'));
+ }
+
+ return $page;
+ }
+
+ /**
+ * Get a page its book and own slug.
+ * @throws NotFoundException
+ */
+ public function getBySlug(string $bookSlug, string $pageSlug): Page
+ {
+ $page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->first();
+
+ if (!$page) {
+ throw new NotFoundException(trans('errors.page_not_found'));
+ }
+
+ return $page;
+ }
+
+ /**
+ * Get a page by its old slug but checking the revisions table
+ * for the last revision that matched the given page and book slug.
+ */
+ public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
+ {
+ $revision = PageRevision::query()
+ ->whereHas('page', function (Builder $query) {
+ $query->visible();
})
+ ->where('slug', '=', $pageSlug)
->where('type', '=', 'version')
->where('book_slug', '=', $bookSlug)
->orderBy('created_at', 'desc')
- ->with('page')->first();
- return $revision !== null ? $revision->page : null;
+ ->with('page')
+ ->first();
+ return $revision ? $revision->page : null;
}
/**
- * Updates a page with any fillable data and saves it into the database.
- * @param Page $page
- * @param int $book_id
- * @param array $input
- * @return Page
- * @throws \Exception
+ * Get pages that have been marked as a template.
*/
- public function updatePage(Page $page, int $book_id, array $input)
+ public function getTemplates(int $count = 10, int $page = 1, string $search = ''): LengthAwarePaginator
{
- // Hold the old details to compare later
- $oldHtml = $page->html;
- $oldName = $page->name;
+ $query = Page::visible()
+ ->where('template', '=', true)
+ ->orderBy('name', 'asc')
+ ->skip(($page - 1) * $count)
+ ->take($count);
+
+ if ($search) {
+ $query->where('name', 'like', '%' . $search . '%');
+ }
- // Prevent slug being updated if no name change
- if ($page->name !== $input['name']) {
- $page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
+ $paginator = $query->paginate($count, ['*'], 'page', $page);
+ $paginator->withPath('/templates');
+
+ return $paginator;
+ }
+
+ /**
+ * Get a parent item via slugs.
+ */
+ public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
+ {
+ if ($chapterSlug !== null) {
+ return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
}
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($page, $input['tags']);
+ return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
+ }
+
+ /**
+ * Get the draft copy of the given page for the current user.
+ */
+ public function getUserDraft(Page $page): ?PageRevision
+ {
+ $revision = $this->getUserDraftQuery($page)->first();
+ return $revision;
+ }
+
+ /**
+ * Get a new draft page belonging to the given parent entity.
+ */
+ public function getNewDraftPage(Entity $parent)
+ {
+ $page = (new Page())->forceFill([
+ 'name' => trans('entities.pages_initial_name'),
+ 'created_by' => user()->id,
+ 'updated_by' => user()->id,
+ 'draft' => true,
+ ]);
+
+ if ($parent instanceof Chapter) {
+ $page->chapter_id = $parent->id;
+ $page->book_id = $parent->book_id;
+ } else {
+ $page->book_id = $parent->id;
}
+ $page->save();
+ $page->refresh()->rebuildPermissions();
+ return $page;
+ }
+
+ /**
+ * Publish a draft page to make it a live, non-draft page.
+ */
+ 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');
+ }
+
+ $pageContent = new PageContent($draft);
+ $pageContent->setNewHTML($input['html']);
+ $draft->draft = false;
+ $draft->revision_count = 1;
+ $draft->priority = $this->getNewPriority($draft);
+ $draft->refreshSlug();
+ $draft->save();
+
+ $this->savePageRevision($draft, trans('entities.pages_initial_revision'));
+ $draft->indexForSearch();
+ return $draft->refresh();
+ }
+
+ /**
+ * Update a page in the system.
+ */
+ public function update(Page $page, array $input): Page
+ {
+ // Hold the old details to compare later
+ $oldHtml = $page->html;
+ $oldName = $page->name;
+
if (isset($input['template']) && userCan('templates-manage')) {
$page->template = ($input['template'] === 'true');
}
+ $this->baseRepo->update($page, $input);
+
// Update with new details
- $userId = user()->id;
$page->fill($input);
- $page->html = $this->formatHtml($input['html']);
- $page->text = $this->pageToPlainText($page);
+ $pageContent = new PageContent($page);
+ $pageContent->setNewHTML($input['html']);
+ $page->revision_count++;
+
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
- $page->updated_by = $userId;
- $page->revision_count++;
+
$page->save();
// Remove all update drafts for this user & page.
- $this->userUpdatePageDraftsQuery($page, $userId)->delete();
+ $this->getUserDraftQuery($page)->delete();
// Save a revision after updating
$summary = $input['summary'] ?? null;
$this->savePageRevision($page, $summary);
}
- $this->searchService->indexEntity($page);
-
return $page;
}
/**
* Saves a page revision into the system.
- * @param Page $page
- * @param null|string $summary
- * @return PageRevision
- * @throws \Exception
*/
- public function savePageRevision(Page $page, string $summary = null)
+ protected function savePageRevision(Page $page, string $summary = null)
{
- $revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
+ $revision = new PageRevision($page->toArray());
+
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
+
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
$revision->revision_number = $page->revision_count;
$revision->save();
- $revisionLimit = config('app.revision_limit');
- if ($revisionLimit !== false) {
- $revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
- ->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
- if ($revisionsToDelete->count() > 0) {
- $this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
- }
- }
-
+ $this->deleteOldRevisions($page);
return $revision;
}
/**
- * Formats a page's html to be tagged correctly within the system.
- * @param string $htmlText
- * @return string
+ * Save a page update draft.
*/
- protected function formatHtml(string $htmlText)
+ public function updatePageDraft(Page $page, array $input)
{
- if ($htmlText == '') {
- return $htmlText;
- }
-
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
-
- $container = $doc->documentElement;
- $body = $container->childNodes->item(0);
- $childNodes = $body->childNodes;
-
- // Set ids on top-level nodes
- $idMap = [];
- foreach ($childNodes as $index => $childNode) {
- $this->setUniqueId($childNode, $idMap);
- }
-
- // Ensure no duplicate ids within child items
- $xPath = new DOMXPath($doc);
- $idElems = $xPath->query('//body//*//*[@id]');
- foreach ($idElems as $domElem) {
- $this->setUniqueId($domElem, $idMap);
+ // 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']);
+ }
+ $page->save();
+ return $page;
}
- // Generate inner html as a string
- $html = '';
- foreach ($childNodes as $childNode) {
- $html .= $doc->saveHTML($childNode);
+ // Otherwise save the data to a revision
+ $draft = $this->getPageRevisionToUpdate($page);
+ $draft->fill($input);
+ if (setting('app-editor') !== 'markdown') {
+ $draft->markdown = '';
}
- return $html;
+ $draft->save();
+ return $draft;
}
/**
- * Set a unique id on the given DOMElement.
- * A map for existing ID's should be passed in to check for current existence.
- * @param DOMElement $element
- * @param array $idMap
+ * Destroy a page from the system.
+ * @throws NotifyException
*/
- protected function setUniqueId($element, array &$idMap)
+ public function destroy(Page $page)
{
- if (get_class($element) !== 'DOMElement') {
- return;
- }
-
- // Overwrite id if not a BookStack custom id
- $existingId = $element->getAttribute('id');
- if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
- $idMap[$existingId] = true;
- return;
- }
-
- // Create an unique id for the element
- // Uses the content as a basis to ensure output is the same every time
- // the same content is passed through.
- $contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
- $newId = urlencode($contentId);
- $loopIndex = 0;
-
- while (isset($idMap[$newId])) {
- $newId = urlencode($contentId . '-' . $loopIndex);
- $loopIndex++;
- }
-
- $element->setAttribute('id', $newId);
- $idMap[$newId] = true;
+ $trashCan = new TrashCan();
+ $trashCan->destroyPage($page);
}
/**
- * Get the plain text version of a page's content.
- * @param \BookStack\Entities\Page $page
- * @return string
+ * Restores a revision's content back into a page.
*/
- protected function pageToPlainText(Page $page) : string
+ public function restoreRevision(Page $page, int $revisionId): Page
{
- $html = $this->renderPage($page, true);
- return strip_tags($html);
- }
+ $page->revision_count++;
+ $this->savePageRevision($page);
- /**
- * Get a new draft page instance.
- * @param Book $book
- * @param Chapter|null $chapter
- * @return \BookStack\Entities\Page
- * @throws \Throwable
- */
- public function getDraftPage(Book $book, Chapter $chapter = null)
- {
- $page = $this->entityProvider->page->newInstance();
- $page->name = trans('entities.pages_initial_name');
- $page->created_by = user()->id;
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ $page->fill($revision->toArray());
+ $content = new PageContent($page);
+ $content->setNewHTML($page->html);
$page->updated_by = user()->id;
- $page->draft = true;
-
- if ($chapter) {
- $page->chapter_id = $chapter->id;
- }
+ $page->refreshSlug();
+ $page->save();
- $book->pages()->save($page);
- $page = $this->entityProvider->page->find($page->id);
- $this->permissionService->buildJointPermissionsForEntity($page);
+ $page->indexForSearch();
return $page;
}
/**
- * Save a page update draft.
- * @param Page $page
- * @param array $data
- * @return PageRevision|Page
+ * Move the given page into a new parent book or chapter.
+ * The $parentIdentifier must be a string of the following format:
+ * 'book:<id>' (book:5)
+ * @throws MoveOperationException
+ * @throws PermissionsException
*/
- public function updatePageDraft(Page $page, array $data = [])
+ public function move(Page $page, string $parentIdentifier): Book
{
- // If the page itself is a draft simply update that
- if ($page->draft) {
- $page->fill($data);
- if (isset($data['html'])) {
- $page->text = $this->pageToPlainText($page);
- }
- $page->save();
- return $page;
+ $parent = $this->findParentByIdentifier($parentIdentifier);
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
}
- // Otherwise save the data to a revision
- $userId = user()->id;
- $drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
-
- if ($drafts->count() > 0) {
- $draft = $drafts->first();
- } else {
- $draft = $this->entityProvider->pageRevision->newInstance();
- $draft->page_id = $page->id;
- $draft->slug = $page->slug;
- $draft->book_slug = $page->book->slug;
- $draft->created_by = $userId;
- $draft->type = 'update_draft';
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
}
- $draft->fill($data);
- if (setting('app-editor') !== 'markdown') {
- $draft->markdown = '';
- }
-
- $draft->save();
- return $draft;
+ $page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
+ $page->rebuildPermissions();
+ return $parent;
}
/**
- * Publish a draft page to make it a normal page.
- * Sets the slug and updates the content.
- * @param Page $draftPage
- * @param array $input
- * @return Page
- * @throws \Exception
+ * Copy an existing page in the system.
+ * Optionally providing a new parent via string identifier and a new name.
+ * @throws MoveOperationException
+ * @throws PermissionsException
*/
- public function publishPageDraft(Page $draftPage, array $input)
+ public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
{
- $draftPage->fill($input);
-
- // Save page tags if present
- if (isset($input['tags'])) {
- $this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
+ $parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
+ if ($parent === null) {
+ throw new MoveOperationException('Book or chapter to move page into not found');
}
- if (isset($input['template']) && userCan('templates-manage')) {
- $draftPage->template = ($input['template'] === 'true');
+ if (!userCan('page-create', $parent)) {
+ throw new PermissionsException('User does not have permission to create a page within the new parent');
}
- $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
- $draftPage->html = $this->formatHtml($input['html']);
- $draftPage->text = $this->pageToPlainText($draftPage);
- $draftPage->draft = false;
- $draftPage->revision_count = 1;
-
- $draftPage->save();
- $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
- $this->searchService->indexEntity($draftPage);
- return $draftPage;
- }
-
- /**
- * The base query for getting user update drafts.
- * @param Page $page
- * @param $userId
- * @return mixed
- */
- protected function userUpdatePageDraftsQuery(Page $page, int $userId)
- {
- return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
- ->where('type', 'update_draft')
- ->where('page_id', '=', $page->id)
- ->orderBy('created_at', 'desc');
- }
+ $copyPage = $this->getNewDraftPage($parent);
+ $pageData = $page->getAttributes();
- /**
- * Get the latest updated draft revision for a particular page and user.
- * @param Page $page
- * @param $userId
- * @return PageRevision|null
- */
- public function getUserPageDraft(Page $page, int $userId)
- {
- return $this->userUpdatePageDraftsQuery($page, $userId)->first();
- }
+ // Update name
+ if (!empty($newName)) {
+ $pageData['name'] = $newName;
+ }
- /**
- * Get the notification message that informs the user that they are editing a draft page.
- * @param PageRevision $draft
- * @return string
- */
- public function getUserPageDraftMessage(PageRevision $draft)
- {
- $message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
- if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
- return $message;
+ // Copy tags from previous page if set
+ if ($page->tags) {
+ $pageData['tags'] = [];
+ foreach ($page->tags as $tag) {
+ $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+ }
}
- return $message . "\n" . trans('entities.pages_draft_edited_notification');
+
+ return $this->publishDraft($copyPage, $pageData);
}
/**
- * A query to check for active update drafts on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return mixed
+ * Find a page parent entity via a identifier string in the format:
+ * {type}:{id}
+ * Example: (book:5)
+ * @throws MoveOperationException
*/
- protected function activePageEditingQuery(Page $page, int $minRange = null)
+ protected function findParentByIdentifier(string $identifier): ?Entity
{
- $query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
- ->where('page_id', '=', $page->id)
- ->where('updated_at', '>', $page->updated_at)
- ->where('created_by', '!=', user()->id)
- ->with('createdBy');
+ $stringExploded = explode(':', $identifier);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
- if ($minRange !== null) {
- $query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
+ if ($entityType !== 'book' && $entityType !== 'chapter') {
+ throw new MoveOperationException('Pages can only be in books or chapters');
}
- return $query;
+ $parentClass = $entityType === 'book' ? Book::class : Chapter::class;
+ return $parentClass::visible()->where('id', '=', $entityId)->first();
}
/**
- * Check if a page is being actively editing.
- * Checks for edits since last page updated.
- * Passing in a minuted range will check for edits
- * within the last x minutes.
- * @param Page $page
- * @param int $minRange
- * @return bool
+ * Update the permissions of a page.
*/
- public function isPageEditingActive(Page $page, int $minRange = null)
+ public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
{
- $draftSearch = $this->activePageEditingQuery($page, $minRange);
- return $draftSearch->count() > 0;
+ $this->baseRepo->updatePermissions($page, $restricted, $permissions);
}
/**
- * Get a notification message concerning the editing activity on a particular page.
- * @param Page $page
- * @param int $minRange
- * @return string
+ * Change the page's parent to the given entity.
*/
- public function getPageEditingActiveMessage(Page $page, int $minRange = null)
+ protected function changeParent(Page $page, Entity $parent)
{
- $pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
+ $book = ($parent instanceof Book) ? $parent : $parent->book;
+ $page->chapter_id = ($parent instanceof Chapter) ? $parent->id : 0;
+ $page->save();
- $userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
- $timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
- return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
+ if ($page->book->id !== $book->id) {
+ $page->changeBook($book->id);
+ }
+
+ $page->load('book');
+ $book->rebuildPermissions();
}
/**
- * Parse the headers on the page to get a navigation menu
- * @param string $pageContent
- * @return array
+ * Get a page revision to update for the given page.
+ * Checks for an existing revisions before providing a fresh one.
*/
- public function getPageNav(string $pageContent)
+ protected function getPageRevisionToUpdate(Page $page): PageRevision
{
- if ($pageContent == '') {
- return [];
- }
- libxml_use_internal_errors(true);
- $doc = new DOMDocument();
- $doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
- $xPath = new DOMXPath($doc);
- $headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
-
- if (is_null($headers)) {
- return [];
+ $drafts = $this->getUserDraftQuery($page)->get();
+ if ($drafts->count() > 0) {
+ return $drafts->first();
}
- $tree = collect($headers)->map(function($header) {
- $text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
- $text = mb_substr($text, 0, 100);
-
- return [
- 'nodeName' => strtolower($header->nodeName),
- 'level' => intval(str_replace('h', '', $header->nodeName)),
- 'link' => '#' . $header->getAttribute('id'),
- 'text' => $text,
- ];
- })->filter(function($header) {
- return mb_strlen($header['text']) > 0;
- });
-
- // Shift headers if only smaller headers have been used
- $levelChange = ($tree->pluck('level')->min() - 1);
- $tree = $tree->map(function ($header) use ($levelChange) {
- $header['level'] -= ($levelChange);
- return $header;
- });
-
- return $tree->toArray();
+ $draft = new PageRevision();
+ $draft->page_id = $page->id;
+ $draft->slug = $page->slug;
+ $draft->book_slug = $page->book->slug;
+ $draft->created_by = user()->id;
+ $draft->type = 'update_draft';
+ return $draft;
}
/**
- * Restores a revision's content back into a page.
- * @param Page $page
- * @param Book $book
- * @param int $revisionId
- * @return Page
- * @throws \Exception
+ * Delete old revisions, for the given page, from the system.
*/
- public function restorePageRevision(Page $page, Book $book, int $revisionId)
+ protected function deleteOldRevisions(Page $page)
{
- $page->revision_count++;
- $this->savePageRevision($page);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- $page->fill($revision->toArray());
- $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
- $page->text = $this->pageToPlainText($page);
- $page->updated_by = user()->id;
- $page->save();
- $this->searchService->indexEntity($page);
- return $page;
- }
+ $revisionLimit = config('app.revision_limit');
+ if ($revisionLimit === false) {
+ return;
+ }
- /**
- * Change the page's parent to the given entity.
- * @param Page $page
- * @param Entity $parent
- * @throws \Throwable
- */
- public function changePageParent(Page $page, Entity $parent)
- {
- $book = $parent->isA('book') ? $parent : $parent->book;
- $page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
- $page->save();
- if ($page->book->id !== $book->id) {
- $page = $this->changeBook('page', $book->id, $page);
+ $revisionsToDelete = PageRevision::query()
+ ->where('page_id', '=', $page->id)
+ ->orderBy('created_at', 'desc')
+ ->skip(intval($revisionLimit))
+ ->take(10)
+ ->get(['id']);
+ if ($revisionsToDelete->count() > 0) {
+ PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
}
- $page->load('book');
- $this->permissionService->buildJointPermissionsForEntity($book);
}
/**
- * Create a copy of a page in a new location with a new name.
- * @param \BookStack\Entities\Page $page
- * @param \BookStack\Entities\Entity $newParent
- * @param string $newName
- * @return \BookStack\Entities\Page
- * @throws \Throwable
+ * Get a new priority for a page
*/
- public function copyPage(Page $page, Entity $newParent, string $newName = '')
+ protected function getNewPriority(Page $page): int
{
- $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
- $newChapter = $newParent->isA('chapter') ? $newParent : null;
- $copyPage = $this->getDraftPage($newBook, $newChapter);
- $pageData = $page->getAttributes();
-
- // Update name
- if (!empty($newName)) {
- $pageData['name'] = $newName;
- }
-
- // Copy tags from previous page if set
- if ($page->tags) {
- $pageData['tags'] = [];
- foreach ($page->tags as $tag) {
- $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
- }
+ if ($page->parent() instanceof Chapter) {
+ $lastPage = $page->parent()->pages('desc')->first();
+ return $lastPage ? $lastPage->priority + 1 : 0;
}
- // Set priority
- if ($newParent->isA('chapter')) {
- $pageData['priority'] = $this->getNewChapterPriority($newParent);
- } else {
- $pageData['priority'] = $this->getNewBookPriority($newParent);
- }
-
- return $this->publishPageDraft($copyPage, $pageData);
+ return (new BookContents($page->book))->getLastPriority() + 1;
}
/**
- * Get pages that have been marked as templates.
- * @param int $count
- * @param int $page
- * @param string $search
- * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
+ * Get the query to find the user's draft copies of the given page.
*/
- public function getPageTemplates(int $count = 10, int $page = 1, string $search = '')
+ protected function getUserDraftQuery(Page $page)
{
- $query = $this->entityQuery('page')
- ->where('template', '=', true)
- ->orderBy('name', 'asc')
- ->skip( ($page - 1) * $count)
- ->take($count);
-
- if ($search) {
- $query->where('name', 'like', '%' . $search . '%');
- }
-
- $paginator = $query->paginate($count, ['*'], 'page', $page);
- $paginator->withPath('/templates');
-
- return $paginator;
+ return PageRevision::query()->where('created_by', '=', user()->id)
+ ->where('type', 'update_draft')
+ ->where('page_id', '=', $page->id)
+ ->orderBy('created_at', 'desc');
}
}
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
+use Illuminate\Support\Str;
class SearchService
{
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
- $functionName = camel_case('filter_' . $filterTerm);
+ $functionName = Str::camel('filter_' . $filterTerm);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
}
protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
{
- $functionName = camel_case('sort_by_' . $input);
+ $functionName = Str::camel('sort_by_' . $input);
if (method_exists($this, $functionName)) {
$this->$functionName($query, $model);
}
--- /dev/null
+<?php namespace BookStack\Entities;
+
+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
+ {
+ $slug = $this->formatNameAsSlug($this->entity->name);
+ while ($this->slugInUse($slug)) {
+ $slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
+ }
+ return $slug;
+ }
+
+ /**
+ * Format a name as a url slug.
+ */
+ protected function formatNameAsSlug(string $name): string
+ {
+ $slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
+ $slug = preg_replace('/\s{2,}/', ' ', $slug);
+ $slug = str_replace(' ', '-', $slug);
+ if ($slug === "") {
+ $slug = substr(md5(rand(1, 500)), 0, 5);
+ }
+ 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
+ {
+ $query = $this->entity->newQuery()->where('slug', '=', $slug);
+
+ if ($this->entity instanceof BookChild) {
+ $query->where('book_id', '=', $this->entity->book_id);
+ }
+
+ if ($this->entity->id) {
+ $query->where('id', '!=', $this->entity->id);
+ }
+
+ return $query->count() > 0;
+ }
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class MoveOperationException extends Exception
+{
+
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+use Exception;
+
+class SortOperationException extends Exception
+{
+
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+class UserTokenExpiredException extends \Exception
+{
+
+ public $userId;
+
+ /**
+ * UserTokenExpiredException constructor.
+ * @param string $message
+ * @param int $userId
+ */
+ public function __construct(string $message, int $userId)
+ {
+ $this->userId = $userId;
+ parent::__construct($message);
+ }
+}
--- /dev/null
+<?php namespace BookStack\Exceptions;
+
+class UserTokenNotFoundException extends \Exception
+{
+
+}
--- /dev/null
+<?php namespace BookStack\Facades;
+
+use Illuminate\Support\Facades\Facade;
+
+class Permissions extends Facade
+{
+ /**
+ * Get the registered name of the component.
+ *
+ * @return string
+ */
+ protected static function getFacadeAccessor()
+ {
+ return 'permissions';
+ }
+}
<?php namespace BookStack\Http\Controllers;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\FileUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Uploads\Attachment;
use BookStack\Uploads\AttachmentService;
+use Exception;
+use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $attachment;
- protected $entityRepo;
+ protected $pageRepo;
/**
* AttachmentController constructor.
- * @param \BookStack\Uploads\AttachmentService $attachmentService
- * @param Attachment $attachment
- * @param EntityRepo $entityRepo
*/
- public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
+ public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
{
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
- $this->entityRepo = $entityRepo;
+ $this->pageRepo = $pageRepo;
parent::__construct();
}
/**
* Endpoint at which attachments are uploaded to.
- * @param Request $request
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+ * @throws ValidationException
+ * @throws NotFoundException
*/
public function upload(Request $request)
{
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
/**
* Update an uploaded attachment.
- * @param int $attachmentId
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function uploadUpdate($attachmentId, Request $request)
+ public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
/**
* Update the details of an existing file.
- * @param $attachmentId
- * @param Request $request
- * @return Attachment|mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function update($attachmentId, Request $request)
+ public function update(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
/**
* Attach a link to a page.
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
public function attachLink(Request $request)
{
]);
$pageId = $request->get('uploaded_to');
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
/**
* Get the attachments for a specific page.
- * @param $pageId
- * @return mixed
*/
- public function listForPage($pageId)
+ public function listForPage(int $pageId)
{
- $page = $this->entityRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
}
/**
* Update the attachment sorting.
- * @param $pageId
- * @param Request $request
- * @return mixed
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function sortForPage($pageId, Request $request)
+ public function sortForPage(Request $request, int $pageId)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
]);
- $page = $this->entityRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
/**
* Get an attachment from storage.
- * @param $attachmentId
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
- * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ * @throws FileNotFoundException
* @throws NotFoundException
*/
- public function get($attachmentId)
+ public function get(int $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
- $page = $this->entityRepo->getById('page', $attachment->uploaded_to);
- if ($page === null) {
+ try {
+ $page = $this->pageRepo->getById($attachment->uploaded_to);
+ } catch (NotFoundException $exception) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
- * @throws \Exception
+ * @throws Exception
*/
- public function delete($attachmentId)
+ public function delete(int $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Auth\Access\EmailConfirmationService;
+use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\ConfirmationEmailException;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Exception;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Redirector;
+use Illuminate\View\View;
+
+class ConfirmEmailController extends Controller
+{
+ protected $emailConfirmationService;
+ protected $userRepo;
+
+ /**
+ * 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();
+ }
+
+
+ /**
+ * Show the page to tell the user to check their email
+ * and confirm their address.
+ */
+ public function show()
+ {
+ return view('auth.register-confirm');
+ }
+
+ /**
+ * Shows a notice that a user's email address has not been confirmed,
+ * Also has the option to re-send the confirmation email.
+ * @return View
+ */
+ public function showAwaiting()
+ {
+ return view('auth.user-unconfirmed');
+ }
+
+ /**
+ * Confirms an email via a token and logs the user into the system.
+ * @param $token
+ * @return RedirectResponse|Redirector
+ * @throws ConfirmationEmailException
+ * @throws Exception
+ */
+ public function confirm($token)
+ {
+ try {
+ $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ if ($exception instanceof UserTokenNotFoundException) {
+ $this->showErrorNotification(trans('errors.email_confirmation_invalid'));
+ return redirect('/register');
+ }
+
+ if ($exception instanceof UserTokenExpiredException) {
+ $user = $this->userRepo->getById($exception->userId);
+ $this->emailConfirmationService->sendConfirmation($user);
+ $this->showErrorNotification(trans('errors.email_confirmation_expired'));
+ return redirect('/register/confirm');
+ }
+
+ throw $exception;
+ }
+
+ $user = $this->userRepo->getById($userId);
+ $user->email_confirmed = true;
+ $user->save();
+
+ auth()->login($user);
+ $this->showSuccessNotification(trans('auth.email_confirm_success'));
+ $this->emailConfirmationService->deleteByUser($user);
+
+ return redirect('/');
+ }
+
+
+ /**
+ * Resend the confirmation email
+ * @param Request $request
+ * @return View
+ */
+ public function resend(Request $request)
+ {
+ $this->validate($request, [
+ 'email' => 'required|email|exists:users,email'
+ ]);
+ $user = $this->userRepo->getByEmail($request->get('email'));
+
+ try {
+ $this->emailConfirmationService->sendConfirmation($user);
+ } catch (Exception $e) {
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
+ return redirect('/register/confirm');
+ }
+
+ $this->showSuccessNotification(trans('auth.email_confirm_resent'));
+ return redirect('/register/confirm');
+ }
+}
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return back()->with('status', trans($response));
}
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Redirector;
-use Illuminate\View\View;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialUser;
use Validator;
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
- * @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService
+ * @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
return Validator::make($data, [
'name' => 'required|min:2|max:255',
'email' => 'required|email|max:255|unique:users',
- 'password' => 'required|min:6',
+ 'password' => 'required|min:8',
]);
}
return User::create([
'name' => $data['name'],
'email' => $data['email'],
- 'password' => bcrypt($data['password']),
+ 'password' => Hash::make($data['password']),
]);
}
$newUser->socialAccounts()->save($socialAccount);
}
- if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
+ if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
$newUser->save();
try {
$this->emailConfirmationService->sendConfirmation($newUser);
} catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
+ $this->showErrorNotification(trans('auth.email_confirm_send_error'));
}
return redirect('/register/confirm');
}
auth()->login($newUser);
- session()->flash('success', trans('auth.register_success'));
+ $this->showSuccessNotification(trans('auth.register_success'));
return redirect($this->redirectPath());
}
- /**
- * Show the page to tell the user to check their email
- * and confirm their address.
- */
- public function getRegisterConfirmation()
- {
- return view('auth.register-confirm');
- }
-
- /**
- * Confirms an email via a token and logs the user into the system.
- * @param $token
- * @return RedirectResponse|Redirector
- * @throws UserRegistrationException
- */
- public function confirmEmail($token)
- {
- $confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
- $user = $confirmation->user;
- $user->email_confirmed = true;
- $user->save();
- auth()->login($user);
- session()->flash('success', trans('auth.email_confirm_success'));
- $this->emailConfirmationService->deleteConfirmationsByUser($user);
- return redirect($this->redirectPath);
- }
-
- /**
- * Shows a notice that a user's email address has not been confirmed,
- * Also has the option to re-send the confirmation email.
- * @return View
- */
- public function showAwaitingConfirmation()
- {
- return view('auth.user-unconfirmed');
- }
-
- /**
- * Resend the confirmation email
- * @param Request $request
- * @return View
- */
- public function resendConfirmation(Request $request)
- {
- $this->validate($request, [
- 'email' => 'required|email|exists:users,email'
- ]);
- $user = $this->userRepo->getByEmail($request->get('email'));
-
- try {
- $this->emailConfirmationService->sendConfirmation($user);
- } catch (Exception $e) {
- session()->flash('error', trans('auth.email_confirm_send_error'));
- return redirect('/register/confirm');
- }
-
- session()->flash('success', trans('auth.email_confirm_resent'));
- return redirect('/register/confirm');
- }
-
/**
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
/**
* The callback for social login services.
- * @param $socialDriver
* @param Request $request
+ * @param string $socialDriver
* @return RedirectResponse|Redirector
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws SocialDriverNotConfigured
*/
- public function socialCallback($socialDriver, Request $request)
+ public function socialCallback(Request $request, string $socialDriver)
{
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
- 'password' => str_random(30)
+ 'password' => Str::random(30)
];
return $this->registerUser($userData, $socialAccount, $emailVerified);
}
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
/**
* Get the response for a successful password reset.
*
- * @param string $response
+ * @param Request $request
+ * @param string $response
* @return \Illuminate\Http\Response
*/
- protected function sendResetResponse($response)
+ protected function sendResetResponse(Request $request, $response)
{
$message = trans('auth.reset_password_success');
- session()->flash('success', $message);
+ $this->showSuccessNotification($message);
return redirect($this->redirectPath())
->with('status', trans($response));
}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers\Auth;
+
+use BookStack\Auth\Access\UserInviteService;
+use BookStack\Auth\UserRepo;
+use BookStack\Exceptions\UserTokenExpiredException;
+use BookStack\Exceptions\UserTokenNotFoundException;
+use BookStack\Http\Controllers\Controller;
+use Exception;
+use Illuminate\Contracts\View\Factory;
+use Illuminate\Http\RedirectResponse;
+use Illuminate\Http\Request;
+use Illuminate\Routing\Redirector;
+use Illuminate\View\View;
+
+class UserInviteController extends Controller
+{
+ protected $inviteService;
+ protected $userRepo;
+
+ /**
+ * Create a new controller instance.
+ *
+ * @param UserInviteService $inviteService
+ * @param UserRepo $userRepo
+ */
+ public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
+ {
+ $this->inviteService = $inviteService;
+ $this->userRepo = $userRepo;
+ $this->middleware('guest');
+ parent::__construct();
+ }
+
+ /**
+ * Show the page for the user to set the password for their account.
+ * @param string $token
+ * @return Factory|View|RedirectResponse
+ * @throws Exception
+ */
+ public function showSetPassword(string $token)
+ {
+ try {
+ $this->inviteService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ return $this->handleTokenException($exception);
+ }
+
+ return view('auth.invite-set-password', [
+ 'token' => $token,
+ ]);
+ }
+
+ /**
+ * Sets the password for an invited user and then grants them access.
+ * @param Request $request
+ * @param string $token
+ * @return RedirectResponse|Redirector
+ * @throws Exception
+ */
+ public function setPassword(Request $request, string $token)
+ {
+ $this->validate($request, [
+ 'password' => 'required|min:8'
+ ]);
+
+ try {
+ $userId = $this->inviteService->checkTokenAndGetUserId($token);
+ } catch (Exception $exception) {
+ return $this->handleTokenException($exception);
+ }
+
+ $user = $this->userRepo->getById($userId);
+ $user->password = bcrypt($request->get('password'));
+ $user->email_confirmed = true;
+ $user->save();
+
+ auth()->login($user);
+ $this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
+ $this->inviteService->deleteByUser($user);
+
+ return redirect('/');
+ }
+
+ /**
+ * Check and validate the exception thrown when checking an invite token.
+ * @param Exception $exception
+ * @return RedirectResponse|Redirector
+ * @throws Exception
+ */
+ protected function handleTokenException(Exception $exception)
+ {
+ if ($exception instanceof UserTokenNotFoundException) {
+ return redirect('/');
+ }
+
+ if ($exception instanceof UserTokenExpiredException) {
+ $this->showErrorNotification(trans('errors.invite_token_expired'));
+ return redirect('/password/email');
+ }
+
+ throw $exception;
+ }
+}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Book;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
-use BookStack\Uploads\ImageRepo;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotifyException;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
use Views;
class BookController extends Controller
{
- protected $entityRepo;
- protected $userRepo;
- protected $exportService;
+ protected $bookRepo;
protected $entityContextManager;
- protected $imageRepo;
/**
* BookController constructor.
- * @param EntityRepo $entityRepo
- * @param UserRepo $userRepo
- * @param ExportService $exportService
- * @param EntityContextManager $entityContextManager
- * @param ImageRepo $imageRepo
*/
- public function __construct(
- EntityRepo $entityRepo,
- UserRepo $userRepo,
- ExportService $exportService,
- EntityContextManager $entityContextManager,
- ImageRepo $imageRepo
- ) {
- $this->entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
- $this->exportService = $exportService;
+ public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
+ {
+ $this->bookRepo = $bookRepo;
$this->entityContextManager = $entityContextManager;
- $this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Display a listing of the book.
- * @return Response
*/
public function index()
{
- $view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
- $sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
- $sortOptions = [
- 'name' => trans('common.sort_name'),
- 'created_at' => trans('common.sort_created_at'),
- 'updated_at' => trans('common.sort_updated_at'),
- ];
-
- $books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
- $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
- $popular = $this->entityRepo->getPopular('book', 4, 0);
- $new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
+ $view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
+ $sort = setting()->getForCurrentUser('books_sort', 'name');
+ $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+
+ $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+ $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
+ $popular = $this->bookRepo->getPopular(4);
+ $new = $this->bookRepo->getRecentlyCreated(4);
$this->entityContextManager->clearShelfContext();
'view' => $view,
'sort' => $sort,
'order' => $order,
- 'sortOptions' => $sortOptions,
]);
}
/**
* Show the form for creating a new book.
- * @param string $shelfSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function create(string $shelfSlug = null)
{
+ $this->checkPermission('book-create-all');
+
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+ $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
- $this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books.create', [
'bookshelf' => $bookshelf
/**
* Store a newly created book in storage.
- *
- * @param Request $request
- * @param string $shelfSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ImageUploadException
+ * @throws ValidationException
*/
public function store(Request $request, string $shelfSlug = null)
{
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
$bookshelf = null;
if ($shelfSlug !== null) {
- $bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
+ $bookshelf = Bookshelf::visible()->where('slug', '=', $shelfSlug)->firstOrFail();
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
- $book = $this->entityRepo->createFromInput('book', $request->all());
- $this->bookUpdateActions($book, $request);
+ $book = $this->bookRepo->create($request->all());
+ $this->bookRepo->updateCoverImage($book, $request->file('image', null));
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
- $this->entityRepo->appendBookToShelf($bookshelf, $book);
+ $bookshelf->appendBook($book);
Activity::add($bookshelf, 'bookshelf_update');
}
/**
* Display the specified book.
- * @param $slug
- * @param Request $request
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
- public function show($slug, Request $request)
+ public function show(Request $request, string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
- $this->checkOwnablePermission('book-view', $book);
-
- $bookChildren = $this->entityRepo->getBookChildren($book);
+ $book = $this->bookRepo->getBySlug($slug);
+ $bookChildren = (new BookContents($book))->getTree(true);
Views::add($book);
if ($request->has('shelf')) {
/**
* Show the form for editing the specified book.
- * @param $slug
- * @return Response
*/
- public function edit($slug)
+ public function edit(string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
+ $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books.edit', ['book' => $book, 'current' => $book]);
/**
* Update the specified book in storage.
- * @param Request $request
- * @param $slug
- * @return Response
- * @throws \BookStack\Exceptions\ImageUploadException
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws ImageUploadException
+ * @throws ValidationException
+ * @throws Throwable
*/
public function update(Request $request, string $slug)
{
- $book = $this->entityRepo->getBySlug('book', $slug);
+ $book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
- $book = $this->entityRepo->updateFromInput('book', $book, $request->all());
- $this->bookUpdateActions($book, $request);
+ $book = $this->bookRepo->update($book, $request->all());
+ $resetCover = $request->has('image_reset');
+ $this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
- Activity::add($book, 'book_update', $book->id);
+ Activity::add($book, 'book_update', $book->id);
- return redirect($book->getUrl());
+ return redirect($book->getUrl());
}
/**
- * Shows the page to confirm deletion
- * @param $bookSlug
- * @return \Illuminate\View\View
+ * Shows the page to confirm deletion.
*/
- public function showDelete($bookSlug)
+ public function showDelete(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- $this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
+ $this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
- * Shows the view which allows pages to be re-ordered and sorted.
- * @param string $bookSlug
- * @return \Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
- */
- public function sort($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $this->checkOwnablePermission('book-update', $book);
-
- $bookChildren = $this->entityRepo->getBookChildren($book, true);
-
- $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
- return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
- }
-
- /**
- * Shows the sort box for a single book.
- * Used via AJAX when loading in extra books to a sort.
- * @param $bookSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Remove the specified book from the system.
+ * @throws Throwable
+ * @throws NotifyException
*/
- public function getSortItem($bookSlug)
+ public function destroy(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $bookChildren = $this->entityRepo->getBookChildren($book);
- return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
- }
-
- /**
- * Saves an array of sort mapping to pages and chapters.
- * @param string $bookSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function saveSort($bookSlug, Request $request)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $this->checkOwnablePermission('book-update', $book);
-
- // Return if no map sent
- if (!$request->filled('sort-tree')) {
- return redirect($book->getUrl());
- }
-
- // Sort pages and chapters
- $sortMap = collect(json_decode($request->get('sort-tree')));
- $bookIdsInvolved = collect([$book->id]);
-
- // Load models into map
- $sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
- $mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
- $mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
- // Store source and target books
- $bookIdsInvolved->push(intval($mapItem->model->book_id));
- $bookIdsInvolved->push(intval($mapItem->book));
- });
-
- // Get the books involved in the sort
- $bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
- $booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
- // Throw permission error if invalid ids or inaccessible books given.
- if (count($bookIdsInvolved) !== count($booksInvolved)) {
- $this->showPermissionError();
- }
- // Check permissions of involved books
- $booksInvolved->each(function (Book $book) {
- $this->checkOwnablePermission('book-update', $book);
- });
-
- // Perform the sort
- $sortMap->each(function ($mapItem) {
- $model = $mapItem->model;
-
- $priorityChanged = intval($model->priority) !== intval($mapItem->sort);
- $bookChanged = intval($model->book_id) !== intval($mapItem->book);
- $chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
-
- if ($bookChanged) {
- $this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
- }
- if ($chapterChanged) {
- $model->chapter_id = intval($mapItem->parentChapter);
- $model->save();
- }
- if ($priorityChanged) {
- $model->priority = intval($mapItem->sort);
- $model->save();
- }
- });
-
- // Rebuild permissions and add activity for involved books.
- $booksInvolved->each(function (Book $book) {
- $this->entityRepo->buildJointPermissionsForBook($book);
- Activity::add($book, 'book_sort', $book->id);
- });
-
- return redirect($book->getUrl());
- }
-
- /**
- * Remove the specified book from storage.
- * @param $bookSlug
- * @return Response
- */
- public function destroy($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
- Activity::addMessage('book_delete', 0, $book->name);
- if ($book->cover) {
- $this->imageRepo->destroyImage($book->cover);
- }
- $this->entityRepo->destroyBook($book);
+ Activity::addMessage('book_delete', $book->name);
+ $this->bookRepo->destroy($book);
return redirect('/books');
}
/**
- * Show the Restrictions view.
- * @param $bookSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Show the permissions view.
*/
- public function showPermissions($bookSlug)
+ public function showPermissions(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $roles = $this->userRepo->getRestrictableRoles();
+
return view('books.permissions', [
'book' => $book,
- 'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
- * @param $bookSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws Throwable
*/
- public function permissions($bookSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
- session()->flash('success', trans('entities.books_permissions_updated'));
- return redirect($book->getUrl());
- }
-
- /**
- * Export a book as a PDF file.
- * @param string $bookSlug
- * @return mixed
- */
- public function exportPdf($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $pdfContent = $this->exportService->bookToPdf($book);
- return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
- }
-
- /**
- * Export a book as a contained HTML file.
- * @param string $bookSlug
- * @return mixed
- */
- public function exportHtml($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $htmlContent = $this->exportService->bookToContainedHtml($book);
- return $this->downloadResponse($htmlContent, $bookSlug . '.html');
- }
-
- /**
- * Export a book as a plain text file.
- * @param $bookSlug
- * @return mixed
- */
- public function exportPlainText($bookSlug)
- {
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
- $textContent = $this->exportService->bookToPlainText($book);
- return $this->downloadResponse($textContent, $bookSlug . '.txt');
- }
- /**
- * Common actions to run on book update.
- * Handles updating the cover image.
- * @param Book $book
- * @param Request $request
- * @throws \BookStack\Exceptions\ImageUploadException
- */
- protected function bookUpdateActions(Book $book, Request $request)
- {
- // Update the cover image if in request
- if ($request->has('image')) {
- $this->imageRepo->destroyImage($book->cover);
- $newImage = $request->file('image');
- $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
- $book->image_id = $image->id;
- $book->save();
- }
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->bookRepo->updatePermissions($book, $restricted, $permissions);
- if ($request->has('image_reset')) {
- $this->imageRepo->destroyImage($book->cover);
- $book->image_id = 0;
- $book->save();
- }
+ $this->showSuccessNotification(trans('entities.books_permissions_updated'));
+ return redirect($book->getUrl());
}
}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\BookRepo;
+use Throwable;
+
+class BookExportController extends Controller
+{
+
+ protected $bookRepo;
+ protected $exportService;
+
+ /**
+ * BookExportController constructor.
+ */
+ public function __construct(BookRepo $bookRepo, ExportService $exportService)
+ {
+ $this->bookRepo = $bookRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Export a book as a PDF file.
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $pdfContent = $this->exportService->bookToPdf($book);
+ return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
+ }
+
+ /**
+ * Export a book as a contained HTML file.
+ * @throws Throwable
+ */
+ public function html(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $htmlContent = $this->exportService->bookToContainedHtml($book);
+ return $this->downloadResponse($htmlContent, $bookSlug . '.html');
+ }
+
+ /**
+ * Export a book as a plain text file.
+ */
+ public function plainText(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $textContent = $this->exportService->bookToPlainText($book);
+ return $this->downloadResponse($textContent, $bookSlug . '.txt');
+ }
+}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Exceptions\SortOperationException;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+
+class BookSortController extends Controller
+{
+
+ protected $bookRepo;
+
+ /**
+ * BookSortController constructor.
+ * @param $bookRepo
+ */
+ public function __construct(BookRepo $bookRepo)
+ {
+ $this->bookRepo = $bookRepo;
+ parent::__construct();
+ }
+
+ /**
+ * Shows the view which allows pages to be re-ordered and sorted.
+ */
+ public function show(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('book-update', $book);
+
+ $bookChildren = (new BookContents($book))->getTree(false);
+
+ $this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
+ return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
+ }
+
+ /**
+ * Shows the sort box for a single book.
+ * Used via AJAX when loading in extra books to a sort.
+ */
+ public function showItem(string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $bookChildren = (new BookContents($book))->getTree();
+ return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
+ }
+
+ /**
+ * Sorts a book using a given mapping array.
+ */
+ public function update(Request $request, string $bookSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $this->checkOwnablePermission('book-update', $book);
+
+ // Return if no map sent
+ if (!$request->filled('sort-tree')) {
+ return redirect($book->getUrl());
+ }
+
+ $sortMap = collect(json_decode($request->get('sort-tree')));
+ $bookContents = new BookContents($book);
+ $booksInvolved = collect();
+
+ try {
+ $booksInvolved = $bookContents->sortUsingMap($sortMap);
+ } catch (SortOperationException $exception) {
+ $this->showPermissionError();
+ }
+
+ // Rebuild permissions and add activity for involved books.
+ $booksInvolved->each(function (Book $book) {
+ Activity::add($book, 'book_sort', $book->id);
+ });
+
+ return redirect($book->getUrl());
+ }
+}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Bookshelf;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\EntityContext;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Exceptions\ImageUploadException;
+use BookStack\Exceptions\NotFoundException;
use BookStack\Uploads\ImageRepo;
+use Exception;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
use Views;
class BookshelfController extends Controller
{
- protected $entityRepo;
- protected $userRepo;
+ protected $bookshelfRepo;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
- * @param EntityRepo $entityRepo
- * @param UserRepo $userRepo
- * @param EntityContextManager $entityContextManager
- * @param ImageRepo $imageRepo
*/
- public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
+ public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
{
- $this->entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
+ $this->bookshelfRepo = $bookshelfRepo;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
/**
* Display a listing of the book.
- * @return Response
*/
public function index()
{
- $view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
- $sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
- $order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
+ $view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
+ $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
+ $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
- $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
- foreach ($shelves as $shelf) {
- $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
- }
-
- $recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
- $popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
- $new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
+ $shelves = $this->bookshelfRepo->getAllPaginated(18, $sort, $order);
+ $recents = $this->isSignedIn() ? $this->bookshelfRepo->getRecentlyViewed(4) : false;
+ $popular = $this->bookshelfRepo->getPopular(4);
+ $new = $this->bookshelfRepo->getRecentlyCreated(4);
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
/**
* Show the form for creating a new bookshelf.
- * @return Response
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
- $books = $this->entityRepo->getAll('book', false, 'update');
+ $books = Book::hasPermission('update')->get();
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
- * @param Request $request
- * @return Response
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ValidationException
+ * @throws ImageUploadException
*/
public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
- 'image' => $this->imageRepo->getImageValidationRules(),
+ 'image' => $this->getImageValidationRules(),
]);
- $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
- $this->shelfUpdateActions($shelf, $request);
+ $bookIds = explode(',', $request->get('books', ''));
+ $shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
+ $this->bookshelfRepo->updateCoverImage($shelf);
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
-
/**
- * Display the specified bookshelf.
- * @param String $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
+ * Display the bookshelf of the given slug.
+ * @throws NotFoundException
*/
public function show(string $slug)
{
- /** @var Bookshelf $shelf */
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('book-view', $shelf);
- $books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$this->setPageTitle($shelf->getShortName());
-
return view('shelves.show', [
'shelf' => $shelf,
- 'books' => $books,
'activity' => Activity::entityActivity($shelf, 20, 1)
]);
}
/**
* Show the form for editing the specified bookshelf.
- * @param $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
- $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
- $shelfBookIds = $shelfBooks->pluck('id');
- $books = $this->entityRepo->getAll('book', false, 'update');
- $books = $books->filter(function ($book) use ($shelfBookIds) {
- return !$shelfBookIds->contains($book->id);
- });
+ $shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
+ $books = Book::hasPermission('update')->whereNotIn('id', $shelfBookIds)->get();
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
- 'shelfBooks' => $shelfBooks,
]);
}
-
/**
* Update the specified bookshelf in storage.
- * @param Request $request
- * @param string $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \BookStack\Exceptions\ImageUploadException
+ * @throws ValidationException
+ * @throws ImageUploadException
+ * @throws NotFoundException
*/
public function update(Request $request, string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
'image' => $this->imageRepo->getImageValidationRules(),
]);
- $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
- $this->shelfUpdateActions($shelf, $request);
- Activity::add($shelf, 'bookshelf_update');
+ $bookIds = explode(',', $request->get('books', ''));
+ $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());
+ return redirect($shelf->getUrl());
}
-
/**
* Shows the page to confirm deletion
- * @param $slug
- * @return \Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
/**
* Remove the specified bookshelf from storage.
- * @param string $slug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws Exception
*/
public function destroy(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
- Activity::addMessage('bookshelf_delete', 0, $shelf->name);
- if ($shelf->cover) {
- $this->imageRepo->destroyImage($shelf->cover);
- }
- $this->entityRepo->destroyBookshelf($shelf);
+ Activity::addMessage('bookshelf_delete', $shelf->name);
+ $this->bookshelfRepo->destroy($shelf);
return redirect('/shelves');
}
/**
* Show the permissions view.
- * @param string $slug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function showPermissions(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $roles = $this->userRepo->getRestrictableRoles();
return view('shelves.permissions', [
'shelf' => $shelf,
- 'roles' => $roles
]);
}
/**
* Set the permissions for this bookshelf.
- * @param string $slug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
*/
- public function permissions(string $slug, Request $request)
+ public function permissions(Request $request, string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
- session()->flash('success', trans('entities.shelves_permissions_updated'));
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+
+ $this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
- * @param string $slug
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
- $shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
+ $shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
- session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
+ $updateCount = $this->bookshelfRepo->copyDownPermissions($shelf);
+ $this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($shelf->getUrl());
}
-
- /**
- * Common actions to run on bookshelf update.
- * @param Bookshelf $shelf
- * @param Request $request
- * @throws \BookStack\Exceptions\ImageUploadException
- */
- protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
- {
- // Update the books that the shelf references
- $this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
-
- // Update the cover image if in request
- if ($request->has('image')) {
- $newImage = $request->file('image');
- $this->imageRepo->destroyImage($shelf->cover);
- $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
- $shelf->image_id = $image->id;
- $shelf->save();
- }
-
- if ($request->has('image_reset')) {
- $this->imageRepo->destroyImage($shelf->cover);
- $shelf->image_id = 0;
- $shelf->save();
- }
- }
}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\MoveOperationException;
+use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
use Views;
class ChapterController extends Controller
{
- protected $userRepo;
- protected $entityRepo;
- protected $exportService;
+ protected $chapterRepo;
/**
* ChapterController constructor.
- * @param EntityRepo $entityRepo
- * @param UserRepo $userRepo
- * @param \BookStack\Entities\ExportService $exportService
*/
- public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
+ public function __construct(ChapterRepo $chapterRepo)
{
- $this->entityRepo = $entityRepo;
- $this->userRepo = $userRepo;
- $this->exportService = $exportService;
+ $this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Show the form for creating a new chapter.
- * @param $bookSlug
- * @return Response
*/
- public function create($bookSlug)
+ public function create(string $bookSlug)
{
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
+
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
* Store a newly created chapter in storage.
- * @param $bookSlug
- * @param Request $request
- * @return Response
+ * @throws ValidationException
*/
- public function store($bookSlug, Request $request)
+ public function store(Request $request, string $bookSlug)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- $book = $this->entityRepo->getBySlug('book', $bookSlug);
+ $book = Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
$this->checkOwnablePermission('chapter-create', $book);
- $input = $request->all();
- $input['priority'] = $this->entityRepo->getNewBookPriority($book);
- $chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
+ $chapter = $this->chapterRepo->create($request->all(), $book);
Activity::add($chapter, 'chapter_create', $book->id);
+
return redirect($chapter->getUrl());
}
/**
* Display the specified chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
*/
- public function show($bookSlug, $chapterSlug)
+ public function show(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
- $sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
+
+ $sidebarTree = (new BookContents($chapter->book))->getTree();
+ $pages = $chapter->getVisiblePages();
Views::add($chapter);
+
$this->setPageTitle($chapter->getShortName());
- $pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
/**
* Show the form for editing the specified chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
*/
- public function edit($bookSlug, $chapterSlug)
+ public function edit(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
+
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Update the specified chapter in storage.
- * @param Request $request
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function update(Request $request, $bookSlug, $chapterSlug)
+ public function update(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
- $this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
+ $this->chapterRepo->update($chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
+
return redirect($chapter->getUrl());
}
/**
* Shows the page to confirm deletion of this chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return \Illuminate\View\View
+ * @throws NotFoundException
*/
- public function showDelete($bookSlug, $chapterSlug)
+ public function showDelete(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
+
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
* Remove the specified chapter from storage.
- * @param $bookSlug
- * @param $chapterSlug
- * @return Response
+ * @throws NotFoundException
+ * @throws Throwable
*/
- public function destroy($bookSlug, $chapterSlug)
+ public function destroy(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
- Activity::addMessage('chapter_delete', $book->id, $chapter->name);
- $this->entityRepo->destroyChapter($chapter);
- return redirect($book->getUrl());
+
+ Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
+ $this->chapterRepo->destroy($chapter);
+
+ return redirect($chapter->book->getUrl());
}
/**
* Show the page for moving a chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @return mixed
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function showMove($bookSlug, $chapterSlug)
+ public function showMove(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
+
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book
/**
* Perform the move action for a chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @param Request $request
- * @return mixed
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function move($bookSlug, $chapterSlug, Request $request)
+ public function move(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
return redirect($chapter->getUrl());
}
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
- $parent = false;
-
- if ($entityType == 'book') {
- $parent = $this->entityRepo->getById('book', $entityId);
- }
-
- if ($parent === false || $parent === null) {
- session()->flash('error', trans('errors.selected_book_not_found'));
+ try {
+ $newBook = $this->chapterRepo->move($chapter, $entitySelection);
+ } catch (MoveOperationException $exception) {
+ $this->showErrorNotification(trans('errors.selected_book_not_found'));
return redirect()->back();
}
- $this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
- Activity::add($chapter, 'chapter_move', $chapter->book->id);
- session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
+ Activity::add($chapter, 'chapter_move', $newBook->id);
+ $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
return redirect($chapter->getUrl());
}
/**
* Show the Restrictions view.
- * @param $bookSlug
- * @param $chapterSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
- * @throws \BookStack\Exceptions\NotFoundException
+ * @throws NotFoundException
*/
- public function showPermissions($bookSlug, $chapterSlug)
+ public function showPermissions(string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $roles = $this->userRepo->getRestrictableRoles();
+
return view('chapters.permissions', [
'chapter' => $chapter,
- 'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
- * @param $bookSlug
- * @param $chapterSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- * @throws \BookStack\Exceptions\NotFoundException
- * @throws \Throwable
+ * @throws NotFoundException
*/
- public function permissions($bookSlug, $chapterSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug, string $chapterSlug)
{
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
- session()->flash('success', trans('entities.chapters_permissions_success'));
- return redirect($chapter->getUrl());
- }
- /**
- * Exports a chapter to pdf .
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPdf($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $pdfContent = $this->exportService->chapterToPdf($chapter);
- return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
- }
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
- /**
- * Export a chapter to a self-contained HTML file.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportHtml($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
- return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
- }
-
- /**
- * Export a chapter to a simple plaintext .txt file.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPlainText($bookSlug, $chapterSlug)
- {
- $chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $chapterText = $this->exportService->chapterToPlainText($chapter);
- return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+ $this->showSuccessNotification(trans('entities.chapters_permissions_success'));
+ return redirect($chapter->getUrl());
}
}
--- /dev/null
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class ChapterExportController extends Controller
+{
+
+ protected $chapterRepo;
+ protected $exportService;
+
+ /**
+ * ChapterExportController constructor.
+ */
+ public function __construct(ChapterRepo $chapterRepo, ExportService $exportService)
+ {
+ $this->chapterRepo = $chapterRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Exports a chapter to pdf.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $pdfContent = $this->exportService->chapterToPdf($chapter);
+ return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
+ }
+
+ /**
+ * Export a chapter to a self-contained HTML file.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function html(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $containedHtml = $this->exportService->chapterToContainedHtml($chapter);
+ return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
+ }
+
+ /**
+ * Export a chapter to a simple plaintext .txt file.
+ * @throws NotFoundException
+ */
+ public function plainText(string $bookSlug, string $chapterSlug)
+ {
+ $chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
+ $chapterText = $this->exportService->chapterToPlainText($chapter);
+ return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
+ }
+}
use Activity;
use BookStack\Actions\CommentRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use Illuminate\Database\Eloquent\ModelNotFoundException;
+use BookStack\Entities\Page;
use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
class CommentController extends Controller
{
- protected $entityRepo;
protected $commentRepo;
/**
* CommentController constructor.
- * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
- * @param \BookStack\Actions\CommentRepo $commentRepo
*/
- public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
+ public function __construct(CommentRepo $commentRepo)
{
- $this->entityRepo = $entityRepo;
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Save a new comment for a Page
- * @param Request $request
- * @param integer $pageId
- * @param null|integer $commentId
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
+ * @throws ValidationException
*/
- public function savePageComment(Request $request, $pageId, $commentId = null)
+ public function savePageComment(Request $request, int $pageId, int $commentId = null)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
- try {
- $page = $this->entityRepo->getById('page', $pageId, true);
- } catch (ModelNotFoundException $e) {
+ $page = Page::visible()->find($pageId);
+ if ($page === null) {
return response('Not found', 404);
}
/**
* Update an existing comment.
- * @param Request $request
- * @param integer $commentId
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws ValidationException
*/
- public function update(Request $request, $commentId)
+ public function update(Request $request, int $commentId)
{
$this->validate($request, [
'text' => 'required|string',
/**
* Delete a comment from the system.
- * @param integer $id
- * @return \Illuminate\Http\JsonResponse
*/
- public function destroy($id)
+ public function destroy(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('comment-delete', $comment);
+
$this->commentRepo->delete($comment);
return response()->json(['message' => trans('entities.comment_deleted')]);
}
namespace BookStack\Http\Controllers;
-use BookStack\Auth\User;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
{
use DispatchesJobs, ValidatesRequests;
- /**
- * @var User static
- */
- protected $currentUser;
- /**
- * @var bool
- */
- protected $signedIn;
-
/**
* Controller constructor.
*/
public function __construct()
{
- $this->middleware(function ($request, $next) {
-
- // Get a user instance for the current user
- $user = user();
-
- // Share variables with controllers
- $this->currentUser = $user;
- $this->signedIn = auth()->check();
-
- // Share variables with views
- view()->share('signedIn', $this->signedIn);
- view()->share('currentUser', $user);
+ //
+ }
- return $next($request);
- });
+ /**
+ * Check if the current user is signed in.
+ */
+ protected function isSignedIn(): bool
+ {
+ return auth()->check();
}
/**
* Stops the application and shows a permission error if
* the application is in demo mode.
*/
- protected function preventAccessForDemoUsers()
+ protected function preventAccessInDemoMode()
{
if (config('app.env') === 'demo') {
$this->showPermissionError();
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
} else {
$response = redirect('/');
- session()->flash('error', trans('errors.permission'));
+ $this->showErrorNotification(trans('errors.permission'));
}
throw new HttpResponseException($response);
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function () use ($userId) {
- return $userId === $this->currentUser->id;
+ return $userId === user()->id;
});
}
*/
protected function jsonError($messageText = "", $statusCode = 500)
{
- return response()->json(['message' => $messageText], $statusCode);
+ return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
+
+ /**
+ * Show a positive, successful notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showSuccessNotification(string $message)
+ {
+ session()->flash('success', $message);
+ }
+
+ /**
+ * Show a warning notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showWarningNotification(string $message)
+ {
+ session()->flash('warning', $message);
+ }
+
+ /**
+ * Show an error notification to the user on next view load.
+ * @param string $message
+ */
+ protected function showErrorNotification(string $message)
+ {
+ session()->flash('error', $message);
+ }
+
+ /**
+ * Get the validation rules for image files.
+ */
+ protected function getImageValidationRules(): string
+ {
+ return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
+ }
}
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Page;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
{
- protected $entityRepo;
-
- /**
- * HomeController constructor.
- * @param EntityRepo $entityRepo
- */
- public function __construct(EntityRepo $entityRepo)
- {
- $this->entityRepo = $entityRepo;
- parent::__construct();
- }
/**
* Display the homepage.
public function index()
{
$activity = Activity::latest(10);
- $draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
+ $draftPages = [];
+
+ if ($this->isSignedIn()) {
+ $draftPages = Page::visible()->where('draft', '=', true)
+ ->where('created_by', '=', user()->id)
+ ->orderBy('updated_at', 'desc')->take(6)->get();
+ }
+
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
- $recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
- $recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
+ $recents = $this->isSignedIn() ?
+ Views::getUserRecentlyViewed(12*$recentFactor, 0)
+ : Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
+ $recentlyUpdatedPages = Page::visible()->where('draft', false)
+ ->orderBy('updated_at', 'desc')->take(12)->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$homepageOption = setting('app-homepage-type', 'default');
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
- $view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
- $sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
- $order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
+ $view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
+ $sort = setting()->getForCurrentUser($key . '_sort', 'name');
+ $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
}
if ($homepageOption === 'bookshelves') {
- $shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
+ $shelfRepo = app(BookshelfRepo::class);
+ $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
foreach ($shelves as $shelf) {
- $shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
+ $shelf->books = $shelf->visibleBooks;
}
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
- $books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
+ $bookRepo = app(BookRepo::class);
+ $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
return view('common.home-book', $data);
}
if ($homepageOption === 'page') {
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
- $customHomepage = $this->entityRepo->getById('page', $id, false, true);
- $this->entityRepo->renderPage($customHomepage, true);
+ $customHomepage = Page::query()->where('draft', '=', false)->findOrFail($id);
+ $pageContent = new PageContent($customHomepage);
+ $customHomepage->html = $pageContent->render(true);
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
<?php namespace BookStack\Http\Controllers\Images;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Page;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
/**
* Update image details
- * @param integer $id
* @param Request $request
+ * @param integer $id
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
- public function update($id, Request $request)
+ public function update(Request $request, $id)
{
$this->validate($request, [
'name' => 'required|min:2|string'
/**
* Show the usage of an image on pages.
- * @param \BookStack\Entities\Repos\EntityRepo $entityRepo
- * @param $id
- * @return \Illuminate\Http\JsonResponse
*/
- public function usage(EntityRepo $entityRepo, $id)
+ public function usage(int $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
- $pageSearch = $entityRepo->searchForImage($image->url);
- return response()->json($pageSearch);
+
+ $pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
+ foreach ($pages as $page) {
+ $page->url = $page->getUrl();
+ $page->html = '';
+ $page->text = '';
+ }
+ $result = count($pages) > 0 ? $pages : false;
+
+ return response()->json($result);
}
/**
<?php namespace BookStack\Http\Controllers;
use Activity;
-use BookStack\Auth\UserRepo;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\BookContents;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Managers\PageEditActivity;
+use BookStack\Entities\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
-use GatherContent\Htmldiff\Htmldiff;
+use BookStack\Exceptions\NotifyException;
+use BookStack\Exceptions\PermissionsException;
+use Exception;
use Illuminate\Http\Request;
-use Illuminate\Http\Response;
+use Illuminate\Validation\ValidationException;
+use Throwable;
use Views;
class PageController extends Controller
{
protected $pageRepo;
- protected $exportService;
- protected $userRepo;
/**
* PageController constructor.
- * @param \BookStack\Entities\Repos\PageRepo $pageRepo
- * @param \BookStack\Entities\ExportService $exportService
- * @param UserRepo $userRepo
*/
- public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
+ public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
- $this->exportService = $exportService;
- $this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new page.
- * @param string $bookSlug
- * @param string $chapterSlug
- * @return Response
- * @internal param bool $pageSlug
- * @throws NotFoundException
+ * @throws Throwable
*/
- public function create($bookSlug, $chapterSlug = null)
+ public function create(string $bookSlug, string $chapterSlug = null)
{
- if ($chapterSlug !== null) {
- $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
- } else {
- $chapter = null;
- $book = $this->pageRepo->getBySlug('book', $bookSlug);
- }
-
- $parent = $chapter ? $chapter : $book;
+ $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
- if ($this->signedIn) {
- $draft = $this->pageRepo->getDraftPage($book, $chapter);
+ if ($this->isSignedIn()) {
+ $draft = $this->pageRepo->getNewDraftPage($parent);
return redirect($draft->getUrl());
}
/**
* Create a new page as a guest user.
- * @param Request $request
- * @param string $bookSlug
- * @param string|null $chapterSlug
- * @return mixed
- * @throws NotFoundException
+ * @throws ValidationException
*/
- public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
+ public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- if ($chapterSlug !== null) {
- $chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
- $book = $chapter->book;
- } else {
- $chapter = null;
- $book = $this->pageRepo->getBySlug('book', $bookSlug);
- }
-
- $parent = $chapter ? $chapter : $book;
+ $parent = $this->pageRepo->getParentFromSlugs($bookSlug, $chapterSlug);
$this->checkOwnablePermission('page-create', $parent);
- $page = $this->pageRepo->getDraftPage($book, $chapter);
- $this->pageRepo->publishPageDraft($page, [
+ $page = $this->pageRepo->getNewDraftPage($parent);
+ $this->pageRepo->publishDraft($page, [
'name' => $request->get('name'),
'html' => ''
]);
+
return redirect($page->getUrl('/edit'));
}
/**
* Show form to continue editing a draft page.
- * @param string $bookSlug
- * @param int $pageId
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * @throws NotFoundException
*/
- public function editDraft($bookSlug, $pageId)
+ public function editDraft(string $bookSlug, int $pageId)
{
- $draft = $this->pageRepo->getById('page', $pageId, true);
- $this->checkOwnablePermission('page-create', $draft->parent);
+ $draft = $this->pageRepo->getById($pageId);
+ $this->checkOwnablePermission('page-create', $draft->parent());
$this->setPageTitle(trans('entities.pages_edit_draft'));
- $draftsEnabled = $this->signedIn;
- $templates = $this->pageRepo->getPageTemplates(10);
+ $draftsEnabled = $this->isSignedIn();
+ $templates = $this->pageRepo->getTemplates(10);
return view('pages.edit', [
'page' => $draft,
/**
* Store a new page by changing a draft into a page.
- * @param Request $request
- * @param string $bookSlug
- * @param int $pageId
- * @return Response
+ * @throws NotFoundException
+ * @throws ValidationException
*/
- public function store(Request $request, $bookSlug, $pageId)
+ public function store(Request $request, string $bookSlug, int $pageId)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
+ $draftPage = $this->pageRepo->getById($pageId);
+ $this->checkOwnablePermission('page-create', $draftPage->parent());
- $input = $request->all();
- $draftPage = $this->pageRepo->getById('page', $pageId, true);
- $book = $draftPage->book;
-
- $parent = $draftPage->parent;
- $this->checkOwnablePermission('page-create', $parent);
-
- if ($parent->isA('chapter')) {
- $input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
- } else {
- $input['priority'] = $this->pageRepo->getNewBookPriority($parent);
- }
-
- $page = $this->pageRepo->publishPageDraft($draftPage, $input);
+ $page = $this->pageRepo->publishDraft($draftPage, $request->all());
+ Activity::add($page, 'page_create', $draftPage->book->id);
- Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
}
/**
* Display the specified page.
* If the page is not found via the slug the revisions are searched for a match.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
* @throws NotFoundException
*/
- public function show($bookSlug, $pageSlug)
+ public function show(string $bookSlug, string $pageSlug)
{
try {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
} catch (NotFoundException $e) {
- $page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getByOldSlug($bookSlug, $pageSlug);
+
if ($page === null) {
throw $e;
}
+
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
- $page->html = $this->pageRepo->renderPage($page);
- $sidebarTree = $this->pageRepo->getBookChildren($page->book);
- $pageNav = $this->pageRepo->getPageNav($page->html);
+ $pageContent = (new PageContent($page));
+ $page->html = $pageContent->render();
+ $sidebarTree = (new BookContents($page->book))->getTree();
+ $pageNav = $pageContent->getNavigation($page->html);
- // check if the comment's are enabled
+ // Check if page comments are enabled
$commentsEnabled = !setting('app-disable-comments');
if ($commentsEnabled) {
$page->load(['comments.createdBy']);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
- 'page' => $page,'book' => $page->book,
+ 'page' => $page,
+ 'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
/**
* Get page from an ajax request.
- * @param int $pageId
- * @return \Illuminate\Http\JsonResponse
+ * @throws NotFoundException
*/
- public function getPageAjax($pageId)
+ public function getPageAjax(int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
* @throws NotFoundException
*/
- public function edit($bookSlug, $pageSlug)
+ public function edit(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
+
$page->isDraft = false;
+ $editActivity = new PageEditActivity($page);
// Check for active editing
$warnings = [];
- if ($this->pageRepo->isPageEditingActive($page, 60)) {
- $warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
+ if ($editActivity->hasActiveEditing()) {
+ $warnings[] = $editActivity->activeEditingMessage();
}
// Check for a current draft version for this user
- $userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
- if ($userPageDraft !== null) {
- $page->name = $userPageDraft->name;
- $page->html = $userPageDraft->html;
- $page->markdown = $userPageDraft->markdown;
+ $userDraft = $this->pageRepo->getUserDraft($page);
+ if ($userDraft !== null) {
+ $page->forceFill($userDraft->only(['name', 'html', 'markdown']));
$page->isDraft = true;
- $warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
+ $warnings[] = $editActivity->getEditingActiveDraftMessage($userDraft);
}
if (count($warnings) > 0) {
- session()->flash('warning', implode("\n", $warnings));
+ $this->showWarningNotification(implode("\n", $warnings));
}
- $draftsEnabled = $this->signedIn;
- $templates = $this->pageRepo->getPageTemplates(10);
-
+ $templates = $this->pageRepo->getTemplates(10);
+ $draftsEnabled = $this->isSignedIn();
+ $this->setPageTitle(trans('entities.pages_editing_named', ['pageName' => $page->getShortName()]));
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
/**
* Update the specified page in storage.
- * @param Request $request
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
+ * @throws ValidationException
+ * @throws NotFoundException
*/
- public function update(Request $request, $bookSlug, $pageSlug)
+ public function update(Request $request, string $bookSlug, string $pageSlug)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
- $this->pageRepo->updatePage($page, $page->book->id, $request->all());
+
+ $this->pageRepo->update($page, $request->all());
Activity::add($page, 'page_update', $page->book->id);
+
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
- * @param Request $request
- * @param int $pageId
- * @return \Illuminate\Http\JsonResponse
+ * @throws NotFoundException
*/
- public function saveDraft(Request $request, $pageId)
+ public function saveDraft(Request $request, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
- if (!$this->signedIn) {
- return response()->json([
- 'status' => 'error',
- 'message' => trans('errors.guests_cannot_save_drafts'),
- ], 500);
+ if (!$this->isSignedIn()) {
+ return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
}
/**
- * Redirect from a special link url which
- * uses the page id rather than the name.
- * @param int $pageId
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * Redirect from a special link url which uses the page id rather than the name.
+ * @throws NotFoundException
*/
- public function redirectFromLink($pageId)
+ public function redirectFromLink(int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId);
+ $page = $this->pageRepo->getById($pageId);
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\View\View
+ * @throws NotFoundException
*/
- public function showDelete($bookSlug, $pageSlug)
+ public function showDelete(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
- return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+ return view('pages.delete', [
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page
+ ]);
}
-
/**
* Show the deletion page for the specified page.
- * @param string $bookSlug
- * @param int $pageId
- * @return \Illuminate\View\View
* @throws NotFoundException
*/
- public function showDeleteDraft($bookSlug, $pageId)
+ public function showDeleteDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
- return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
+ return view('pages.delete', [
+ 'book' => $page->book,
+ 'page' => $page,
+ 'current' => $page
+ ]);
}
/**
* Remove the specified page from storage.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return Response
- * @internal param int $id
+ * @throws NotFoundException
+ * @throws Throwable
+ * @throws NotifyException
*/
- public function destroy($bookSlug, $pageSlug)
+ public function destroy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $book = $page->book;
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
- $this->pageRepo->destroyPage($page);
- Activity::addMessage('page_delete', $book->id, $page->name);
- session()->flash('success', trans('entities.pages_delete_success'));
+ $book = $page->book;
+ $this->pageRepo->destroy($page);
+ Activity::addMessage('page_delete', $page->name, $book->id);
+
+ $this->showSuccessNotification(trans('entities.pages_delete_success'));
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
- * @param string $bookSlug
- * @param int $pageId
- * @return Response
* @throws NotFoundException
+ * @throws NotifyException
+ * @throws Throwable
*/
- public function destroyDraft($bookSlug, $pageId)
+ public function destroyDraft(string $bookSlug, int $pageId)
{
- $page = $this->pageRepo->getById('page', $pageId, true);
+ $page = $this->pageRepo->getById($pageId);
$book = $page->book;
+ $chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page);
- session()->flash('success', trans('entities.pages_delete_draft_success'));
- $this->pageRepo->destroyPage($page);
- return redirect($book->getUrl());
- }
-
- /**
- * Shows the last revisions for this page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\View\View
- * @throws NotFoundException
- */
- public function showRevisions($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
- return view('pages.revisions', ['page' => $page, 'current' => $page]);
- }
- /**
- * Shows a preview of a single revision
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revisionId
- * @return \Illuminate\View\View
- */
- public function showRevision($bookSlug, $pageSlug, $revisionId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- if ($revision === null) {
- abort(404);
- }
+ $this->pageRepo->destroy($page);
- $page->fill($revision->toArray());
- $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+ $this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
- return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => null,
- 'revision' => $revision
- ]);
- }
-
- /**
- * Shows the changes of a single revision
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revisionId
- * @return \Illuminate\View\View
- */
- public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $revision = $page->revisions()->where('id', '=', $revisionId)->first();
- if ($revision === null) {
- abort(404);
+ if ($chapter && userCan('view', $chapter)) {
+ return redirect($chapter->getUrl());
}
-
- $prev = $revision->getPrevious();
- $prevContent = ($prev === null) ? '' : $prev->html;
- $diff = (new Htmldiff)->diff($prevContent, $revision->html);
-
- $page->fill($revision->toArray());
- $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
-
- return view('pages.revision', [
- 'page' => $page,
- 'book' => $page->book,
- 'diff' => $diff,
- 'revision' => $revision
- ]);
- }
-
- /**
- * Restores a page using the content of the specified revision.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revisionId
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function restoreRevision($bookSlug, $pageSlug, $revisionId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->checkOwnablePermission('page-update', $page);
- $page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
- Activity::add($page, 'page_restore', $page->book->id);
- return redirect($page->getUrl());
- }
-
-
- /**
- * Deletes a revision using the id of the specified revision.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param int $revId
- * @throws NotFoundException
- * @throws BadRequestException
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- */
- public function destroyRevision($bookSlug, $pageSlug, $revId)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $this->checkOwnablePermission('page-delete', $page);
-
- $revision = $page->revisions()->where('id', '=', $revId)->first();
- if ($revision === null) {
- throw new NotFoundException("Revision #{$revId} not found");
- }
-
- // Get the current revision for the page
- $currentRevision = $page->getCurrentRevision();
-
- // Check if its the latest revision, cannot delete latest revision.
- if (intval($currentRevision->id) === intval($revId)) {
- session()->flash('error', trans('entities.revision_cannot_delete_latest'));
- return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
- }
-
- $revision->delete();
- session()->flash('success', trans('entities.revision_delete_success'));
- return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
- }
-
- /**
- * Exports a page to a PDF.
- * https://github.com/barryvdh/laravel-dompdf
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPdf($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $page->html = $this->pageRepo->renderPage($page);
- $pdfContent = $this->exportService->pageToPdf($page);
- return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
- }
-
- /**
- * Export a page to a self-contained HTML file.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportHtml($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $page->html = $this->pageRepo->renderPage($page);
- $containedHtml = $this->exportService->pageToContainedHtml($page);
- return $this->downloadResponse($containedHtml, $pageSlug . '.html');
- }
-
- /**
- * Export a page to a simple plaintext .txt file.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Http\Response
- */
- public function exportPlainText($bookSlug, $pageSlug)
- {
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
- $pageText = $this->exportService->pageToPlainText($page);
- return $this->downloadResponse($pageText, $pageSlug . '.txt');
+ return redirect($book->getUrl());
}
/**
- * Show a listing of recently created pages
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ * Show a listing of recently created pages.
*/
public function showRecentlyUpdated()
{
- // TODO - Still exist?
- $pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
+ $pages = Page::visible()->orderBy('updated_at', 'desc')
+ ->paginate(20)
+ ->setPath(url('/pages/recently-updated'));
+
return view('pages.detailed-listing', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
/**
* Show the view to choose a new parent to move a page into.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return mixed
* @throws NotFoundException
*/
- public function showMove($bookSlug, $pageSlug)
+ public function showMove(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
}
/**
- * Does the action of moving the location of a page
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return mixed
+ * Does the action of moving the location of a page.
* @throws NotFoundException
+ * @throws Throwable
*/
- public function move($bookSlug, $pageSlug, Request $request)
+ public function move(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return redirect($page->getUrl());
}
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
-
try {
- $parent = $this->pageRepo->getById($entityType, $entityId);
- } catch (\Exception $e) {
- session()->flash(trans('entities.selected_book_chapter_not_found'));
+ $parent = $this->pageRepo->move($page, $entitySelection);
+ } catch (Exception $exception) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
+
+ $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
return redirect()->back();
}
- $this->checkOwnablePermission('page-create', $parent);
-
- $this->pageRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
- session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
+ $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return mixed
* @throws NotFoundException
*/
- public function showCopy($bookSlug, $pageSlug)
+ public function showCopy(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
]);
}
+
/**
* Create a copy of a page within the requested target destination.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return mixed
* @throws NotFoundException
+ * @throws Throwable
*/
- public function copy($bookSlug, $pageSlug, Request $request)
+ public function copy(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
- $entitySelection = $request->get('entity_selection', null);
- if ($entitySelection === null || $entitySelection === '') {
- $parent = $page->chapter ? $page->chapter : $page->book;
- } else {
- $stringExploded = explode(':', $entitySelection);
- $entityType = $stringExploded[0];
- $entityId = intval($stringExploded[1]);
-
- try {
- $parent = $this->pageRepo->getById($entityType, $entityId);
- } catch (\Exception $e) {
- session()->flash(trans('entities.selected_book_chapter_not_found'));
- return redirect()->back();
- }
- }
+ $entitySelection = $request->get('entity_selection', null) ?? null;
+ $newName = $request->get('name', null);
- $this->checkOwnablePermission('page-create', $parent);
+ try {
+ $pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
+ } catch (Exception $exception) {
+ if ($exception instanceof PermissionsException) {
+ $this->showPermissionError();
+ }
- $pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
+ $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
+ return redirect()->back();
+ }
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
- session()->flash('success', trans('entities.pages_copy_success'));
+ $this->showSuccessNotification(trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
- * @param string $bookSlug
- * @param string $pageSlug
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws NotFoundException
*/
- public function showPermissions($bookSlug, $pageSlug)
+ public function showPermissions(string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $roles = $this->userRepo->getRestrictableRoles();
return view('pages.permissions', [
'page' => $page,
- 'roles' => $roles
]);
}
/**
* Set the permissions for this page.
- * @param string $bookSlug
- * @param string $pageSlug
- * @param Request $request
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
- * @throws \Throwable
+ * @throws Throwable
*/
- public function permissions($bookSlug, $pageSlug, Request $request)
+ public function permissions(Request $request, string $bookSlug, string $pageSlug)
{
- $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
- session()->flash('success', trans('entities.pages_permissions_success'));
+
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
+ $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+
+ $this->showSuccessNotification(trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\ExportService;
+use BookStack\Entities\Managers\PageContent;
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use Throwable;
+
+class PageExportController extends Controller
+{
+
+ protected $pageRepo;
+ protected $exportService;
+
+ /**
+ * PageExportController constructor.
+ * @param PageRepo $pageRepo
+ * @param ExportService $exportService
+ */
+ public function __construct(PageRepo $pageRepo, ExportService $exportService)
+ {
+ $this->pageRepo = $pageRepo;
+ $this->exportService = $exportService;
+ parent::__construct();
+ }
+
+ /**
+ * Exports a page to a PDF.
+ * https://github.com/barryvdh/laravel-dompdf
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function pdf(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page->html = (new PageContent($page))->render();
+ $pdfContent = $this->exportService->pageToPdf($page);
+ return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
+ }
+
+ /**
+ * Export a page to a self-contained HTML file.
+ * @throws NotFoundException
+ * @throws Throwable
+ */
+ public function html(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $page->html = (new PageContent($page))->render();
+ $containedHtml = $this->exportService->pageToContainedHtml($page);
+ return $this->downloadResponse($containedHtml, $pageSlug . '.html');
+ }
+
+ /**
+ * Export a page to a simple plaintext .txt file.
+ * @throws NotFoundException
+ */
+ public function plainText(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $pageText = $this->exportService->pageToPlainText($page);
+ return $this->downloadResponse($pageText, $pageSlug . '.txt');
+ }
+}
--- /dev/null
+<?php namespace BookStack\Http\Controllers;
+
+use BookStack\Entities\Repos\PageRepo;
+use BookStack\Exceptions\NotFoundException;
+use BookStack\Facades\Activity;
+use GatherContent\Htmldiff\Htmldiff;
+
+class PageRevisionController extends Controller
+{
+
+ protected $pageRepo;
+
+ /**
+ * PageRevisionController constructor.
+ */
+ public function __construct(PageRepo $pageRepo)
+ {
+ $this->pageRepo = $pageRepo;
+ parent::__construct();
+ }
+
+ /**
+ * Shows the last revisions for this page.
+ * @throws NotFoundException
+ */
+ public function index(string $bookSlug, string $pageSlug)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
+ return view('pages.revisions', [
+ 'page' => $page,
+ 'current' => $page
+ ]);
+ }
+
+ /**
+ * Shows a preview of a single revision.
+ * @throws NotFoundException
+ */
+ public function show(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ if ($revision === null) {
+ throw new NotFoundException();
+ }
+
+ $page->fill($revision->toArray());
+
+ $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
+ return view('pages.revision', [
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => null,
+ 'revision' => $revision
+ ]);
+ }
+
+ /**
+ * Shows the changes of a single revision.
+ * @throws NotFoundException
+ */
+ public function changes(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $revision = $page->revisions()->where('id', '=', $revisionId)->first();
+ if ($revision === null) {
+ throw new NotFoundException();
+ }
+
+ $prev = $revision->getPrevious();
+ $prevContent = $prev->html ?? '';
+ $diff = (new Htmldiff)->diff($prevContent, $revision->html);
+
+ $page->fill($revision->toArray());
+ $this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
+
+ return view('pages.revision', [
+ 'page' => $page,
+ 'book' => $page->book,
+ 'diff' => $diff,
+ 'revision' => $revision
+ ]);
+ }
+
+ /**
+ * Restores a page using the content of the specified revision.
+ * @throws NotFoundException
+ */
+ public function restore(string $bookSlug, string $pageSlug, int $revisionId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->checkOwnablePermission('page-update', $page);
+
+ $page = $this->pageRepo->restoreRevision($page, $revisionId);
+
+ Activity::add($page, 'page_restore', $page->book->id);
+ return redirect($page->getUrl());
+ }
+
+ /**
+ * Deletes a revision using the id of the specified revision.
+ * @throws NotFoundException
+ */
+ public function destroy(string $bookSlug, string $pageSlug, int $revId)
+ {
+ $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+ $this->checkOwnablePermission('page-delete', $page);
+
+ $revision = $page->revisions()->where('id', '=', $revId)->first();
+ if ($revision === null) {
+ throw new NotFoundException("Revision #{$revId} not found");
+ }
+
+ // Get the current revision for the page
+ $currentRevision = $page->getCurrentRevision();
+
+ // Check if its the latest revision, cannot delete latest revision.
+ if (intval($currentRevision->id) === intval($revId)) {
+ $this->showErrorNotification(trans('entities.revision_cannot_delete_latest'));
+ return redirect($page->getUrl('/revisions'));
+ }
+
+ $revision->delete();
+ $this->showSuccessNotification(trans('entities.revision_delete_success'));
+ return redirect($page->getUrl('/revisions'));
+ }
+}
protected $pageRepo;
/**
- * PageTemplateController constructor.
- * @param $pageRepo
+ * PageTemplateController constructor
*/
public function __construct(PageRepo $pageRepo)
{
/**
* Fetch a list of templates from the system.
- * @param Request $request
- * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function list(Request $request)
{
$page = $request->get('page', 1);
$search = $request->get('search', '');
- $templates = $this->pageRepo->getPageTemplates(10, $page, $search);
+ $templates = $this->pageRepo->getTemplates(10, $page, $search);
if ($search) {
$templates->appends(['search' => $search]);
/**
* Get the content of a template.
- * @param $templateId
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
* @throws NotFoundException
*/
- public function get($templateId)
+ public function get(int $templateId)
{
- $page = $this->pageRepo->getById('page', $templateId);
+ $page = $this->pageRepo->getById($templateId);
if (!$page->template) {
throw new NotFoundException();
'markdown' => $page->markdown,
]);
}
-
}
]);
$this->permissionsRepo->saveNewRole($request->all());
- session()->flash('success', trans('settings.role_create_success'));
+ $this->showSuccessNotification(trans('settings.role_create_success'));
return redirect('/settings/roles');
}
/**
* Updates a user role.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
+ * @throws \Illuminate\Validation\ValidationException
*/
- public function updateRole($id, Request $request)
+ public function updateRole(Request $request, $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
]);
$this->permissionsRepo->updateRole($id, $request->all());
- session()->flash('success', trans('settings.role_update_success'));
+ $this->showSuccessNotification(trans('settings.role_update_success'));
return redirect('/settings/roles');
}
/**
* Delete a role from the system,
* Migrate from a previous role if set.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
- public function deleteRole($id, Request $request)
+ public function deleteRole(Request $request, $id)
{
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
- session()->flash('error', $e->getMessage());
+ $this->showErrorNotification($e->getMessage());
return redirect()->back();
}
- session()->flash('success', trans('settings.role_delete_success'));
+ $this->showSuccessNotification(trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
-use BookStack\Entities\EntityContextManager;
-use BookStack\Entities\Repos\EntityRepo;
+use BookStack\Entities\Book;
+use BookStack\Entities\Bookshelf;
+use BookStack\Entities\Entity;
+use BookStack\Entities\Managers\EntityContext;
use BookStack\Entities\SearchService;
-use BookStack\Exceptions\NotFoundException;
-use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
-use Illuminate\View\View;
class SearchController extends Controller
{
- protected $entityRepo;
protected $viewService;
protected $searchService;
protected $entityContextManager;
/**
* SearchController constructor.
- * @param EntityRepo $entityRepo
- * @param ViewService $viewService
- * @param SearchService $searchService
- * @param EntityContextManager $entityContextManager
*/
public function __construct(
- EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
- EntityContextManager $entityContextManager
+ EntityContext $entityContextManager
) {
- $this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
/**
* Searches all entities.
- * @param Request $request
- * @return View
- * @internal param string $searchTerm
*/
public function search(Request $request)
{
/**
* Searches all entities within a book.
- * @param Request $request
- * @param integer $bookId
- * @return View
- * @internal param string $searchTerm
*/
- public function searchBook(Request $request, $bookId)
+ public function searchBook(Request $request, int $bookId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
/**
* Searches all entities within a chapter.
- * @param Request $request
- * @param integer $chapterId
- * @return View
- * @internal param string $searchTerm
*/
- public function searchChapter(Request $request, $chapterId)
+ public function searchChapter(Request $request, int $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
/**
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
- * @param Request $request
- * @return mixed
*/
public function searchEntitiesAjax(Request $request)
{
/**
* Search siblings items in the system.
- * @param Request $request
- * @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
- $entity = $this->entityRepo->getById($type, $id);
+ $entity = Entity::getEntityInstance($type)->newQuery()->visible()->find($id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
- $entities = $this->entityRepo->getChapterChildren($entity->chapter);
+ $entities = $entity->chapter->visiblePages();
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
- $entities = $this->entityRepo->getBookDirectChildren($entity->book);
+ $entities = $entity->book->getDirectChildren();
}
// Book
if ($entity->isA('book')) {
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
- $entities = $this->entityRepo->getBookshelfChildren($contextShelf);
+ $entities = $contextShelf->visibleBooks()->get();
} else {
- $entities = $this->entityRepo->getAll('book');
+ $entities = Book::visible()->get();
}
}
// Shelve
if ($entity->isA('bookshelf')) {
- $entities = $this->entityRepo->getAll('bookshelf');
+ $entities = Bookshelf::visible()->get();
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
*/
public function update(Request $request)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
setting()->remove('app-logo');
}
- session()->flash('success', trans('settings.settings_save_success'));
+ $this->showSuccessNotification(trans('settings.settings_save_success'));
return redirect('/settings');
}
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
- session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
+ $this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
- session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
+ $this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\Access\SocialAuthService;
+use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
+use Illuminate\Support\Str;
class UserController extends Controller
{
protected $user;
protected $userRepo;
+ protected $inviteService;
protected $imageRepo;
/**
* UserController constructor.
* @param User $user
* @param UserRepo $userRepo
+ * @param UserInviteService $inviteService
* @param ImageRepo $imageRepo
*/
- public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
+ public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
+ $this->inviteService = $inviteService;
$this->imageRepo = $imageRepo;
parent::__construct();
}
];
$authMethod = config('auth.method');
- if ($authMethod === 'standard') {
- $validationRules['password'] = 'required|min:5';
+ $sendInvite = ($request->get('send_invite', 'false') === 'true');
+
+ if ($authMethod === 'standard' && !$sendInvite) {
+ $validationRules['password'] = 'required|min:6';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
$validationRules['external_auth_id'] = 'required';
$user = $this->user->fill($request->all());
if ($authMethod === 'standard') {
- $user->password = bcrypt($request->get('password'));
+ $user->password = bcrypt($request->get('password', Str::random(32)));
} elseif ($authMethod === 'ldap') {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
+ if ($sendInvite) {
+ $this->inviteService->sendInvitation($user);
+ }
+
if ($request->filled('roles')) {
$roles = $request->get('roles');
$this->userRepo->setUserRoles($user, $roles);
*/
public function update(Request $request, $id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->validate($request, [
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
- 'password' => 'min:5|required_with:password_confirm',
+ 'password' => 'min:6|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
}
// External auth id updates
- if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
+ if (user()->can('users-manage') && $request->filled('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
}
$user->save();
- session()->flash('success', trans('settings.users_edit_success'));
+ $this->showSuccessNotification(trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
*/
public function destroy($id)
{
- $this->preventAccessForDemoUsers();
+ $this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) {
- session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
+ $this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
if ($user->system_name === 'public') {
- session()->flash('error', trans('errors.users_cannot_delete_guest'));
+ $this->showErrorNotification(trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
- session()->flash('success', trans('settings.users_delete_success'));
+ $this->showSuccessNotification(trans('settings.users_delete_success'));
return redirect('/settings/users');
}
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
- $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
+ $recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users.profile', [
/**
* Update the user's preferred book-list display setting.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse
*/
- public function switchBookView($id, Request $request)
+ public function switchBookView(Request $request, $id)
{
return $this->switchViewType($id, $request, 'books');
}
/**
* Update the user's preferred shelf-list display setting.
- * @param $id
* @param Request $request
+ * @param $id
* @return \Illuminate\Http\RedirectResponse
*/
- public function switchShelfView($id, Request $request)
+ public function switchShelfView(Request $request, $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* Change the stored sort type for a particular view.
+ * @param Request $request
* @param string $id
* @param string $type
- * @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
- public function changeSort(string $id, string $type, Request $request)
+ public function changeSort(Request $request, string $id, string $type)
{
$validSortTypes = ['books', 'bookshelves'];
if (!in_array($type, $validSortTypes)) {
/**
* Update the stored section expansion preference for the given user.
+ * @param Request $request
* @param string $id
* @param string $key
- * @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
- public function updateExpansionPreference(string $id, string $key, Request $request)
+ public function updateExpansionPreference(Request $request, string $id, string $key)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
* @var array
*/
protected $middleware = [
- \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
+ \BookStack\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\BookStack\Http\Middleware\TrimStrings::class,
\BookStack\Http\Middleware\TrustProxies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \Illuminate\Routing\Middleware\ThrottleRequests::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
- \BookStack\Http\Middleware\Localization::class
+ \BookStack\Http\Middleware\Localization::class,
+ \BookStack\Http\Middleware\GlobalViewData::class,
],
'api' => [
'throttle:60,1',
--- /dev/null
+<?php
+
+namespace BookStack\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
+
+class CheckForMaintenanceMode extends Middleware
+{
+ /**
+ * The URIs that should be reachable while maintenance mode is enabled.
+ *
+ * @var array
+ */
+ protected $except = [
+ //
+ ];
+}
--- /dev/null
+<?php namespace BookStack\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+
+/**
+ * Class GlobalViewData
+ * Sets up data that is accessible to any view rendered by the web routes.
+ */
+class GlobalViewData
+{
+
+ /**
+ * Handle an incoming request.
+ *
+ * @param Request $request
+ * @param Closure $next
+ * @return mixed
+ */
+ public function handle(Request $request, Closure $next)
+ {
+ view()->share('signedIn', auth()->check());
+ view()->share('currentUser', user());
+
+ return $next($request);
+ }
+}
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
+ 'tr' => 'tr_TR',
];
/**
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
+ config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
+
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
return $default;
}
+ /**
+ * Get the ISO version of a BookStack language name
+ * @param string $locale
+ * @return string
+ */
+ public function getLocaleIso(string $locale)
+ {
+ return $this->localeMap[$locale] ?? $locale;
+ }
+
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
*/
protected function setSystemDateLocale(string $locale)
{
- $systemLocale = $this->localeMap[$locale] ?? $locale;
+ $systemLocale = $this->getLocaleIso($locale);
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
protected $proxies;
/**
- * The current proxy header mappings.
+ * The headers that should be used to detect proxies.
*
- * @var array
+ * @var int
*/
- protected $headers = [
- Request::HEADER_FORWARDED => 'FORWARDED',
- Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
- Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
- Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
- Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
- ];
+ protected $headers = Request::HEADER_X_FORWARDED_ALL;
/**
* Handle the request, Set the correct user-configured proxy information.
class VerifyCsrfToken extends Middleware
{
+ /**
+ * Indicates whether the XSRF-TOKEN cookie should be set on the response.
+ *
+ * @var bool
+ */
+ protected $addHttpCookie = true;
+
/**
* The URIs that should be excluded from CSRF verification.
*
return $base;
}
-
-}
\ No newline at end of file
+}
--- /dev/null
+<?php namespace BookStack\Notifications;
+
+class UserInvite extends MailNotification
+{
+ public $token;
+
+ /**
+ * Create a new notification instance.
+ * @param string $token
+ */
+ public function __construct($token)
+ {
+ $this->token = $token;
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ $appName = ['appName' => setting('app-name')];
+ return $this->newMailMessage()
+ ->subject(trans('auth.user_invite_email_subject', $appName))
+ ->greeting(trans('auth.user_invite_email_greeting', $appName))
+ ->line(trans('auth.user_invite_email_text'))
+ ->action(trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
+ }
+}
public function boot()
{
// Set root URL
- URL::forceRootUrl(config('app.url'));
+ $appUrl = config('app.url');
+ if ($appUrl) {
+ $isHttps = (strpos($appUrl, 'https://') === 0);
+ URL::forceRootUrl($appUrl);
+ URL::forceScheme($isHttps ? 'https' : 'http');
+ }
// Custom validation methods
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
return "<?php echo icon($expression); ?>";
});
- Blade::directive('exposeTranslations', function($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" .
use BookStack\Actions\ActivityService;
use BookStack\Actions\ViewService;
+use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\SettingService;
use BookStack\Uploads\ImageService;
use Illuminate\Support\ServiceProvider;
*/
public function register()
{
- $this->app->bind('activity', function () {
+ $this->app->singleton('activity', function () {
return $this->app->make(ActivityService::class);
});
- $this->app->bind('views', function () {
+ $this->app->singleton('views', function () {
return $this->app->make(ViewService::class);
});
- $this->app->bind('setting', function () {
+ $this->app->singleton('setting', function () {
return $this->app->make(SettingService::class);
});
- $this->app->bind('images', function () {
+ $this->app->singleton('images', function () {
return $this->app->make(ImageService::class);
});
+
+ $this->app->singleton('permissions', function () {
+ return $this->app->make(PermissionService::class);
+ });
}
}
*/
public function getFileName()
{
- if (str_contains($this->name, '.')) {
+ if (strpos($this->name, '.') !== false) {
return $this->name;
}
return $this->name . '.' . $this->extension;
use BookStack\Exceptions\FileUploadException;
use Exception;
+use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
$storage = $this->getStorage();
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
- $uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
+ $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
- $uploadFileName = str_random(3) . $uploadFileName;
+ $uploadFileName = Str::random(3) . $uploadFileName;
}
$attachmentPath = $basePath . $uploadFileName;
use Exception;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
+use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) {
- $imageName = str_random(3) . $imageName;
+ $imageName = Str::random(3) . $imageName;
}
$fullPath = $imagePath . $imageName;
if ($secureUploads) {
- $fullPath = $imagePath . str_random(16) . '-' . $imageName;
+ $fullPath = $imagePath . Str::random(16) . '-' . $imageName;
}
try {
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
- $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
+ $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
* @return string
* @throws Exception
*/
-function versioned_asset($file = '') : string
+function versioned_asset(string $file = ''): string
{
static $version = null;
* Defaults to public 'Guest' user if not logged in.
* @return User
*/
-function user() : User
+function user(): User
{
return auth()->user() ?: User::getDefault();
}
* Check if current user is a signed in user.
* @return bool
*/
-function signedInUser() : bool
+function signedInUser(): bool
{
return auth()->user() && !auth()->user()->isDefault();
}
* Check if the current user has general access.
* @return bool
*/
-function hasAppAccess() : bool
+function hasAppAccess(): bool
{
return !auth()->guest() || setting('app-public');
}
* @param Ownable $ownable
* @return bool
*/
-function userCan(string $permission, Ownable $ownable = null) : bool
+function userCan(string $permission, Ownable $ownable = null): bool
{
if ($ownable === null) {
return user() && user()->can($permission);
* @param string|null $entityClass
* @return bool
*/
-function userCanOnAny(string $permission, string $entityClass = null) : bool
+function userCanOnAny(string $permission, string $entityClass = null): bool
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
/**
* Helper to access system settings.
- * @param $key
- * @param bool $default
+ * @param string $key
+ * @param $default
* @return bool|string|SettingService
*/
-function setting($key = null, $default = false)
+function setting(string $key = null, $default = false)
{
$settingService = resolve(SettingService::class);
if (is_null($key)) {
* @param string $path
* @return string
*/
-function theme_path($path = '') : string
+function theme_path(string $path = ''): string
{
$theme = config('view.theme');
if (!$theme) {
* @param array $attrs
* @return mixed
*/
-function icon($name, $attrs = [])
+function icon(string $name, array $attrs = []): string
{
$attrs = array_merge([
- 'class' => 'svg-icon',
- 'data-icon' => $name
+ 'class' => 'svg-icon',
+ 'data-icon' => $name,
+ 'role' => 'presentation',
], $attrs);
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
- $iconPath = resource_path('assets/icons/' . $name . '.svg');
+ $iconPath = resource_path('icons/' . $name . '.svg');
$themeIconPath = theme_path('icons/' . $name . '.svg');
if ($themeIconPath && file_exists($themeIconPath)) {
$iconPath = $themeIconPath;
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
- * @param $path
+ * @param string $path
* @param array $data
* @param array $overrideData
* @return string
*/
-function sortUrl($path, $data, $overrideData = [])
+function sortUrl(string $path, array $data, array $overrideData = []): string
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
|
*/
-$app = new \BookStack\Application(
- realpath(__DIR__.'/../')
+$app = new BookStack\Application(
+ dirname(__DIR__)
);
/*
"license": "MIT",
"type": "project",
"require": {
- "php": ">=7.0.5",
+ "php": "^7.2",
+ "ext-curl": "*",
+ "ext-dom": "*",
+ "ext-gd": "*",
"ext-json": "*",
+ "ext-mbstring": "*",
"ext-tidy": "*",
- "ext-dom": "*",
"ext-xml": "*",
- "ext-mbstring": "*",
- "ext-gd": "*",
- "ext-curl": "*",
- "laravel/framework": "~5.5.44",
- "fideloper/proxy": "~3.3",
- "intervention/image": "^2.4",
- "laravel/socialite": "3.0.x-dev",
+ "barryvdh/laravel-dompdf": "^0.8.5",
+ "barryvdh/laravel-snappy": "^0.4.5",
+ "doctrine/dbal": "^2.9",
+ "fideloper/proxy": "^4.0",
+ "gathercontent/htmldiff": "^0.2.1",
+ "intervention/image": "^2.5",
+ "laravel/framework": "^6.0",
+ "laravel/socialite": "^4.2",
"league/flysystem-aws-s3-v3": "^1.0",
- "barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",
- "gathercontent/htmldiff": "^0.2.1",
- "barryvdh/laravel-snappy": "^0.4.0",
- "socialiteproviders/slack": "^3.0",
+ "socialiteproviders/discord": "^2.0",
+ "socialiteproviders/gitlab": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
- "socialiteproviders/gitlab": "^3.0",
- "socialiteproviders/twitch": "^3.0",
- "socialiteproviders/discord": "^2.0",
- "doctrine/dbal": "^2.5"
+ "socialiteproviders/slack": "^3.0",
+ "socialiteproviders/twitch": "^5.0"
},
"require-dev": {
- "filp/whoops": "~2.0",
- "fzaninotto/faker": "~1.4",
- "mockery/mockery": "~1.0",
- "phpunit/phpunit": "~6.0",
- "symfony/css-selector": "3.1.*",
- "symfony/dom-crawler": "3.1.*",
- "laravel/browser-kit-testing": "^2.0",
- "barryvdh/laravel-ide-helper": "^2.4.1",
- "barryvdh/laravel-debugbar": "^3.1.0",
- "squizlabs/php_codesniffer": "^3.2"
+ "barryvdh/laravel-debugbar": "^3.2.8",
+ "barryvdh/laravel-ide-helper": "^2.6.4",
+ "facade/ignition": "^1.4",
+ "fzaninotto/faker": "^1.4",
+ "laravel/browser-kit-testing": "^5.1",
+ "mockery/mockery": "^1.0",
+ "nunomaduro/collision": "^3.0",
+ "phpunit/phpunit": "^8.0",
+ "squizlabs/php_codesniffer": "^3.4",
+ "wnx/laravel-stats": "^2.0"
},
"autoload": {
"classmap": [
- "database"
+ "database/seeds",
+ "database/factories"
],
"psr-4": {
"BookStack\\": "app/"
},
"scripts": {
"post-root-package-install": [
- "php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
- "php artisan key:generate"
+ "@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');\""
+ "@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');\"",
+ "@php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"post-install-cmd": [
- "php artisan cache:clear",
- "php artisan view:clear"
+ "@php artisan cache:clear",
+ "@php artisan view:clear"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
- "@php artisan package:discover"
+ "@php artisan package:discover --ansi"
],
"refresh-test-database": [
- "php artisan migrate:refresh --database=mysql_testing",
- "php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
+ "@php artisan migrate:refresh --database=mysql_testing",
+ "@php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
]
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
+ "sort-packages": true,
"platform": {
- "php": "7.0.5"
+ "php": "7.2.0"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "dont-discover": []
}
- }
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "0946a07729a7a1bfef9bac185a870afd",
+ "content-hash": "c156e1738dbab2a57f9a926d9a9a5a6a",
"packages": [
{
"name": "aws/aws-sdk-php",
- "version": "3.86.2",
+ "version": "3.112.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
+ "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
- "reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
+ "reference": "1e21446c6780a3b9b5e4315bd6d4347d2c3381eb",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
- "ext-spl": "*",
"guzzlehttp/guzzle": "^5.3.3|^6.2.1",
"guzzlehttp/promises": "~1.0",
"guzzlehttp/psr7": "^1.4.1",
"ext-sockets": "*",
"nette/neon": "^2.3",
"phpunit/phpunit": "^4.8.35|^5.4.3",
- "psr/cache": "^1.0"
+ "psr/cache": "^1.0",
+ "psr/simple-cache": "^1.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"s3",
"sdk"
],
- "time": "2019-01-18T21:10:44+00:00"
+ "time": "2019-09-12T18:09:53+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
- "version": "v0.8.3",
+ "version": "v0.8.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-dompdf.git",
- "reference": "46781d0304277845a19c09c169bc595fd182cce4"
+ "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/46781d0304277845a19c09c169bc595fd182cce4",
- "reference": "46781d0304277845a19c09c169bc595fd182cce4",
+ "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
+ "reference": "7393732b2f3a3ee357974cbb0c46c9b65b84dad1",
"shasum": ""
},
"require": {
"dompdf/dompdf": "^0.8",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/support": "^5.5|^6",
"php": ">=7"
},
"type": "library",
"laravel",
"pdf"
],
- "time": "2018-08-31T13:25:44+00:00"
+ "time": "2019-08-23T14:30:33+00:00"
},
{
"name": "barryvdh/laravel-snappy",
- "version": "v0.4.3",
+ "version": "v0.4.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-snappy.git",
- "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478"
+ "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
- "reference": "62bb5017b7004bf3e48bfed3d5c00d3dc6e60478",
+ "url": "https://api.github.com/repos/barryvdh/laravel-snappy/zipball/9be767fc7a082665a84945f36c70b0cbead91ce9",
+ "reference": "9be767fc7a082665a84945f36c70b0cbead91ce9",
"shasum": ""
},
"require": {
- "illuminate/filesystem": "5.5.x|5.6.x|5.7.x",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/filesystem": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
+ "illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x|6.0.*",
"knplabs/knp-snappy": "^1",
"php": ">=7"
},
}
],
- "description": "Snappy PDF/Image for Laravel 4",
+ "description": "Snappy PDF/Image for Laravel",
"keywords": [
"image",
"laravel",
"wkhtmltoimage",
"wkhtmltopdf"
],
- "time": "2018-09-06T10:14:15+00:00"
+ "time": "2019-08-30T16:12:23+00:00"
},
{
"name": "cogpowered/finediff",
],
"time": "2014-05-19T10:25:02+00:00"
},
- {
- "name": "doctrine/annotations",
- "version": "v1.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/annotations.git",
- "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
- "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
- "shasum": ""
- },
- "require": {
- "doctrine/lexer": "1.*",
- "php": "^5.6 || ^7.0"
- },
- "require-dev": {
- "doctrine/cache": "1.*",
- "phpunit/phpunit": "^5.7"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.4.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- },
- {
- "name": "Benjamin Eberlei",
- },
- {
- "name": "Guilherme Blanco",
- },
- {
- "name": "Jonathan Wage",
- },
- {
- "name": "Johannes Schmitt",
- }
- ],
- "description": "Docblock Annotations Parser",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "annotations",
- "docblock",
- "parser"
- ],
- "time": "2017-02-24T16:22:25+00:00"
- },
{
"name": "doctrine/cache",
- "version": "v1.6.2",
+ "version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
- "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
+ "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57",
+ "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57",
"shasum": ""
},
"require": {
- "php": "~5.5|~7.0"
+ "php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
- "phpunit/phpunit": "~4.8|~5.0",
- "predis/predis": "~1.0",
- "satooshi/php-coveralls": "~0.6"
+ "alcaeus/mongo-php-adapter": "^1.1",
+ "doctrine/coding-standard": "^4.0",
+ "mongodb/mongodb": "^1.1",
+ "phpunit/phpunit": "^7.0",
+ "predis/predis": "~1.0"
+ },
+ "suggest": {
+ "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6.x-dev"
+ "dev-master": "1.8.x-dev"
}
},
"autoload": {
}
],
"description": "Caching library offering an object-oriented API for many cache backends",
- "homepage": "http://www.doctrine-project.org",
+ "homepage": "https://www.doctrine-project.org",
"keywords": [
"cache",
"caching"
],
- "time": "2017-07-22T12:49:21+00:00"
+ "time": "2018-08-21T18:01:43+00:00"
},
{
- "name": "doctrine/collections",
- "version": "v1.4.0",
+ "name": "doctrine/dbal",
+ "version": "v2.9.2",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/collections.git",
- "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
+ "url": "https://github.com/doctrine/dbal.git",
+ "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
- "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
+ "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
+ "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "doctrine/cache": "^1.0",
+ "doctrine/event-manager": "^1.0",
+ "ext-pdo": "*",
+ "php": "^7.1"
},
"require-dev": {
- "doctrine/coding-standard": "~0.1@dev",
- "phpunit/phpunit": "^5.7"
+ "doctrine/coding-standard": "^5.0",
+ "jetbrains/phpstorm-stubs": "^2018.1.2",
+ "phpstan/phpstan": "^0.10.1",
+ "phpunit/phpunit": "^7.4",
+ "symfony/console": "^2.0.5|^3.0|^4.0",
+ "symfony/phpunit-bridge": "^3.4.5|^4.0.5"
+ },
+ "suggest": {
+ "symfony/console": "For helpful console commands such as SQL execution and import of files."
},
+ "bin": [
+ "bin/doctrine-dbal"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.3.x-dev"
+ "dev-master": "2.9.x-dev",
+ "dev-develop": "3.0.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Collections\\": "lib/"
+ "psr-4": {
+ "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
}
},
"notification-url": "https://packagist.org/downloads/",
{
"name": "Jonathan Wage",
- },
- {
- "name": "Johannes Schmitt",
}
],
- "description": "Collections Abstraction library",
- "homepage": "http://www.doctrine-project.org",
+ "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
+ "homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
- "array",
- "collections",
- "iterator"
+ "abstraction",
+ "database",
+ "dbal",
+ "mysql",
+ "persistence",
+ "pgsql",
+ "php",
+ "queryobject"
],
- "time": "2017-01-03T10:49:41+00:00"
+ "time": "2018-12-31T03:27:51+00:00"
},
{
- "name": "doctrine/common",
- "version": "v2.7.3",
+ "name": "doctrine/event-manager",
+ "version": "v1.0.0",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/common.git",
- "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
+ "url": "https://github.com/doctrine/event-manager.git",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
- "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
+ "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3",
+ "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3",
"shasum": ""
},
"require": {
- "doctrine/annotations": "1.*",
- "doctrine/cache": "1.*",
- "doctrine/collections": "1.*",
- "doctrine/inflector": "1.*",
- "doctrine/lexer": "1.*",
- "php": "~5.6|~7.0"
+ "php": "^7.1"
+ },
+ "conflict": {
+ "doctrine/common": "<2.9@dev"
},
"require-dev": {
- "phpunit/phpunit": "^5.4.6"
+ "doctrine/coding-standard": "^4.0",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.7.x-dev"
+ "dev-master": "1.0.x-dev"
}
},
"autoload": {
{
"name": "Johannes Schmitt",
- }
- ],
- "description": "Common Library for Doctrine projects",
- "homepage": "http://www.doctrine-project.org",
- "keywords": [
- "annotations",
- "collections",
- "eventmanager",
- "persistence",
- "spl"
- ],
- "time": "2017-07-22T08:35:12+00:00"
- },
- {
- "name": "doctrine/dbal",
- "version": "v2.5.13",
- "source": {
- "type": "git",
- "url": "https://github.com/doctrine/dbal.git",
- "reference": "729340d8d1eec8f01bff708e12e449a3415af873"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
- "reference": "729340d8d1eec8f01bff708e12e449a3415af873",
- "shasum": ""
- },
- "require": {
- "doctrine/common": ">=2.4,<2.8-dev",
- "php": ">=5.3.2"
- },
- "require-dev": {
- "phpunit/phpunit": "4.*",
- "symfony/console": "2.*||^3.0"
- },
- "suggest": {
- "symfony/console": "For helpful console commands such as SQL execution and import of files."
- },
- "bin": [
- "bin/doctrine-dbal"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "2.5.x-dev"
- }
- },
- "autoload": {
- "psr-0": {
- "Doctrine\\DBAL\\": "lib/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Roman Borschel",
- },
- {
- "name": "Benjamin Eberlei",
- },
- {
- "name": "Guilherme Blanco",
},
{
- "name": "Jonathan Wage",
- "email": "jonwage@gmail.com"
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com"
}
],
- "description": "Database Abstraction Layer",
- "homepage": "http://www.doctrine-project.org",
+ "description": "Doctrine Event Manager component",
+ "homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
- "database",
- "dbal",
- "persistence",
- "queryobject"
+ "event",
+ "eventdispatcher",
+ "eventmanager"
],
- "time": "2017-07-22T20:44:48+00:00"
+ "time": "2018-06-11T11:59:03+00:00"
},
{
"name": "doctrine/inflector",
- "version": "v1.2.0",
+ "version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
- "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
- "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/5527a48b7313d15261292c149e55e26eae771b0a",
+ "reference": "5527a48b7313d15261292c149e55e26eae771b0a",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^6.2"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "1.3.x-dev"
}
},
"autoload": {
"singularize",
"string"
],
- "time": "2017-07-22T12:18:28+00:00"
+ "time": "2018-01-09T20:05:19+00:00"
},
{
"name": "doctrine/lexer",
- "version": "v1.0.1",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
- "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea",
+ "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": "^7.2"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "phpstan/phpstan": "^0.11.8",
+ "phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.1.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Doctrine\\Common\\Lexer\\": "lib/"
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
}
},
"notification-url": "https://packagist.org/downloads/",
"MIT"
],
"authors": [
- {
- "name": "Roman Borschel",
- },
{
"name": "Guilherme Blanco",
},
+ {
+ "name": "Roman Borschel",
+ },
{
"name": "Johannes Schmitt",
}
],
- "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
- "homepage": "http://www.doctrine-project.org",
+ "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
+ "annotations",
+ "docblock",
"lexer",
- "parser"
+ "parser",
+ "php"
],
- "time": "2014-09-09T13:34:57+00:00"
+ "time": "2019-07-30T19:33:28+00:00"
},
{
"name": "dompdf/dompdf",
"homepage": "https://github.com/dompdf/dompdf",
"time": "2018-12-14T02:40:31+00:00"
},
+ {
+ "name": "dragonmantank/cron-expression",
+ "version": "v2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+ "reference": "72b6fbf76adb3cf5bc0db68559b33d41219aba27",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.4|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Chris Tankersley",
+ "homepage": "https://github.com/dragonmantank"
+ }
+ ],
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+ "keywords": [
+ "cron",
+ "schedule"
+ ],
+ "time": "2019-03-31T00:38:28+00:00"
+ },
{
"name": "egulias/email-validator",
- "version": "2.1.7",
+ "version": "2.1.11",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e"
+ "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/709f21f92707308cdf8f9bcfa1af4cb26586521e",
- "reference": "709f21f92707308cdf8f9bcfa1af4cb26586521e",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23",
+ "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23",
"shasum": ""
},
"require": {
"require-dev": {
"dominicsayers/isemail": "dev-master",
"phpunit/phpunit": "^4.8.35||^5.7||^6.0",
- "satooshi/php-coveralls": "^1.0.1"
+ "satooshi/php-coveralls": "^1.0.1",
+ "symfony/phpunit-bridge": "^4.4@dev"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.1.x-dev"
}
},
"autoload": {
"validation",
"validator"
],
- "time": "2018-12-04T22:38:24+00:00"
+ "time": "2019-08-13T17:33:27+00:00"
},
{
"name": "erusev/parsedown",
- "version": "1.7.1",
+ "version": "1.7.3",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
+ "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
+ "reference": "6d893938171a817f4e9bc9e86f2da1e370b7bcd7",
"shasum": ""
},
"require": {
"markdown",
"parser"
],
- "time": "2018-03-08T01:11:30+00:00"
+ "time": "2019-03-17T18:48:37+00:00"
},
{
"name": "fideloper/proxy",
- "version": "3.3.4",
+ "version": "4.2.1",
"source": {
"type": "git",
"url": "https://github.com/fideloper/TrustedProxy.git",
- "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f"
+ "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9cdf6f118af58d89764249bbcc7bb260c132924f",
- "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f",
+ "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/03085e58ec7bee24773fa5a8850751a6e61a7e8a",
+ "reference": "03085e58ec7bee24773fa5a8850751a6e61a7e8a",
"shasum": ""
},
"require": {
- "illuminate/contracts": "~5.0",
+ "illuminate/contracts": "^5.0|^6.0|^7.0",
"php": ">=5.4.0"
},
"require-dev": {
- "illuminate/http": "~5.0",
- "mockery/mockery": "~0.9.3",
- "phpunit/phpunit": "^5.7"
+ "illuminate/http": "^5.0|^6.0|^7.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^6.0"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-master": "3.3-dev"
- },
"laravel": {
"providers": [
"Fideloper\\Proxy\\TrustedProxyServiceProvider"
"proxy",
"trusted proxy"
],
- "time": "2017-06-15T17:19:42+00:00"
+ "time": "2019-09-03T16:45:42+00:00"
},
{
"name": "gathercontent/htmldiff",
},
{
"name": "guzzlehttp/psr7",
- "version": "1.5.2",
+ "version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "9f83dded91781a01c63574e387eaa769be769115"
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115",
- "reference": "9f83dded91781a01c63574e387eaa769be769115",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"psr/http-message": "~1.0",
- "ralouphie/getallheaders": "^2.0.5"
+ "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
+ "ext-zlib": "*",
"phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
},
+ "suggest": {
+ "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.5-dev"
+ "dev-master": "1.6-dev"
}
},
"autoload": {
"uri",
"url"
],
- "time": "2018-12-04T20:46:45+00:00"
+ "time": "2019-07-01T23:21:34+00:00"
},
{
"name": "intervention/image",
- "version": "2.4.2",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
- "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb"
+ "reference": "39eaef720d082ecc54c64bf54541c55f10db546d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Intervention/image/zipball/e82d274f786e3d4b866a59b173f42e716f0783eb",
- "reference": "e82d274f786e3d4b866a59b173f42e716f0783eb",
+ "url": "https://api.github.com/repos/Intervention/image/zipball/39eaef720d082ecc54c64bf54541c55f10db546d",
+ "reference": "39eaef720d082ecc54c64bf54541c55f10db546d",
"shasum": ""
},
"require": {
"thumbnail",
"watermark"
],
- "time": "2018-05-29T14:19:03+00:00"
+ "time": "2019-06-24T14:06:31+00:00"
},
{
"name": "knplabs/knp-snappy",
- "version": "v1.0.4",
+ "version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/KnpLabs/snappy.git",
- "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850"
+ "reference": "ea037298d3c613454da77ecb9588cf0397d695e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/144c4ecd1ccaeda936bf832b93079efc490e6850",
- "reference": "144c4ecd1ccaeda936bf832b93079efc490e6850",
+ "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/ea037298d3c613454da77ecb9588cf0397d695e1",
+ "reference": "ea037298d3c613454da77ecb9588cf0397d695e1",
"shasum": ""
},
"require": {
- "php": ">=5.6",
+ "php": ">=7.1",
"psr/log": "^1.0",
- "symfony/process": "~2.3 || ~3.0 || ~4.0"
+ "symfony/process": "~3.4||~4.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.8.36"
+ "phpunit/phpunit": "~7.4"
},
"suggest": {
"h4cc/wkhtmltoimage-amd64": "Provides wkhtmltoimage-amd64 binary for Linux-compatible machines, use version `~0.12` as dependency",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
"thumbnail",
"wkhtmltopdf"
],
- "time": "2018-01-22T19:40:51+00:00"
+ "time": "2018-12-14T14:59:37+00:00"
},
{
"name": "laravel/framework",
- "version": "v5.5.44",
+ "version": "v6.0.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b"
+ "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
- "reference": "00615aa27eb98f0ee6fb9f2160c6c60ae04abd1b",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
+ "reference": "56789e9dec750e0fbe8e9e6ae90a01a4e6887902",
"shasum": ""
},
"require": {
- "doctrine/inflector": "~1.1",
- "erusev/parsedown": "~1.7",
+ "doctrine/inflector": "^1.1",
+ "dragonmantank/cron-expression": "^2.0",
+ "egulias/email-validator": "^2.1.10",
+ "erusev/parsedown": "^1.7",
+ "ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"league/flysystem": "^1.0.8",
- "monolog/monolog": "~1.12",
- "mtdowling/cron-expression": "~1.0",
- "nesbot/carbon": "^1.24.1",
- "php": ">=7.0",
- "psr/container": "~1.0",
+ "monolog/monolog": "^1.12|^2.0",
+ "nesbot/carbon": "^2.0",
+ "opis/closure": "^3.1",
+ "php": "^7.2",
+ "psr/container": "^1.0",
"psr/simple-cache": "^1.0",
- "ramsey/uuid": "~3.0",
- "swiftmailer/swiftmailer": "~6.0",
- "symfony/console": "~3.3",
- "symfony/debug": "~3.3",
- "symfony/finder": "~3.3",
- "symfony/http-foundation": "~3.3",
- "symfony/http-kernel": "~3.3",
- "symfony/process": "~3.3",
- "symfony/routing": "~3.3",
- "symfony/var-dumper": "~3.3",
- "tijsverkoyen/css-to-inline-styles": "~2.2",
- "vlucas/phpdotenv": "~2.2"
+ "ramsey/uuid": "^3.7",
+ "swiftmailer/swiftmailer": "^6.0",
+ "symfony/console": "^4.3.4",
+ "symfony/debug": "^4.3.4",
+ "symfony/finder": "^4.3.4",
+ "symfony/http-foundation": "^4.3.4",
+ "symfony/http-kernel": "^4.3.4",
+ "symfony/process": "^4.3.4",
+ "symfony/routing": "^4.3.4",
+ "symfony/var-dumper": "^4.3.4",
+ "tijsverkoyen/css-to-inline-styles": "^2.2.1",
+ "vlucas/phpdotenv": "^3.3"
+ },
+ "conflict": {
+ "tightenco/collect": "<5.5.33"
},
"replace": {
"illuminate/auth": "self.version",
"illuminate/support": "self.version",
"illuminate/translation": "self.version",
"illuminate/validation": "self.version",
- "illuminate/view": "self.version",
- "tightenco/collect": "<5.5.33"
+ "illuminate/view": "self.version"
},
"require-dev": {
- "aws/aws-sdk-php": "~3.0",
- "doctrine/dbal": "~2.5",
- "filp/whoops": "^2.1.4",
- "mockery/mockery": "~1.0",
- "orchestra/testbench-core": "3.5.*",
- "pda/pheanstalk": "~3.0",
- "phpunit/phpunit": "~6.0",
+ "aws/aws-sdk-php": "^3.0",
+ "doctrine/dbal": "^2.6",
+ "filp/whoops": "^2.4",
+ "guzzlehttp/guzzle": "^6.3",
+ "league/flysystem-cached-adapter": "^1.0",
+ "mockery/mockery": "^1.2.3",
+ "moontoast/math": "^1.1",
+ "orchestra/testbench-core": "^4.0",
+ "pda/pheanstalk": "^4.0",
+ "phpunit/phpunit": "^8.3",
"predis/predis": "^1.1.1",
- "symfony/css-selector": "~3.3",
- "symfony/dom-crawler": "~3.3"
+ "symfony/cache": "^4.3",
+ "true/punycode": "^2.1"
},
"suggest": {
- "aws/aws-sdk-php": "Required to use the SQS queue driver and SES mail driver (~3.0).",
- "doctrine/dbal": "Required to rename columns and drop SQLite columns (~2.5).",
+ "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).",
+ "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
+ "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
+ "ext-memcached": "Required to use the memcache cache driver.",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-posix": "Required to use all features of the queue worker.",
- "fzaninotto/faker": "Required to use the eloquent factory builder (~1.4).",
- "guzzlehttp/guzzle": "Required to use the Mailgun and Mandrill mail drivers and the ping methods on schedules (~6.0).",
- "laravel/tinker": "Required to use the tinker console command (~1.0).",
- "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).",
- "league/flysystem-cached-adapter": "Required to use Flysystem caching (~1.0).",
- "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).",
- "nexmo/client": "Required to use the Nexmo transport (~1.0).",
- "pda/pheanstalk": "Required to use the beanstalk queue driver (~3.0).",
- "predis/predis": "Required to use the redis cache and queue drivers (~1.0).",
- "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~3.0).",
- "symfony/css-selector": "Required to use some of the crawler integration testing tools (~3.3).",
- "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (~3.3).",
- "symfony/psr-http-message-bridge": "Required to psr7 bridging features (~1.0)."
+ "ext-redis": "Required to use the Redis cache and queue drivers.",
+ "filp/whoops": "Required for friendly error pages in development (^2.4).",
+ "fzaninotto/faker": "Required to use the eloquent factory builder (^1.4).",
+ "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0).",
+ "laravel/tinker": "Required to use the tinker console command (^1.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).",
+ "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
+ "moontoast/math": "Required to use ordered UUIDs (^1.1).",
+ "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
+ "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^3.0).",
+ "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).",
+ "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).",
+ "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.5-dev"
+ "dev-master": "6.x-dev"
}
},
"autoload": {
"framework",
"laravel"
],
- "time": "2018-10-04T14:51:24+00:00"
+ "time": "2019-09-10T18:46:24+00:00"
},
{
"name": "laravel/socialite",
- "version": "3.0.x-dev",
+ "version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
+ "reference": "f509d06e1e7323997b804c5152874f8aad4508e9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
- "reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/f509d06e1e7323997b804c5152874f8aad4508e9",
+ "reference": "f509d06e1e7323997b804c5152874f8aad4508e9",
"shasum": ""
},
"require": {
+ "ext-json": "*",
"guzzlehttp/guzzle": "~6.0",
- "illuminate/contracts": "~5.4",
- "illuminate/http": "~5.4",
- "illuminate/support": "~5.4",
+ "illuminate/http": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0",
"league/oauth1-client": "~1.0",
- "php": ">=5.6.4"
+ "php": "^7.1.3"
},
"require-dev": {
- "mockery/mockery": "~0.9",
- "phpunit/phpunit": "~4.0|~5.0"
+ "illuminate/contracts": "~5.7.0|~5.8.0|^6.0|^7.0",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^7.0|^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
},
"laravel": {
"providers": [
"laravel",
"oauth"
],
- "time": "2018-12-21T14:06:32+00:00"
+ "time": "2019-09-03T15:27:17+00:00"
},
{
"name": "league/flysystem",
- "version": "1.0.49",
+ "version": "1.0.55",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd"
+ "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a63cc83d8a931b271be45148fa39ba7156782ffd",
- "reference": "a63cc83d8a931b271be45148fa39ba7156782ffd",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6",
+ "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6",
"shasum": ""
},
"require": {
"sftp",
"storage"
],
- "time": "2018-11-23T23:41:29+00:00"
+ "time": "2019-08-24T11:17:19+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "1.0.21",
+ "version": "1.0.23",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49"
+ "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/43523fec10a831ea48bedb3277e3f3fa218f4e49",
- "reference": "43523fec10a831ea48bedb3277e3f3fa218f4e49",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
+ "reference": "15b0cdeab7240bf8e8bffa85ae5275bbc3692bf4",
"shasum": ""
},
"require": {
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
- "time": "2018-10-08T07:53:55+00:00"
+ "time": "2019-06-05T17:18:29+00:00"
},
{
"name": "league/oauth1-client",
},
{
"name": "monolog/monolog",
- "version": "1.24.0",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
- "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/68545165e19249013afd1d6f7485aecff07a2d22",
+ "reference": "68545165e19249013afd1d6f7485aecff07a2d22",
"shasum": ""
},
"require": {
- "php": ">=5.3.0",
- "psr/log": "~1.0"
+ "php": "^7.2",
+ "psr/log": "^1.0.1"
},
"provide": {
"psr/log-implementation": "1.0.0"
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
- "graylog2/gelf-php": "~1.0",
- "jakub-onderka/php-parallel-lint": "0.9",
+ "elasticsearch/elasticsearch": "^6.0",
+ "graylog2/gelf-php": "^1.4.2",
+ "jakub-onderka/php-parallel-lint": "^0.9",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
- "phpunit/phpunit": "~4.5",
- "phpunit/phpunit-mock-objects": "2.3.0",
+ "phpspec/prophecy": "^1.6.1",
+ "phpunit/phpunit": "^8.3",
+ "predis/predis": "^1.1",
+ "rollbar/rollbar": "^1.3",
"ruflin/elastica": ">=0.90 <3.0",
- "sentry/sentry": "^0.13",
"swiftmailer/swiftmailer": "^5.3|^6.0"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
- "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
- "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"php-console/php-console": "Allow sending log messages to Google Chrome",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
- "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
- "sentry/sentry": "Allow sending log messages to a Sentry server"
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.x-dev"
}
},
"autoload": {
"logging",
"psr-3"
],
- "time": "2018-11-05T09:00:11+00:00"
+ "time": "2019-08-30T09:56:44+00:00"
},
{
- "name": "mtdowling/cron-expression",
- "version": "v1.2.1",
+ "name": "mtdowling/jmespath.php",
+ "version": "2.4.0",
"source": {
"type": "git",
- "url": "https://github.com/mtdowling/cron-expression.git",
- "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
+ "url": "https://github.com/jmespath/jmespath.php.git",
+ "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
- "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
+ "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
+ "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
"shasum": ""
},
"require": {
- "php": ">=5.3.2"
+ "php": ">=5.4.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0|~5.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Cron\\": "src/Cron/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Michael Dowling",
- "homepage": "https://github.com/mtdowling"
- }
- ],
- "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
- "keywords": [
- "cron",
- "schedule"
- ],
- "time": "2017-01-23T04:29:33+00:00"
- },
- {
- "name": "mtdowling/jmespath.php",
- "version": "2.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/jmespath/jmespath.php.git",
- "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
- "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "~4.0"
},
"bin": [
"bin/jp.php"
},
{
"name": "nesbot/carbon",
- "version": "1.36.2",
+ "version": "2.24.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
+ "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
- "reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/934459c5ac0658bc765ad1e53512c7c77adcac29",
+ "reference": "934459c5ac0658bc765ad1e53512c7c77adcac29",
"shasum": ""
},
"require": {
- "php": ">=5.3.9",
- "symfony/translation": "~2.6 || ~3.0 || ~4.0"
+ "ext-json": "*",
+ "php": "^7.1.8 || ^8.0",
+ "symfony/translation": "^3.4 || ^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7"
- },
- "suggest": {
- "friendsofphp/php-cs-fixer": "Needed for the `composer phpcs` command. Allow to automatically fix code style.",
- "phpstan/phpstan": "Needed for the `composer phpstan` command. Allow to detect potential errors."
+ "friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
+ "kylekatarnls/multi-tester": "^1.1",
+ "phpmd/phpmd": "dev-php-7.1-compatibility",
+ "phpstan/phpstan": "^0.11",
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "squizlabs/php_codesniffer": "^3.4"
},
+ "bin": [
+ "bin/carbon"
+ ],
"type": "library",
"extra": {
"laravel": {
},
"autoload": {
"psr-4": {
- "": "src/"
+ "Carbon\\": "src/Carbon/"
}
},
"notification-url": "https://packagist.org/downloads/",
"name": "Brian Nesbitt",
"homepage": "http://nesbot.com"
+ },
+ {
+ "name": "kylekatarnls",
+ "homepage": "http://github.com/kylekatarnls"
}
],
- "description": "A simple API extension for DateTime.",
+ "description": "A API extension for DateTime that supports 281 different languages.",
"homepage": "http://carbon.nesbot.com",
"keywords": [
"date",
"datetime",
"time"
],
- "time": "2018-12-28T10:07:33+00:00"
+ "time": "2019-08-31T16:37:55+00:00"
+ },
+ {
+ "name": "opis/closure",
+ "version": "3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/opis/closure.git",
+ "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/opis/closure/zipball/60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+ "reference": "60a97fff133b1669a5b1776aa8ab06db3f3962b7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.4 || ^7.0"
+ },
+ "require-dev": {
+ "jeremeamia/superclosure": "^2.0",
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Opis\\Closure\\": "src/"
+ },
+ "files": [
+ "functions.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marius Sarca",
+ },
+ {
+ "name": "Sorin Sarca",
+ }
+ ],
+ "description": "A library that can be used to serialize closures (anonymous functions) and arbitrary objects.",
+ "homepage": "https://opis.io/closure",
+ "keywords": [
+ "anonymous functions",
+ "closure",
+ "function",
+ "serializable",
+ "serialization",
+ "serialize"
+ ],
+ "time": "2019-09-02T21:07:33+00:00"
},
{
"name": "paragonie/random_compat",
},
{
"name": "phenx/php-svg-lib",
- "version": "v0.3.2",
+ "version": "v0.3.3",
"source": {
"type": "git",
"url": "https://github.com/PhenX/php-svg-lib.git",
- "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c"
+ "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/ccc46ef6340d4b8a4a68047e68d8501ea961442c",
- "reference": "ccc46ef6340d4b8a4a68047e68d8501ea961442c",
+ "url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
+ "reference": "5fa61b65e612ce1ae15f69b3d223cb14ecc60e32",
"shasum": ""
},
"require": {
- "sabberworm/php-css-parser": "8.1.*"
+ "sabberworm/php-css-parser": "^8.3"
},
"require-dev": {
- "phpunit/phpunit": "~5.0"
+ "phpunit/phpunit": "^5.5|^6.5"
},
"type": "library",
"autoload": {
- "psr-0": {
- "Svg\\": "src/"
+ "psr-4": {
+ "Svg\\": "src/Svg"
}
},
"notification-url": "https://packagist.org/downloads/",
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
- "time": "2018-06-03T10:10:03+00:00"
+ "time": "2019-09-11T20:02:13+00:00"
+ },
+ {
+ "name": "phpoption/phpoption",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/schmittjoh/php-option.git",
+ "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+ "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.7.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "PhpOption\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache2"
+ ],
+ "authors": [
+ {
+ "name": "Johannes M. Schmitt",
+ }
+ ],
+ "description": "Option Type for PHP",
+ "keywords": [
+ "language",
+ "option",
+ "php",
+ "type"
+ ],
+ "time": "2015-07-25T16:39:46+00:00"
},
{
"name": "predis/predis",
},
{
"name": "ralouphie/getallheaders",
- "version": "2.0.5",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
- "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa"
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
- "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
- "php": ">=5.3"
+ "php": ">=5.6"
},
"require-dev": {
- "phpunit/phpunit": "~3.7.0",
- "satooshi/php-coveralls": ">=1.0"
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
}
],
"description": "A polyfill for getallheaders.",
- "time": "2016-02-11T07:05:27+00:00"
+ "time": "2019-03-08T08:55:37+00:00"
},
{
"name": "ramsey/uuid",
},
{
"name": "sabberworm/php-css-parser",
- "version": "8.1.0",
+ "version": "8.3.0",
"source": {
"type": "git",
"url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
- "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef"
+ "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/850cbbcbe7fbb155387a151ea562897a67e242ef",
- "reference": "850cbbcbe7fbb155387a151ea562897a67e242ef",
+ "url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
+ "reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
- "phpunit/phpunit": "*"
+ "codacy/coverage": "^1.4",
+ "phpunit/phpunit": "~4.8"
},
"type": "library",
"autoload": {
"parser",
"stylesheet"
],
- "time": "2016-07-19T19:14:21+00:00"
+ "time": "2019-02-22T07:42:52+00:00"
},
{
"name": "socialiteproviders/discord",
},
{
"name": "socialiteproviders/manager",
- "version": "v3.3.4",
+ "version": "v3.4.2",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
- "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
+ "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
- "reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
+ "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e3e8e78b9a3060801cd008941a0894a0a0c479e1",
+ "reference": "e3e8e78b9a3060801cd008941a0894a0a0c479e1",
"shasum": ""
},
"require": {
+ "illuminate/support": "~5.4|~5.7.0|~5.8.0|^6.0",
"laravel/socialite": "~3.0|~4.0",
"php": "^5.6 || ^7.0"
},
{
"name": "Anton Komarev",
+ },
+ {
+ "name": "Miguel Piedrafita",
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
- "time": "2019-01-16T07:58:54+00:00"
+ "time": "2019-09-09T03:07:52+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
},
{
"name": "socialiteproviders/twitch",
- "version": "v3.0.0",
+ "version": "v5.0.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Twitch.git",
- "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93"
+ "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
- "reference": "a7ad148c0b42d0c607d8a034b6e47faf5fc85e93",
+ "url": "https://api.github.com/repos/SocialiteProviders/Twitch/zipball/8c19b26ff24c40cc019413042a5492c5ed21a658",
+ "reference": "8c19b26ff24c40cc019413042a5492c5ed21a658",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~3.0"
+ "socialiteproviders/manager": "~2.0 || ~3.0"
},
"type": "library",
"autoload": {
}
],
"description": "Twitch OAuth2 Provider for Laravel Socialite",
- "time": "2017-01-25T09:48:29+00:00"
+ "time": "2018-06-20T10:59:51+00:00"
},
{
"name": "swiftmailer/swiftmailer",
- "version": "v6.1.3",
+ "version": "v6.2.1",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
- "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4"
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/8ddcb66ac10c392d3beb54829eef8ac1438595f4",
- "reference": "8ddcb66ac10c392d3beb54829eef8ac1438595f4",
+ "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
+ "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a",
"shasum": ""
},
"require": {
"egulias/email-validator": "~2.0",
- "php": ">=7.0.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.3@dev"
+ "symfony/phpunit-bridge": "^3.4.19|^4.1.8"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.1-dev"
+ "dev-master": "6.2-dev"
}
},
"autoload": {
"mail",
"mailer"
],
- "time": "2018-09-11T07:12:52+00:00"
+ "time": "2019-04-21T09:21:45+00:00"
},
{
"name": "symfony/console",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "b0878233cb5c4391347e5495089c7af11b8e6201"
+ "reference": "de63799239b3881b8a08f8481b22348f77ed7b36"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201",
- "reference": "b0878233cb5c4391347e5495089c7af11b8e6201",
+ "url": "https://api.github.com/repos/symfony/console/zipball/de63799239b3881b8a08f8481b22348f77ed7b36",
+ "reference": "de63799239b3881b8a08f8481b22348f77ed7b36",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/debug": "~2.8|~3.0",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php73": "^1.8",
+ "symfony/service-contracts": "^1.1"
},
"conflict": {
- "symfony/dependency-injection": "<3.3"
+ "symfony/dependency-injection": "<3.4",
+ "symfony/event-dispatcher": "<4.3",
+ "symfony/process": "<3.3"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~3.3",
- "symfony/dependency-injection": "~3.3",
- "symfony/event-dispatcher": "~2.8|~3.0",
- "symfony/filesystem": "~2.8|~3.0",
- "symfony/http-kernel": "~2.8|~3.0",
- "symfony/process": "~2.8|~3.0"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/lock": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
+ "symfony/var-dumper": "^4.3"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
- "symfony/filesystem": "",
+ "symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2017-07-29T21:27:59+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v3.1.10",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d"
+ "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
- "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
+ "reference": "c6e5e2a00db768c92c3ae131532af4e1acc7bd03",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
"MIT"
],
"authors": [
- {
- "name": "Jean-François Simon",
- },
{
"name": "Fabien Potencier",
},
+ {
+ "name": "Jean-François Simon",
+ },
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
- "time": "2017-01-02T20:31:54+00:00"
+ "time": "2019-08-20T14:07:54+00:00"
},
{
"name": "symfony/debug",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
- "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13"
+ "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
- "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/afcdea44a2e399c1e4b52246ec8d54c715393ced",
+ "reference": "afcdea44a2e399c1e4b52246ec8d54c715393ced",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
"psr/log": "~1.0"
},
"conflict": {
- "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+ "symfony/http-kernel": "<3.4"
},
"require-dev": {
- "symfony/http-kernel": "~2.8|~3.0"
+ "symfony/http-kernel": "~3.4|~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
- "time": "2017-07-28T15:27:31+00:00"
+ "time": "2019-08-20T14:27:59+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e"
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e",
- "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
+ "reference": "429d0a1451d4c9c4abe1959b2986b88794b9b7d2",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3",
+ "symfony/event-dispatcher-contracts": "^1.1"
},
"conflict": {
- "symfony/dependency-injection": "<3.3"
+ "symfony/dependency-injection": "<3.4"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "1.1"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/stopwatch": "~2.8|~3.0"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/http-foundation": "^3.4|^4.0",
+ "symfony/service-contracts": "^1.1",
+ "symfony/stopwatch": "~3.4|~4.0"
},
"suggest": {
"symfony/dependency-injection": "",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
- "time": "2017-06-09T14:53:08+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c61766f4440ca687de1084a5c00b08e167a2575c",
+ "reference": "c61766f4440ca687de1084a5c00b08e167a2575c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "psr/event-dispatcher": "",
+ "symfony/event-dispatcher-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "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 dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-06-20T06:46:26+00:00"
},
{
"name": "symfony/finder",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+ "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
+ "reference": "86c1c929f0a4b24812e1eb109262fc3372c8e9f2",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
- "time": "2017-06-01T21:01:25+00:00"
+ "time": "2019-08-14T12:26:46+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031"
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031",
- "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d804bea118ff340a12e22a79f9c7e7eb56b35adc",
+ "reference": "d804bea118ff340a12e22a79f9c7e7eb56b35adc",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
+ "symfony/mime": "^4.3",
"symfony/polyfill-mbstring": "~1.1"
},
"require-dev": {
- "symfony/expression-language": "~2.8|~3.0"
+ "predis/predis": "~1.0",
+ "symfony/expression-language": "~3.4|~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
- "time": "2017-07-21T11:04:46+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5"
+ "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db10d05f1d95e4168e638db7a81c79616f568ea5",
- "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/5e0fc71be03d52cd00c423061cfd300bd6f92a52",
+ "reference": "5e0fc71be03d52cd00c423061cfd300bd6f92a52",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
"psr/log": "~1.0",
- "symfony/debug": "~2.8|~3.0",
- "symfony/event-dispatcher": "~2.8|~3.0",
- "symfony/http-foundation": "~3.3"
+ "symfony/debug": "~3.4|~4.0",
+ "symfony/event-dispatcher": "^4.3",
+ "symfony/http-foundation": "^4.1.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-php73": "^1.9"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/dependency-injection": "<3.3",
- "symfony/var-dumper": "<3.3",
+ "symfony/browser-kit": "<4.3",
+ "symfony/config": "<3.4",
+ "symfony/dependency-injection": "<4.3",
+ "symfony/translation": "<4.2",
+ "symfony/var-dumper": "<4.1.1",
"twig/twig": "<1.34|<2.4,>=2"
},
+ "provide": {
+ "psr/log-implementation": "1.0"
+ },
"require-dev": {
"psr/cache": "~1.0",
- "symfony/browser-kit": "~2.8|~3.0",
- "symfony/class-loader": "~2.8|~3.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/console": "~2.8|~3.0",
- "symfony/css-selector": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/dom-crawler": "~2.8|~3.0",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/finder": "~2.8|~3.0",
- "symfony/process": "~2.8|~3.0",
- "symfony/routing": "~2.8|~3.0",
- "symfony/stopwatch": "~2.8|~3.0",
- "symfony/templating": "~2.8|~3.0",
- "symfony/translation": "~2.8|~3.0",
- "symfony/var-dumper": "~3.3"
+ "symfony/browser-kit": "^4.3",
+ "symfony/config": "~3.4|~4.0",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/css-selector": "~3.4|~4.0",
+ "symfony/dependency-injection": "^4.3",
+ "symfony/dom-crawler": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/finder": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
+ "symfony/routing": "~3.4|~4.0",
+ "symfony/stopwatch": "~3.4|~4.0",
+ "symfony/templating": "~3.4|~4.0",
+ "symfony/translation": "~4.2",
+ "symfony/translation-contracts": "^1.1",
+ "symfony/var-dumper": "^4.1.1",
+ "twig/twig": "^1.34|^2.4"
},
"suggest": {
"symfony/browser-kit": "",
- "symfony/class-loader": "",
"symfony/config": "",
"symfony/console": "",
"symfony/dependency-injection": "",
- "symfony/finder": "",
"symfony/var-dumper": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
- "time": "2017-08-01T10:25:59+00:00"
+ "time": "2019-08-26T16:47:42+00:00"
},
{
- "name": "symfony/polyfill-ctype",
- "version": "v1.10.0",
+ "name": "symfony/mime",
+ "version": "v4.3.4",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+ "url": "https://github.com/symfony/mime.git",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/987a05df1c6ac259b34008b932551353f4f408df",
+ "reference": "987a05df1c6ac259b34008b932551353f4f408df",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": "^7.1.3",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
},
- "suggest": {
- "ext-ctype": "For best performance"
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10",
+ "symfony/dependency-injection": "~3.4|^4.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.9-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Ctype\\": ""
+ "Symfony\\Component\\Mime\\": ""
},
- "files": [
- "bootstrap.php"
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
],
"authors": [
{
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
+ "name": "Fabien Potencier",
},
{
- "name": "Gert de Pagter",
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill for ctype functions",
+ "description": "A library to manipulate MIME messages",
"homepage": "https://symfony.com",
"keywords": [
- "compatibility",
- "ctype",
- "polyfill",
- "portable"
+ "mime",
+ "mime-type"
],
- "time": "2018-08-06T14:22:27+00:00"
+ "time": "2019-08-22T08:16:11+00:00"
},
{
- "name": "symfony/polyfill-mbstring",
- "version": "v1.10.0",
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.12.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
- "ext-mbstring": "For best performance"
+ "ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.9-dev"
+ "dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
+ "Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
],
"authors": [
{
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony polyfill for the Mbstring extension",
+ "description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
- "mbstring",
+ "ctype",
"polyfill",
- "portable",
- "shim"
+ "portable"
],
- "time": "2018-09-21T13:07:52+00:00"
+ "time": "2019-08-06T08:03:45+00:00"
},
{
- "name": "symfony/process",
- "version": "v3.3.6",
+ "name": "symfony/polyfill-iconv",
+ "version": "v1.12.0",
"source": {
"type": "git",
- "url": "https://github.com/symfony/process.git",
- "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a"
+ "url": "https://github.com/symfony/polyfill-iconv.git",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a",
- "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a",
+ "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f",
+ "reference": "685968b11e61a347c18bf25db32effa478be610f",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "1.12-dev"
}
},
"autoload": {
"psr-4": {
- "Symfony\\Component\\Process\\": ""
+ "Symfony\\Polyfill\\Iconv\\": ""
},
- "exclude-from-classmap": [
- "/Tests/"
+ "files": [
+ "bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
],
"authors": [
{
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Process Component",
+ "description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
- "time": "2017-07-13T13:05:09+00:00"
+ "keywords": [
+ "compatibility",
+ "iconv",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php72": "^1.9"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "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 for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php72",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e",
+ "reference": "04ce3335667451138df4307d6a9b61565560199e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php72\\": ""
+ },
+ "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"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php73",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php73.git",
+ "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/2ceb49eaccb9352bff54d22570276bb75ba4a188",
+ "reference": "2ceb49eaccb9352bff54d22570276bb75ba4a188",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php73\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ],
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "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.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v4.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/e89969c00d762349f078db1128506f7f3dcc0d4a",
+ "reference": "e89969c00d762349f078db1128506f7f3dcc0d4a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/routing",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26"
+ "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
- "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
+ "reference": "ff1049f6232dc5b6023b1ff1c6de56f82bcd264f",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/dependency-injection": "<3.3",
- "symfony/yaml": "<3.3"
+ "symfony/config": "<4.2",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/yaml": "<3.4"
},
"require-dev": {
- "doctrine/annotations": "~1.0",
- "doctrine/common": "~2.2",
+ "doctrine/annotations": "~1.2",
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/dependency-injection": "~3.3",
- "symfony/expression-language": "~2.8|~3.0",
- "symfony/http-foundation": "~2.8|~3.0",
- "symfony/yaml": "~3.3"
+ "symfony/config": "~4.2",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/expression-language": "~3.4|~4.0",
+ "symfony/http-foundation": "~3.4|~4.0",
+ "symfony/yaml": "~3.4|~4.0"
},
"suggest": {
"doctrine/annotations": "For using the annotation loader",
"symfony/config": "For using the all-in-one router or any loader",
- "symfony/dependency-injection": "For loading routes from a service",
"symfony/expression-language": "For using expression matching",
"symfony/http-foundation": "For using a Symfony Request object",
"symfony/yaml": "For using the YAML loader"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
"uri",
"url"
],
- "time": "2017-07-21T17:43:13+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+ "reference": "ea7263d6b6d5f798b56a45a5b8d686725f2719a3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "psr/container": "^1.0"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ }
+ },
+ "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 writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-08-20T14:44:19+00:00"
},
{
"name": "symfony/translation",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3"
+ "reference": "28498169dd334095fa981827992f3a24d50fed0f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
- "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/28498169dd334095fa981827992f3a24d50fed0f",
+ "reference": "28498169dd334095fa981827992f3a24d50fed0f",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/translation-contracts": "^1.1.6"
},
"conflict": {
- "symfony/config": "<2.8",
- "symfony/yaml": "<3.3"
+ "symfony/config": "<3.4",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/yaml": "<3.4"
+ },
+ "provide": {
+ "symfony/translation-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
- "symfony/config": "~2.8|~3.0",
- "symfony/intl": "^2.8.18|^3.2.5",
- "symfony/yaml": "~3.3"
+ "symfony/config": "~3.4|~4.0",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/finder": "~2.8|~3.0|~4.0",
+ "symfony/http-kernel": "~3.4|~4.0",
+ "symfony/intl": "~3.4|~4.0",
+ "symfony/service-contracts": "^1.1.2",
+ "symfony/var-dumper": "~3.4|~4.0",
+ "symfony/yaml": "~3.4|~4.0"
},
"suggest": {
- "psr/log": "To use logging capability in translator",
+ "psr/log-implementation": "To use logging capability in translator",
"symfony/config": "",
"symfony/yaml": ""
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
- "time": "2017-06-24T16:45:30+00:00"
+ "time": "2019-08-26T08:55:16+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+ "reference": "325b17c24f3ee23cbecfa63ba809c6d89b5fa04a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "suggest": {
+ "symfony/translation-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ }
+ },
+ "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 translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "time": "2019-08-02T12:15:04+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "b2623bccb969ad595c2090f9be498b74670d0663"
+ "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663",
- "reference": "b2623bccb969ad595c2090f9be498b74670d0663",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/641043e0f3e615990a0f29479f9c117e8a6698c6",
+ "reference": "641043e0f3e615990a0f29479f9c117e8a6698c6",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "symfony/polyfill-mbstring": "~1.0"
+ "php": "^7.1.3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/polyfill-php72": "~1.5"
},
"conflict": {
- "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
+ "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0",
+ "symfony/console": "<3.4"
},
"require-dev": {
"ext-iconv": "*",
+ "symfony/console": "~3.4|~4.0",
+ "symfony/process": "~3.4|~4.0",
"twig/twig": "~1.34|~2.4"
},
"suggest": {
"ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).",
- "ext-symfony_debug": ""
+ "ext-intl": "To show region name in time zone dump",
+ "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script"
},
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
"debug",
"dump"
],
- "time": "2017-07-28T06:06:09+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
},
{
"name": "vlucas/phpdotenv",
- "version": "v2.5.2",
+ "version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
+ "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
- "reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+ "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
"shasum": ""
},
"require": {
- "php": ">=5.3.9"
+ "php": "^5.4 || ^7.0",
+ "phpoption/phpoption": "^1.5",
+ "symfony/polyfill-ctype": "^1.9"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.0"
+ "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev"
+ "dev-master": "3.6-dev"
}
},
"autoload": {
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Graham Campbell",
+ "homepage": "https://gjcampbell.co.uk/"
+ },
{
"name": "Vance Lucas",
- "homepage": "http://www.vancelucas.com"
+ "homepage": "https://vancelucas.com/"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"env",
"environment"
],
- "time": "2018-10-30T17:29:25+00:00"
+ "time": "2019-09-10T21:37:39+00:00"
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
- "version": "v3.2.1",
+ "version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
- "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c"
+ "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/9d5caf43c5f3a3aea2178942f281054805872e7c",
- "reference": "9d5caf43c5f3a3aea2178942f281054805872e7c",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/18208d64897ab732f6c04a19b319fe8f1d57a9c0",
+ "reference": "18208d64897ab732f6c04a19b319fe8f1d57a9c0",
"shasum": ""
},
"require": {
- "illuminate/routing": "5.5.x|5.6.x|5.7.x",
- "illuminate/session": "5.5.x|5.6.x|5.7.x",
- "illuminate/support": "5.5.x|5.6.x|5.7.x",
+ "illuminate/routing": "^5.5|^6",
+ "illuminate/session": "^5.5|^6",
+ "illuminate/support": "^5.5|^6",
"maximebf/debugbar": "~1.15.0",
"php": ">=7.0",
"symfony/debug": "^3|^4",
"profiler",
"webprofiler"
],
- "time": "2018-11-09T08:37:55+00:00"
+ "time": "2019-08-29T07:01:03+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
- "version": "v2.5.3",
+ "version": "v2.6.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
- "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2"
+ "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/3d7f1240896a075aa23b13f82dfcbe165dadeef2",
- "reference": "3d7f1240896a075aa23b13f82dfcbe165dadeef2",
+ "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
+ "reference": "8740a9a158d3dd5cfc706a9d4cc1bf7a518f99f3",
"shasum": ""
},
"require": {
"barryvdh/reflection-docblock": "^2.0.6",
"composer/composer": "^1.6",
- "illuminate/console": "^5.5,<5.8",
- "illuminate/filesystem": "^5.5,<5.8",
- "illuminate/support": "^5.5,<5.8",
+ "doctrine/dbal": "~2.3",
+ "illuminate/console": "^5.5|^6",
+ "illuminate/filesystem": "^5.5|^6",
+ "illuminate/support": "^5.5|^6",
"php": ">=7"
},
"require-dev": {
- "doctrine/dbal": "~2.3",
- "illuminate/config": "^5.1,<5.8",
- "illuminate/view": "^5.1,<5.8",
+ "illuminate/config": "^5.5|^6",
+ "illuminate/view": "^5.5|^6",
"phpro/grumphp": "^0.14",
"phpunit/phpunit": "4.*",
"scrutinizer/ocular": "~1.1",
"squizlabs/php_codesniffer": "^3"
},
- "suggest": {
- "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
- },
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.5-dev"
+ "dev-master": "2.6-dev"
},
"laravel": {
"providers": [
"phpstorm",
"sublime"
],
- "time": "2018-12-19T12:12:05+00:00"
+ "time": "2019-09-08T09:56:38+00:00"
},
{
"name": "barryvdh/reflection-docblock",
},
{
"name": "composer/ca-bundle",
- "version": "1.1.3",
+ "version": "1.2.4",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
- "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660"
+ "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660",
- "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
+ "reference": "10bb96592168a0f8e8f6dcde3532d9fa50b0b527",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
- "php": "^5.3.2 || ^7.0"
+ "php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
"psr/log": "^1.0",
"symfony/process": "^2.5 || ^3.0 || ^4.0"
},
"ssl",
"tls"
],
- "time": "2018-10-18T06:09:13+00:00"
+ "time": "2019-08-30T08:44:50+00:00"
},
{
"name": "composer/composer",
- "version": "1.8.0",
+ "version": "1.9.0",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
- "reference": "d8aef3af866b28786ce9b8647e52c42496436669"
+ "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669",
- "reference": "d8aef3af866b28786ce9b8647e52c42496436669",
+ "url": "https://api.github.com/repos/composer/composer/zipball/314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
+ "reference": "314aa57fdcfc942065996f59fb73a8b3f74f3fa5",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.8-dev"
+ "dev-master": "1.9-dev"
}
},
"autoload": {
"homepage": "http://seld.be"
}
],
- "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.",
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
"homepage": "https://getcomposer.org/",
"keywords": [
"autoload",
"dependency",
"package"
],
- "time": "2018-12-03T09:31:16+00:00"
+ "time": "2019-08-02T18:55:33+00:00"
},
{
"name": "composer/semver",
- "version": "1.4.2",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573"
+ "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573",
- "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573",
+ "url": "https://api.github.com/repos/composer/semver/zipball/46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
+ "reference": "46d9139568ccb8d9e7cdd4539cab7347568a5e2e",
"shasum": ""
},
"require": {
"homepage": "http://robbast.nl"
}
],
- "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "time": "2019-03-19T17:25:45+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7ac1e6aec371357df067f8a688c3d6974df68fa5",
+ "reference": "7ac1e6aec371357df067f8a688c3d6974df68fa5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Spdx\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "SPDX licenses list and validation library.",
+ "keywords": [
+ "license",
+ "spdx",
+ "validator"
+ ],
+ "time": "2019-07-29T10:31:59+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/46867cbf8ca9fb8d60c506895449eb799db1184f",
+ "reference": "46867cbf8ca9fb8d60c506895449eb799db1184f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ }
+ ],
+ "description": "Restarts a process without xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "time": "2019-05-27T17:52:04+00:00"
+ },
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
- "semantic",
- "semver",
- "validation",
- "versioning"
+ "constructor",
+ "instantiate"
],
- "time": "2016-08-30T16:08:34+00:00"
+ "time": "2019-03-17T17:37:11+00:00"
},
{
- "name": "composer/spdx-licenses",
- "version": "1.5.0",
+ "name": "facade/flare-client-php",
+ "version": "1.0.4",
"source": {
"type": "git",
- "url": "https://github.com/composer/spdx-licenses.git",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2"
+ "url": "https://github.com/facade/flare-client-php.git",
+ "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
- "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2",
+ "url": "https://api.github.com/repos/facade/flare-client-php/zipball/7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
+ "reference": "7128b251b48f24ef64e5cddd7f8d40cc3a06fd3e",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0"
+ "facade/ignition-contracts": "~1.0",
+ "illuminate/pipeline": "~5.5|~5.6|~5.7|~5.8|^6.0",
+ "php": "^7.1",
+ "symfony/http-foundation": "~3.3|~4.1",
+ "symfony/var-dumper": "^3.4|^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
- "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0"
+ "larapack/dd": "^1.1",
+ "phpunit/phpunit": "^7.0",
+ "spatie/phpunit-snapshot-assertions": "^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.x-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
- "Composer\\Spdx\\": "src"
- }
+ "Facade\\FlareClient\\": "src"
+ },
+ "files": [
+ "src/helpers.php"
+ ]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "Nils Adermann",
- "homepage": "http://www.naderman.de"
- },
- {
- "name": "Jordi Boggiano",
- "homepage": "http://seld.be"
- },
- {
- "name": "Rob Bast",
- "homepage": "http://robbast.nl"
- }
- ],
- "description": "SPDX licenses list and validation library.",
+ "description": "Send PHP errors to Flare",
+ "homepage": "https://github.com/facade/flare-client-php",
"keywords": [
- "license",
- "spdx",
- "validator"
+ "exception",
+ "facade",
+ "flare",
+ "reporting"
],
- "time": "2018-11-01T09:45:54+00:00"
+ "time": "2019-09-11T14:19:56+00:00"
},
{
- "name": "composer/xdebug-handler",
- "version": "1.3.1",
+ "name": "facade/ignition",
+ "version": "1.6.5",
"source": {
"type": "git",
- "url": "https://github.com/composer/xdebug-handler.git",
- "reference": "dc523135366eb68f22268d069ea7749486458562"
+ "url": "https://github.com/facade/ignition.git",
+ "reference": "97244f6d511332f3574acab8242c09ddcfda892b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562",
- "reference": "dc523135366eb68f22268d069ea7749486458562",
+ "url": "https://api.github.com/repos/facade/ignition/zipball/97244f6d511332f3574acab8242c09ddcfda892b",
+ "reference": "97244f6d511332f3574acab8242c09ddcfda892b",
"shasum": ""
},
"require": {
- "php": "^5.3.2 || ^7.0",
- "psr/log": "^1.0"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "facade/flare-client-php": "^1.0.4",
+ "facade/ignition-contracts": "^1.0",
+ "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",
+ "scrivo/highlight.php": "^9.15",
+ "symfony/console": "^3.4 || ^4.0",
+ "symfony/var-dumper": "^3.4 || ^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
+ "friendsofphp/php-cs-fixer": "^2.14",
+ "mockery/mockery": "^1.2",
+ "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0"
+ },
+ "suggest": {
+ "laravel/telescope": "^2.0"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Facade\\Ignition\\IgnitionServiceProvider"
+ ],
+ "aliases": {
+ "Flare": "Facade\\Ignition\\Facades\\Flare"
+ }
+ }
+ },
"autoload": {
"psr-4": {
- "Composer\\XdebugHandler\\": "src"
+ "Facade\\Ignition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "John Stevenson",
- }
- ],
- "description": "Restarts a process without xdebug.",
+ "description": "A beautiful error page for Laravel applications.",
+ "homepage": "https://github.com/facade/ignition",
"keywords": [
- "Xdebug",
- "performance"
+ "error",
+ "flare",
+ "laravel",
+ "page"
],
- "time": "2018-11-29T10:59:02+00:00"
+ "time": "2019-09-13T13:38:04+00:00"
},
{
- "name": "doctrine/instantiator",
- "version": "1.0.5",
+ "name": "facade/ignition-contracts",
+ "version": "1.0.0",
"source": {
"type": "git",
- "url": "https://github.com/doctrine/instantiator.git",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+ "url": "https://github.com/facade/ignition-contracts.git",
+ "reference": "f445db0fb86f48e205787b2592840dd9c80ded28"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "url": "https://api.github.com/repos/facade/ignition-contracts/zipball/f445db0fb86f48e205787b2592840dd9c80ded28",
+ "reference": "f445db0fb86f48e205787b2592840dd9c80ded28",
"shasum": ""
},
"require": {
- "php": ">=5.3,<8.0-DEV"
- },
- "require-dev": {
- "athletic/athletic": "~0.1.8",
- "ext-pdo": "*",
- "ext-phar": "*",
- "phpunit/phpunit": "~4.0",
- "squizlabs/php_codesniffer": "~2.0"
+ "php": "^7.1"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
"autoload": {
"psr-4": {
- "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ "Facade\\IgnitionContracts\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
],
"authors": [
{
- "name": "Marco Pivetta",
- "homepage": "http://ocramius.github.com/"
+ "name": "Freek Van der Herten",
+ "homepage": "https://flareapp.io",
+ "role": "Developer"
}
],
- "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://github.com/doctrine/instantiator",
+ "description": "Solution contracts for Ignition",
+ "homepage": "https://github.com/facade/ignition-contracts",
"keywords": [
- "constructor",
- "instantiate"
+ "contracts",
+ "flare",
+ "ignition"
],
- "time": "2015-06-14T21:17:01+00:00"
+ "time": "2019-08-30T14:06:08+00:00"
},
{
"name": "filp/whoops",
- "version": "2.3.1",
+ "version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
- "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7"
+ "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/filp/whoops/zipball/bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
- "reference": "bc0fd11bc455cc20ee4b5edabc63ebbf859324c7",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/cde50e6720a39fdacb240159d3eea6865d51fd96",
+ "reference": "cde50e6720a39fdacb240159d3eea6865d51fd96",
"shasum": ""
},
"require": {
"authors": [
{
"name": "Filipe Dobreira",
- "homepage": "https://github.com/filp",
- "role": "Developer"
+ "role": "Developer",
+ "homepage": "https://github.com/filp"
}
],
"description": "php error handling for cool kids",
"throwable",
"whoops"
],
- "time": "2018-10-23T09:00:00+00:00"
+ "time": "2019-08-07T09:00:00+00:00"
},
{
"name": "fzaninotto/faker",
],
"time": "2016-01-20T08:20:44+00:00"
},
+ {
+ "name": "jakub-onderka/php-console-color",
+ "version": "v0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Color.git",
+ "reference": "d5deaecff52a0d61ccb613bb3804088da0307191"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191",
+ "reference": "d5deaecff52a0d61ccb613bb3804088da0307191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "1.0",
+ "jakub-onderka/php-parallel-lint": "1.0",
+ "jakub-onderka/php-var-dump-check": "0.*",
+ "phpunit/phpunit": "~4.3",
+ "squizlabs/php_codesniffer": "1.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "JakubOnderka\\PhpConsoleColor\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ }
+ ],
+ "time": "2018-09-29T17:23:10+00:00"
+ },
+ {
+ "name": "jakub-onderka/php-console-highlighter",
+ "version": "v0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git",
+ "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547",
+ "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "jakub-onderka/php-console-color": "~0.2",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-code-style": "~1.0",
+ "jakub-onderka/php-parallel-lint": "~1.0",
+ "jakub-onderka/php-var-dump-check": "~0.1",
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "~1.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "JakubOnderka\\PhpConsoleHighlighter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "homepage": "http://www.acci.cz/"
+ }
+ ],
+ "description": "Highlight PHP code in terminal",
+ "time": "2018-09-29T18:48:56+00:00"
+ },
{
"name": "justinrainbow/json-schema",
"version": "5.2.8",
},
{
"name": "laravel/browser-kit-testing",
- "version": "v2.0.1",
+ "version": "v5.1.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/browser-kit-testing.git",
- "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9"
+ "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
- "reference": "f0bb9f200ec35f9d876ded6eacfbc60868d311b9",
+ "url": "https://api.github.com/repos/laravel/browser-kit-testing/zipball/cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
+ "reference": "cb0cf22cf38fe8796842adc8b9ad550ded2a1377",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
- "phpunit/phpunit": "~6.0",
- "symfony/css-selector": "~3.1",
- "symfony/dom-crawler": "~3.1"
+ "ext-dom": "*",
+ "ext-json": "*",
+ "illuminate/contracts": "~5.7.0|~5.8.0|^6.0",
+ "illuminate/database": "~5.7.0|~5.8.0|^6.0",
+ "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.0|^8.0",
+ "symfony/console": "^4.2",
+ "symfony/css-selector": "^4.2",
+ "symfony/dom-crawler": "^4.2",
+ "symfony/http-foundation": "^4.2",
+ "symfony/http-kernel": "^4.2"
+ },
+ "require-dev": {
+ "laravel/framework": "~5.7.0|~5.8.0|^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
}
],
- "description": "Provides backwards compatibility for BrowserKit testing in Laravel 5.4.",
+ "description": "Provides backwards compatibility for BrowserKit testing in the latest Laravel release.",
"keywords": [
"laravel",
"testing"
],
- "time": "2017-06-21T11:44:53+00:00"
+ "time": "2019-07-30T14:57:44+00:00"
},
{
"name": "maximebf/debugbar",
},
{
"name": "mockery/mockery",
- "version": "1.2.0",
+ "version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
- "reference": "100633629bf76d57430b86b7098cd6beb996a35a"
+ "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mockery/mockery/zipball/100633629bf76d57430b86b7098cd6beb996a35a",
- "reference": "100633629bf76d57430b86b7098cd6beb996a35a",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/4eff936d83eb809bde2c57a3cea0ee9643769031",
+ "reference": "4eff936d83eb809bde2c57a3cea0ee9643769031",
"shasum": ""
},
"require": {
"php": ">=5.6.0"
},
"require-dev": {
- "phpunit/phpunit": "~5.7.10|~6.5|~7.0"
+ "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0"
},
"type": "library",
"extra": {
"test double",
"testing"
],
- "time": "2018-10-02T21:52:37+00:00"
+ "time": "2019-08-07T15:01:07+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.7.0",
+ "version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.1"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
- "phpunit/phpunit": "^4.1"
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"autoload": {
"object",
"object graph"
],
- "time": "2017-10-19T19:58:43+00:00"
+ "time": "2019-08-09T12:45:53+00:00"
+ },
+ {
+ "name": "nunomaduro/collision",
+ "version": "v3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nunomaduro/collision.git",
+ "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+ "reference": "af42d339fe2742295a54f6fdd42aaa6f8c4aca68",
+ "shasum": ""
+ },
+ "require": {
+ "filp/whoops": "^2.1.4",
+ "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*",
+ "php": "^7.1",
+ "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"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "NunoMaduro\\Collision\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ }
+ ],
+ "description": "Cli error handling for console/command-line PHP applications.",
+ "keywords": [
+ "artisan",
+ "cli",
+ "command-line",
+ "console",
+ "error",
+ "handling",
+ "laravel",
+ "laravel-zero",
+ "php",
+ "symfony"
+ ],
+ "time": "2019-03-07T21:35:13+00:00"
},
{
"name": "phar-io/manifest",
- "version": "1.0.1",
+ "version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
- "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
- "phar-io/version": "^1.0.1",
+ "phar-io/version": "^2.0",
"php": "^5.6 || ^7.0"
},
"type": "library",
"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": "2017-03-05T18:14:27+00:00"
+ "time": "2018-07-08T19:23:20+00:00"
},
{
"name": "phar-io/version",
- "version": "1.0.1",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
- "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
- "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"shasum": ""
},
"require": {
}
],
"description": "Library for handling version information and constraints",
- "time": "2017-03-05T17:38:23+00:00"
+ "time": "2018-07-08T19:19:57+00:00"
},
{
"name": "phpdocumentor/reflection-common",
- "version": "1.0.1",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
"shasum": ""
},
"require": {
- "php": ">=5.5"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.6"
+ "phpunit/phpunit": "~6"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src"
- ]
+ "phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"reflection",
"static analysis"
],
- "time": "2017-09-11T18:02:19+00:00"
+ "time": "2018-08-07T13:53:10+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "4.3.0",
+ "version": "4.3.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "94fd0001232e47129dd3504189fa1c7225010d08"
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08",
- "reference": "94fd0001232e47129dd3504189fa1c7225010d08",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
"shasum": ""
},
"require": {
"php": "^7.0",
- "phpdocumentor/reflection-common": "^1.0.0",
- "phpdocumentor/type-resolver": "^0.4.0",
+ "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+ "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
- "doctrine/instantiator": "~1.0.5",
+ "doctrine/instantiator": "^1.0.5",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^6.4"
},
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2017-11-30T07:14:17+00:00"
+ "time": "2019-09-12T14:27:41+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "0.4.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
- "phpdocumentor/reflection-common": "^1.0"
+ "php": "^7.1",
+ "phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
- "mockery/mockery": "^0.9.4",
- "phpunit/phpunit": "^5.2||^4.8.24"
+ "ext-tokenizer": "^7.1",
+ "mockery/mockery": "~1",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src/"
- ]
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "time": "2019-08-22T18:11:29+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"
}
},
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "Mike van Riel",
+ "name": "Sebastian Bergmann",
+ "role": "lead"
}
],
- "time": "2017-07-14T14:27:02+00:00"
+ "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"
},
{
"name": "phpspec/prophecy",
- "version": "1.8.0",
+ "version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
+ "reference": "1927e75f4ed19131ec9bcc3b002e07fb1173ee76",
"shasum": ""
},
"require": {
}
},
"autoload": {
- "psr-0": {
- "Prophecy\\": "src/"
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
}
},
"notification-url": "https://packagist.org/downloads/",
"spy",
"stub"
],
- "time": "2018-08-05T17:53:17+00:00"
+ "time": "2019-06-13T12:50:23+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "5.3.2",
+ "version": "7.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
+ "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
- "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
+ "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
- "php": "^7.0",
- "phpunit/php-file-iterator": "^1.4.2",
+ "php": "^7.2",
+ "phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
- "phpunit/php-token-stream": "^2.0.1",
+ "phpunit/php-token-stream": "^3.1.0",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
- "sebastian/environment": "^3.0",
+ "sebastian/environment": "^4.2.2",
"sebastian/version": "^2.0.1",
- "theseer/tokenizer": "^1.1"
+ "theseer/tokenizer": "^1.1.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^8.2.2"
},
"suggest": {
- "ext-xdebug": "^2.5.5"
+ "ext-xdebug": "^2.7.2"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.3.x-dev"
+ "dev-master": "7.0-dev"
}
},
"autoload": {
"testing",
"xunit"
],
- "time": "2018-04-06T15:36:58+00:00"
+ "time": "2019-07-25T05:31:54+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "1.4.5",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+ "reference": "050bedf145a257b1ff02746c31894800e5122946"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+ "reference": "050bedf145a257b1ff02746c31894800e5122946",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
"filesystem",
"iterator"
],
- "time": "2017-11-27T13:52:08+00:00"
+ "time": "2018-09-13T20:33:42+00:00"
},
{
"name": "phpunit/php-text-template",
},
{
"name": "phpunit/php-timer",
- "version": "1.0.9",
+ "version": "2.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "2.1-dev"
}
},
"autoload": {
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
"keywords": [
"timer"
],
- "time": "2017-02-26T11:10:40+00:00"
+ "time": "2019-06-07T04:22:29+00:00"
},
{
"name": "phpunit/php-token-stream",
- "version": "2.0.2",
+ "version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "791198a2c6254db10131eecfe8c06670700904db"
+ "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
- "reference": "791198a2c6254db10131eecfe8c06670700904db",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
+ "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.2.4"
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.1-dev"
}
},
"autoload": {
"keywords": [
"tokenizer"
],
- "time": "2017-11-27T05:48:46+00:00"
+ "time": "2019-07-25T05:29:42+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "6.5.13",
+ "version": "8.3.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "0973426fb012359b2f18d3bd1e90ef1172839693"
+ "reference": "302faed7059fde575cf3403a78c730c5e3a62750"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693",
- "reference": "0973426fb012359b2f18d3bd1e90ef1172839693",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/302faed7059fde575cf3403a78c730c5e3a62750",
+ "reference": "302faed7059fde575cf3403a78c730c5e3a62750",
"shasum": ""
},
"require": {
+ "doctrine/instantiator": "^1.2.0",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
- "myclabs/deep-copy": "^1.6.1",
- "phar-io/manifest": "^1.0.1",
- "phar-io/version": "^1.0",
- "php": "^7.0",
- "phpspec/prophecy": "^1.7",
- "phpunit/php-code-coverage": "^5.3",
- "phpunit/php-file-iterator": "^1.4.3",
+ "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",
+ "phpunit/php-file-iterator": "^2.0.2",
"phpunit/php-text-template": "^1.2.1",
- "phpunit/php-timer": "^1.0.9",
- "phpunit/phpunit-mock-objects": "^5.0.9",
- "sebastian/comparator": "^2.1",
- "sebastian/diff": "^2.0",
- "sebastian/environment": "^3.1",
- "sebastian/exporter": "^3.1",
- "sebastian/global-state": "^2.0",
+ "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/global-state": "^3.0.0",
"sebastian/object-enumerator": "^3.0.3",
- "sebastian/resource-operations": "^1.0",
+ "sebastian/resource-operations": "^2.0.1",
+ "sebastian/type": "^1.1.3",
"sebastian/version": "^2.0.1"
},
- "conflict": {
- "phpdocumentor/reflection-docblock": "3.0.2",
- "phpunit/dbunit": "<3.0"
- },
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
+ "ext-soap": "*",
"ext-xdebug": "*",
- "phpunit/php-invoker": "^1.1"
+ "phpunit/php-invoker": "^2.0.0"
},
"bin": [
"phpunit"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.5.x-dev"
+ "dev-master": "8.3-dev"
}
},
"autoload": {
"testing",
"xunit"
],
- "time": "2018-09-08T15:10:43+00:00"
+ "time": "2019-09-14T09:12:03+00:00"
},
{
- "name": "phpunit/phpunit-mock-objects",
- "version": "5.0.10",
+ "name": "scrivo/highlight.php",
+ "version": "v9.15.10.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
+ "url": "https://github.com/scrivo/highlight.php.git",
+ "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
- "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
+ "url": "https://api.github.com/repos/scrivo/highlight.php/zipball/9ad3adb4456dc91196327498dbbce6aa1ba1239e",
+ "reference": "9ad3adb4456dc91196327498dbbce6aa1ba1239e",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.0.5",
- "php": "^7.0",
- "phpunit/php-text-template": "^1.2.1",
- "sebastian/exporter": "^3.1"
- },
- "conflict": {
- "phpunit/phpunit": "<6.0"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=5.4"
},
"require-dev": {
- "phpunit/phpunit": "^6.5.11"
+ "phpunit/phpunit": "^4.8|^5.7",
+ "symfony/finder": "^2.8"
},
"suggest": {
- "ext-soap": "*"
+ "ext-dom": "Needed to make use of the features in the utilities namespace"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.0.x-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-0": {
+ "Highlight\\": "",
+ "HighlightUtilities\\": ""
+ },
+ "files": [
+ "HighlightUtilities/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "role": "lead"
+ "name": "Geert Bergman",
+ "role": "Project Author",
+ "homepage": "http://www.scrivo.org/"
+ },
+ {
+ "name": "Vladimir Jimenez",
+ "role": "Contributor",
+ "homepage": "https://allejo.io"
+ },
+ {
+ "name": "Martin Folkers",
+ "role": "Contributor",
+ "homepage": "https://twobrain.io"
}
],
- "description": "Mock Object library for PHPUnit",
- "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
+ "description": "Server side syntax highlighter that supports 185 languages. It's a PHP port of highlight.js",
"keywords": [
- "mock",
- "xunit"
+ "code",
+ "highlight",
+ "highlight.js",
+ "highlight.php",
+ "syntax"
],
- "time": "2018-08-09T05:50:03+00:00"
+ "time": "2019-08-27T04:27:48+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
},
{
"name": "sebastian/comparator",
- "version": "2.1.3",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
- "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"shasum": ""
},
"require": {
- "php": "^7.0",
- "sebastian/diff": "^2.0 || ^3.0",
+ "php": "^7.1",
+ "sebastian/diff": "^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.4"
+ "phpunit/phpunit": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
"compare",
"equality"
],
- "time": "2018-02-01T13:46:46+00:00"
+ "time": "2018-07-12T15:12:46+00:00"
},
{
"name": "sebastian/diff",
- "version": "2.0.1",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
- "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.2"
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
- "diff"
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
],
- "time": "2017-08-03T08:09:46+00:00"
+ "time": "2019-02-04T06:01:07+00:00"
},
{
"name": "sebastian/environment",
- "version": "3.1.0",
+ "version": "4.2.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
- "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.1"
},
"require-dev": {
- "phpunit/phpunit": "^6.1"
+ "phpunit/phpunit": "^7.5"
+ },
+ "suggest": {
+ "ext-posix": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1.x-dev"
+ "dev-master": "4.2-dev"
}
},
"autoload": {
"environment",
"hhvm"
],
- "time": "2017-07-01T08:51:00+00:00"
+ "time": "2019-05-05T09:05:15+00:00"
},
{
"name": "sebastian/exporter",
- "version": "3.1.0",
+ "version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "234199f4528de6d12aaa58b612e98f7d36adb937"
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937",
- "reference": "234199f4528de6d12aaa58b612e98f7d36adb937",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
"shasum": ""
},
"require": {
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ },
{
"name": "Jeff Welch",
"name": "Volker Dusch",
},
- {
- "name": "Bernhard Schussek",
- },
- {
- "name": "Sebastian Bergmann",
- },
{
"name": "Adam Harvey",
+ },
+ {
+ "name": "Bernhard Schussek",
}
],
"description": "Provides the functionality to export PHP variables for visualization",
"export",
"exporter"
],
- "time": "2017-04-03T13:19:02+00:00"
+ "time": "2019-09-14T09:02:43+00:00"
+ },
+ {
+ "name": "sebastian/finder-facade",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/finder-facade.git",
+ "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+ "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
+ "shasum": ""
+ },
+ "require": {
+ "symfony/finder": "~2.3|~3.0|~4.0",
+ "theseer/fdomdocument": "~1.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "role": "lead"
+ }
+ ],
+ "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
+ "homepage": "https://github.com/sebastianbergmann/finder-facade",
+ "time": "2017-11-18T17:31:49+00:00"
},
{
"name": "sebastian/global-state",
- "version": "2.0.0",
+ "version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+ "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
- "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
+ "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4",
"shasum": ""
},
"require": {
- "php": "^7.0"
+ "php": "^7.2",
+ "sebastian/object-reflector": "^1.1.1",
+ "sebastian/recursion-context": "^3.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "ext-dom": "*",
+ "phpunit/phpunit": "^8.0"
},
"suggest": {
"ext-uopz": "*"
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
"keywords": [
"global state"
],
- "time": "2017-04-27T15:39:26+00:00"
+ "time": "2019-02-01T05:30:01+00:00"
},
{
"name": "sebastian/object-enumerator",
},
{
"name": "sebastian/resource-operations",
- "version": "1.0.0",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"shasum": ""
},
"require": {
- "php": ">=5.6.0"
+ "php": "^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2015-07-28T20:34:47+00:00"
+ "time": "2018-10-04T04:07:39+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3",
+ "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "role": "lead"
+ }
+ ],
+ "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"
},
{
"name": "sebastian/version",
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.4.0",
+ "version": "3.4.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "379deb987e26c7cd103a7b387aea178baec96e48"
+ "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48",
- "reference": "379deb987e26c7cd103a7b387aea178baec96e48",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
+ "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
"shasum": ""
},
"require": {
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "http://www.squizlabs.com/php-codesniffer",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
- "time": "2018-12-19T23:57:18+00:00"
+ "time": "2019-04-10T23:49:02+00:00"
},
{
"name": "symfony/dom-crawler",
- "version": "v3.1.10",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
- "reference": "7eede2a901a19928494194f7d1815a77b9a473a0"
+ "reference": "cc686552948d627528c0e2e759186dff67c2610e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0",
- "reference": "7eede2a901a19928494194f7d1815a77b9a473a0",
+ "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cc686552948d627528c0e2e759186dff67c2610e",
+ "reference": "cc686552948d627528c0e2e759186dff67c2610e",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
+ "conflict": {
+ "masterminds/html5": "<2.6"
+ },
"require-dev": {
- "symfony/css-selector": "~2.8|~3.0"
+ "masterminds/html5": "^2.6",
+ "symfony/css-selector": "~3.4|~4.0"
},
"suggest": {
"symfony/css-selector": ""
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
- "time": "2017-01-21T17:13:55+00:00"
+ "time": "2019-08-26T08:26:39+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v3.3.6",
+ "version": "v4.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.3-dev"
+ "dev-master": "4.3-dev"
}
},
"autoload": {
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
- "time": "2017-07-11T07:17:58+00:00"
+ "time": "2019-08-20T14:07:54+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"
},
{
"name": "theseer/tokenizer",
- "version": "1.1.0",
+ "version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b"
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b",
- "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"shasum": ""
},
"require": {
"authors": [
{
"name": "Arne Blankerts",
- "role": "Developer"
+ "role": "Developer",
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
- "time": "2017-04-07T12:08:54+00:00"
+ "time": "2019-06-13T22:48:21+00:00"
},
{
"name": "webmozart/assert",
- "version": "1.4.0",
+ "version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
"shasum": ""
},
"require": {
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
- "phpunit/phpunit": "^4.6",
- "sebastian/version": "^1.0.1"
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
},
"type": "library",
"extra": {
"check",
"validate"
],
- "time": "2018-12-25T11:19:39+00:00"
+ "time": "2019-08-24T08:43:50+00:00"
+ },
+ {
+ "name": "wnx/laravel-stats",
+ "version": "v2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stefanzweifel/laravel-stats.git",
+ "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stefanzweifel/laravel-stats/zipball/1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+ "reference": "1b3c60bfbf81233973cbc2a63be4e6f83b2d6205",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "~5.8.0|^6.0",
+ "illuminate/support": "~5.8.0|^6.0",
+ "php": ">=7.2.0",
+ "phploc/phploc": "~4.0|~5.0",
+ "symfony/finder": "~3.3|~4.0"
+ },
+ "require-dev": {
+ "laravel/browser-kit-testing": "~2.0|~3.0|~4.0|~5.0",
+ "laravel/dusk": "~3.0|~4.0|~5.0",
+ "mockery/mockery": "^1.1",
+ "orchestra/testbench": "^3.8",
+ "phpunit/phpunit": "6.*|7.*|8.*"
+ },
+ "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": "2019-09-01T14:18:49+00:00"
}
],
"aliases": [],
- "minimum-stability": "stable",
- "stability-flags": {
- "laravel/socialite": 20
- },
- "prefer-stable": false,
+ "minimum-stability": "dev",
+ "stability-flags": [],
+ "prefer-stable": true,
"prefer-lowest": false,
"platform": {
- "php": ">=7.0.5",
- "ext-json": "*",
- "ext-tidy": "*",
+ "php": "^7.2",
+ "ext-curl": "*",
"ext-dom": "*",
- "ext-xml": "*",
- "ext-mbstring": "*",
"ext-gd": "*",
- "ext-curl": "*"
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-tidy": "*",
+ "ext-xml": "*"
},
"platform-dev": [],
"platform-overrides": {
- "php": "7.0.5"
+ "php": "7.2.0"
}
}
return [
'name' => $faker->name,
'email' => $faker->email,
- 'password' => str_random(10),
- 'remember_token' => str_random(10),
+ 'password' => Str::random(10),
+ 'remember_token' => Str::random(10),
'email_confirmed' => 1
];
});
$factory->define(\BookStack\Entities\Bookshelf::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
$factory->define(\BookStack\Entities\Book::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
$factory->define(\BookStack\Entities\Chapter::class, function ($faker) {
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'description' => $faker->paragraph
];
});
$html = '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>';
return [
'name' => $faker->sentence,
- 'slug' => str_random(10),
+ 'slug' => Str::random(10),
'html' => $html,
'text' => strip_tags($html),
'revision_count' => 1
--- /dev/null
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+class AddUserInvitesTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('user_invites', function (Blueprint $table) {
+ $table->increments('id');
+ $table->integer('user_id')->index();
+ $table->string('token')->index();
+ $table->nullableTimestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('user_invites');
+ }
+}
class DatabaseSeeder extends Seeder
{
/**
- * Run the database seeds.
+ * Seed the application's database.
*
* @return void
*/
<?php
+use BookStack\Auth\Permissions\PermissionService;
+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 Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
class DummyContentSeeder extends Seeder
{
public function run()
{
// Create an editor user
- $editorUser = factory(\BookStack\Auth\User::class)->create();
- $editorRole = \BookStack\Auth\Role::getRole('editor');
+ $editorUser = factory(User::class)->create();
+ $editorRole = Role::getRole('editor');
$editorUser->attachRole($editorRole);
// Create a viewer user
- $viewerUser = factory(\BookStack\Auth\User::class)->create();
- $role = \BookStack\Auth\Role::getRole('viewer');
+ $viewerUser = factory(User::class)->create();
+ $role = Role::getRole('viewer');
$viewerUser->attachRole($role);
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
factory(\BookStack\Entities\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
- $chapters = factory(\BookStack\Entities\Chapter::class, 3)->create($byData)
+ $chapters = factory(Chapter::class, 3)->create($byData)
->each(function($chapter) use ($editorUser, $book, $byData){
- $pages = factory(\BookStack\Entities\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
+ $pages = factory(Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
- $pages = factory(\BookStack\Entities\Page::class, 3)->make($byData);
+ $pages = factory(Page::class, 3)->make($byData);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
- $largeBook = factory(\BookStack\Entities\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
- $pages = factory(\BookStack\Entities\Page::class, 200)->make($byData);
- $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make($byData);
+ $largeBook = factory(\BookStack\Entities\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);
$largeBook->chapters()->saveMany($chapters);
- $shelves = factory(\BookStack\Entities\Bookshelf::class, 10)->create($byData);
+ $shelves = factory(Bookshelf::class, 10)->create($byData);
$largeBook->shelves()->attach($shelves->pluck('id'));
- app(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
- app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+ app(PermissionService::class)->buildJointPermissions();
+ app(SearchService::class)->indexAllEntities();
}
}
<?php
+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 Illuminate\Database\Seeder;
+use Illuminate\Support\Str;
class LargeContentSeeder extends Seeder
{
public function run()
{
// Create an editor user
- $editorUser = factory(\BookStack\Auth\User::class)->create();
- $editorRole = \BookStack\Auth\Role::getRole('editor');
+ $editorUser = factory(User::class)->create();
+ $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]);
- $pages = factory(\BookStack\Entities\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
- $chapters = factory(\BookStack\Entities\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $largeBook = factory(\BookStack\Entities\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(\BookStack\Auth\Permissions\PermissionService::class)->buildJointPermissions();
- app(\BookStack\Entities\SearchService::class)->indexAllEntities();
+ app(PermissionService::class)->buildJointPermissions();
+ app(SearchService::class)->indexAllEntities();
}
}
--- /dev/null
+FROM php:7.3-apache
+
+ENV APACHE_DOCUMENT_ROOT /app/public
+WORKDIR /app
+
+RUN apt-get update -y \
+ && apt-get install -y git zip unzip libtidy-dev libpng-dev libldap2-dev libxml++2.6-dev wait-for-it \
+ && docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
+ && docker-php-ext-install pdo pdo_mysql tidy dom xml mbstring gd ldap \
+ && a2enmod rewrite \
+ && sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
+ && sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
+ && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
+ && php composer-setup.php \
+ && mv composer.phar /usr/bin/composer \
+ && php -r "unlink('composer-setup.php');"
--- /dev/null
+#!/bin/bash
+
+set -e
+
+env
+
+if [[ -n "$1" ]]; then
+ exec "$@"
+else
+ wait-for-it db:3306 -t 45
+ php artisan migrate --database=mysql
+ chown -R www-data:www-data storage
+ exec apache2-foreground
+fi
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+
+set -e
+
+npm install
+npm rebuild node-sass
+
+exec npm run watch
\ No newline at end of file
--- /dev/null
+# This is a Docker Compose configuration
+# intended for development purposes only
+
+version: '3'
+
+volumes:
+ db: {}
+
+services:
+ db:
+ image: mysql:8
+ environment:
+ MYSQL_DATABASE: bookstack-test
+ MYSQL_USER: bookstack-test
+ MYSQL_PASSWORD: bookstack-test
+ MYSQL_RANDOM_ROOT_PASSWORD: 'true'
+ command: --default-authentication-plugin=mysql_native_password
+ volumes:
+ - db:/var/lib/mysql
+ app:
+ build:
+ context: .
+ dockerfile: ./dev/docker/Dockerfile
+ environment:
+ DB_CONNECTION: mysql
+ DB_HOST: db
+ DB_PORT: 3306
+ DB_DATABASE: bookstack-test
+ DB_USERNAME: bookstack-test
+ DB_PASSWORD: bookstack-test
+ MAIL_DRIVER: smtp
+ MAIL_HOST: mailhog
+ MAIL_PORT: 1025
+ ports:
+ - ${DEV_PORT:-8080}:80
+ volumes:
+ - ./:/app
+ entrypoint: /app/dev/docker/entrypoint.app.sh
+ node:
+ image: node:alpine
+ working_dir: /app
+ volumes:
+ - ./:/app
+ entrypoint: /app/dev/docker/entrypoint.node.sh
+ mailhog:
+ image: mailhog/mailhog
+ ports:
+ - ${DEV_MAIL_PORT:-8025}:8025
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
+ "fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
+ "fsevents": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+ "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "nan": "^2.12.1",
+ "node-pre-gyp": "^0.12.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minipass": {
+ "version": "2.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.2.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.3.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^4.1.0",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.12.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
+ }
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.4.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.7.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.2"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "yallist": {
+ "version": "3.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
}
},
"lodash": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash.tail": {
}
},
"mixin-deep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
- "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true,
"requires": {
"for-in": "^1.0.2",
"dev": true
},
"set-value": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
- "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"dev": true
},
"union-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
- "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true,
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
- "set-value": "^0.4.3"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "set-value": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
- "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.1",
- "to-object-path": "^0.3.0"
- }
- }
+ "set-value": "^2.0.1"
}
},
"uniq": {
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
- stopOnFailure="false"
- syntaxCheck="false">
+ stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests/</directory>
</whitelist>
</filter>
<php>
- <env name="APP_ENV" value="testing"/>
- <env name="APP_DEBUG" value="false"/>
- <env name="APP_LANG" value="en"/>
- <env name="APP_AUTO_LANG_PUBLIC" value="true"/>
- <env name="CACHE_DRIVER" value="array"/>
- <env name="SESSION_DRIVER" value="array"/>
- <env name="QUEUE_DRIVER" value="sync"/>
- <env name="DB_CONNECTION" value="mysql_testing"/>
- <env name="MAIL_DRIVER" value="log"/>
- <env name="AUTH_METHOD" value="standard"/>
- <env name="DISABLE_EXTERNAL_SERVICES" value="true"/>
- <env name="AVATAR_URL" value=""/>
- <env name="LDAP_VERSION" value="3"/>
- <env name="STORAGE_TYPE" value="local"/>
- <env name="STORAGE_ATTACHMENT_TYPE" value="local"/>
- <env name="STORAGE_IMAGE_TYPE" value="local"/>
- <env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
- <env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
- <env name="GITHUB_AUTO_REGISTER" value=""/>
- <env name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
- <env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
- <env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
- <env name="GOOGLE_AUTO_REGISTER" value=""/>
- <env name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
- <env name="GOOGLE_SELECT_ACCOUNT" value=""/>
- <env name="APP_URL" value="http://bookstack.dev"/>
- <env name="DEBUGBAR_ENABLED" value="false"/>
+ <server name="APP_ENV" value="testing"/>
+ <server name="APP_DEBUG" value="false"/>
+ <server name="APP_LANG" value="en"/>
+ <server name="APP_AUTO_LANG_PUBLIC" value="true"/>
+ <server name="CACHE_DRIVER" value="array"/>
+ <server name="SESSION_DRIVER" value="array"/>
+ <server name="QUEUE_CONNECTION" value="sync"/>
+ <server name="DB_CONNECTION" value="mysql_testing"/>
+ <server name="BCRYPT_ROUNDS" value="4"/>
+ <server name="MAIL_DRIVER" value="array"/>
+ <server name="LOG_CHANNEL" value="single"/>
+ <server name="AUTH_METHOD" value="standard"/>
+ <server name="DISABLE_EXTERNAL_SERVICES" value="true"/>
+ <server name="AVATAR_URL" value=""/>
+ <server name="LDAP_VERSION" value="3"/>
+ <server name="STORAGE_TYPE" value="local"/>
+ <server name="STORAGE_ATTACHMENT_TYPE" value="local"/>
+ <server name="STORAGE_IMAGE_TYPE" value="local"/>
+ <server name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
+ <server name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+ <server name="GITHUB_AUTO_REGISTER" value=""/>
+ <server name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
+ <server name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
+ <server name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
+ <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"/>
</php>
</phpunit>
--- /dev/null
+<!--
+ Rewrites requires Microsoft URL Rewrite Module for IIS
+ Download: https://www.microsoft.com/en-us/download/details.aspx?id=47337
+ Debug Help: https://docs.microsoft.com/en-us/iis/extensions/url-rewrite-module/using-failed-request-tracing-to-trace-rewrite-rules
+-->
+<configuration>
+ <system.webServer>
+ <rewrite>
+ <rules>
+ <rule name="Imported Rule 1" stopProcessing="true">
+ <match url="^(.*)/$" ignoreCase="false" />
+ <conditions>
+ <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+ </conditions>
+ <action type="Redirect" redirectType="Permanent" url="/{R:1}" />
+ </rule>
+ <rule name="Imported Rule 2" stopProcessing="true">
+ <match url="^" ignoreCase="false" />
+ <conditions>
+ <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />
+ <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />
+ </conditions>
+ <action type="Rewrite" url="index.php" />
+ </rule>
+ </rules>
+ </rewrite>
+ </system.webServer>
+</configuration>
\ No newline at end of file
[](https://github.com/BookStackApp/BookStack/releases/latest)
[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
-[](https://travis-ci.org/BookStackApp/BookStack)
+[](https://github.com/BookStackApp/BookStack/actions)
[](https://discord.gg/ztkBqR2)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
* [Admin Login](https://demo.bookstackapp.com/
[email protected]&password=password)
* [BookStack Blog](https://www.bookstackapp.com/blog)
-## Project Definition
+## 📚 Project Definition
BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
-## Road Map
+## 🛣️ Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
-## Release Versioning & Process
+## 🚀 Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
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).
-## Development & Testing
+## 🛠️ 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:
Once done you can run `php vendor/bin/phpunit` in the application root directory to run all tests.
-## Translations
+### 📜 Code Standards
+
+PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+
+### 🐋 Development using Docker
+
+This repository ships with a Docker Compose configuration intended for development purposes. It'll build a PHP image with all needed extensions installed and start up a MySQL server and a Node image watching the UI assets.
+
+To get started, make sure you meet the following requirements:
+
+- Docker and Docker Compose are installed
+- Your user is part of the `docker` group
+
+If all the conditions are met, you can proceed with the following steps:
+
+1. Install PHP/Composer dependencies with **`docker-compose run app composer install`** (first time can take a while because the image has to be built).
+2. **Copy `.env.example` to `.env`** and change `APP_KEY` to a random 32 char string.
+3. Make sure **port 8080 is unused** *or else* change `DEV_PORT` to a free port on your host.
+4. **Run `chgrp -R docker storage`**. The development container will chown the `storage` directory to the `www-data` user inside the container so BookStack can write to it. You need to change the group to your host's `docker` group here to not lose access to the `storage` directory.
+5. **Run `docker-compose up`** and wait until all database migrations have been done.
+6. You can now login with `
[email protected]` and `password` as password on `localhost:8080` (or another port if specified).
+
+If needed, You'll be able to run any artisan commands via docker-compose like so:
+
+ ```shell script
+docker-compose run app php artisan list
+```
+
+The docker-compose setup runs an instance of [MailHog](https://github.com/mailhog/MailHog) and sets environment variables to redirect any BookStack-sent emails to MailHog. You can view this mail via the MailHog web interface on `localhost:8025`. You can change the port MailHog is accessible on by setting a `DEV_MAIL_PORT` environment variable.
+
+## 🌎 Translations
All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
-## Contributing & Maintenance
+## 🎁 Contributing, Issues & Pull Requests
-Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
+Feel free to create issues to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
+
+Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/CODE_OF_CONDUCT.md).
-### Code Standards
+## 🔒 Security
-PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr-2/) standards. From the BookStack root folder you can run `./vendor/bin/phpcs` to check code is formatted correctly and `./vendor/bin/phpcbf` to auto-fix non-PSR-2 code.
+Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
-### Pull Requests
+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).
-Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
+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).
-Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
+## ♿ Accessibility
-If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
+We want BookStack to remain accessible to as many people as possible. We aim for at least WCAG 2.1 Level A standards where possible although we do not strictly test this upon each release. If you come across any accessibility issues please feel free to open an issue.
-## Website, Docs & Blog
+## 🖥️ Website, Docs & Blog
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
-## Security
-
-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 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).
-
-
-## License
+## ⚖️ License
-The BookStack source is provided under the MIT License.
+The BookStack source is provided under the MIT License. The libraries used by, and included with, BookStack are provided under their own licenses.
-## Attribution
+## 👪 Attribution
The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors).
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
* [Draw.io](https://github.com/jgraph/drawio)
+* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
\ No newline at end of file
+++ /dev/null
-/**
- * Dropdown
- * Provides some simple logic to create simple dropdown menus.
- */
-class DropDown {
-
- constructor(elem) {
- this.container = elem;
- this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
- this.moveMenu = elem.hasAttribute('dropdown-move-menu');
- this.toggle = elem.querySelector('[dropdown-toggle]');
- this.body = document.body;
- this.setupListeners();
- }
-
- show(event) {
- this.hideAll();
-
- this.menu.style.display = 'block';
- this.menu.classList.add('anim', 'menuIn');
-
- if (this.moveMenu) {
- // Move to body to prevent being trapped within scrollable sections
- this.rect = this.menu.getBoundingClientRect();
- this.body.appendChild(this.menu);
- this.menu.style.position = 'fixed';
- this.menu.style.left = `${this.rect.left}px`;
- this.menu.style.top = `${this.rect.top}px`;
- this.menu.style.width = `${this.rect.width}px`;
- }
-
- // Set listener to hide on mouse leave or window click
- this.menu.addEventListener('mouseleave', this.hide.bind(this));
- window.addEventListener('click', event => {
- if (!this.menu.contains(event.target)) {
- this.hide();
- }
- });
-
- // Focus on first input if existing
- let input = this.menu.querySelector('input');
- if (input !== null) input.focus();
-
- event.stopPropagation();
- }
-
- hideAll() {
- for (let dropdown of window.components.dropdown) {
- dropdown.hide();
- }
- }
-
- hide() {
- this.menu.style.display = 'none';
- this.menu.classList.remove('anim', 'menuIn');
- if (this.moveMenu) {
- this.menu.style.position = '';
- this.menu.style.left = '';
- this.menu.style.top = '';
- this.menu.style.width = '';
- this.container.appendChild(this.menu);
- }
- }
-
- setupListeners() {
- // Hide menu on option click
- this.container.addEventListener('click', event => {
- let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
- if (possibleChildren.indexOf(event.target) !== -1) this.hide();
- });
- // Show dropdown on toggle click
- this.toggle.addEventListener('click', this.show.bind(this));
- // Hide menu on enter press
- this.container.addEventListener('keypress', event => {
- if (event.keyCode !== 13) return true;
- event.preventDefault();
- this.hide();
- return false;
- });
- }
-
-}
-
-export default DropDown;
\ No newline at end of file
+++ /dev/null
-/**
- * Simple global events manager
- */
-class Events {
- constructor() {
- this.listeners = {};
- this.stack = [];
- }
-
- emit(eventName, eventData) {
- this.stack.push({name: eventName, data: eventData});
- if (typeof this.listeners[eventName] === 'undefined') return this;
- let eventsToStart = this.listeners[eventName];
- for (let i = 0; i < eventsToStart.length; i++) {
- let event = eventsToStart[i];
- event(eventData);
- }
- return this;
- }
-
- listen(eventName, callback) {
- if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
- this.listeners[eventName].push(callback);
- return this;
- }
-}
-
-export default Events;
\ No newline at end of file
+++ /dev/null
-@import "variables";
-
-header {
- display: none;
-}
-
-body {
- font-size: 12px;
-}
-
-.page-content {
- margin: 0 auto;
-}
-
-.flex-fill {
- display: block;
-}
-
-.flex.sidebar + .flex.content {
- border-left: none;
-}
-
-.print-hidden {
- display: none;
-}
-
-.print-full-width {
- width: 100%;
- float: none;
- display: block;
-}
-
-h2 {
- font-size: 2em;
- line-height: 1;
- margin-top: 0.6em;
- margin-bottom: 0.3em;
-}
-
-.comments-container {
- display: none;
-}
\ No newline at end of file
this.searchInput = elem.querySelector('input');
this.loadingElem = elem.querySelector('.loading-container');
this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
- this.toggleElem = elem.querySelector('[dropdown-toggle]');
// this.loadingElem.style.display = 'none';
const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
this.entityType = entityDescriptor[0];
this.entityId = Number(entityDescriptor[1]);
- this.toggleElem.addEventListener('click', this.onShow.bind(this));
+ this.elem.addEventListener('show', this.onShow.bind(this));
this.searchInput.addEventListener('input', this.onSearch.bind(this));
- this.elem.addEventListener('keydown', this.keyDown.bind(this));
- }
-
- keyDown(event) {
- if (event.key === 'ArrowDown') {
- this.listFocusChange(1);
- event.preventDefault();
- } else if (event.key === 'ArrowUp') {
- this.listFocusChange(-1);
- event.preventDefault();
- }
- }
-
- listFocusChange(indexChange = 1) {
- const links = Array.from(this.entityListElem.querySelectorAll('a:not(.hidden)'));
- const currentFocused = this.entityListElem.querySelector('a:focus');
- const currentFocusedIndex = links.indexOf(currentFocused);
- const defaultFocus = (indexChange > 0) ? links[0] : this.searchInput;
- const nextElem = links[currentFocusedIndex + indexChange] || defaultFocus;
- nextElem.focus();
}
onShow() {
open() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.add('open');
+ this.elem.setAttribute('aria-expanded', 'true');
slideDown(list, 240);
}
close() {
const list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.remove('open');
+ this.elem.setAttribute('aria-expanded', 'false');
slideUp(list, 240);
}
open() {
this.elem.classList.add('open');
+ this.trigger.setAttribute('aria-expanded', 'true');
slideDown(this.content, 300);
}
close() {
this.elem.classList.remove('open');
+ this.trigger.setAttribute('aria-expanded', 'false');
slideUp(this.content, 300);
}
--- /dev/null
+import {onSelect} from "../services/dom";
+
+/**
+ * Dropdown
+ * Provides some simple logic to create simple dropdown menus.
+ */
+class DropDown {
+
+ constructor(elem) {
+ this.container = elem;
+ this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]');
+ this.moveMenu = elem.hasAttribute('dropdown-move-menu');
+ this.toggle = elem.querySelector('[dropdown-toggle]');
+ this.body = document.body;
+ this.showing = false;
+ this.setupListeners();
+ }
+
+ show(event = null) {
+ this.hideAll();
+
+ this.menu.style.display = 'block';
+ this.menu.classList.add('anim', 'menuIn');
+ this.toggle.setAttribute('aria-expanded', 'true');
+
+ if (this.moveMenu) {
+ // Move to body to prevent being trapped within scrollable sections
+ this.rect = this.menu.getBoundingClientRect();
+ this.body.appendChild(this.menu);
+ this.menu.style.position = 'fixed';
+ this.menu.style.left = `${this.rect.left}px`;
+ this.menu.style.top = `${this.rect.top}px`;
+ this.menu.style.width = `${this.rect.width}px`;
+ }
+
+ // Set listener to hide on mouse leave or window click
+ this.menu.addEventListener('mouseleave', this.hide.bind(this));
+ window.addEventListener('click', event => {
+ if (!this.menu.contains(event.target)) {
+ this.hide();
+ }
+ });
+
+ // Focus on first input if existing
+ const input = this.menu.querySelector('input');
+ if (input !== null) input.focus();
+
+ this.showing = true;
+
+ const showEvent = new Event('show');
+ this.container.dispatchEvent(showEvent);
+
+ if (event) {
+ event.stopPropagation();
+ }
+ }
+
+ hideAll() {
+ for (let dropdown of window.components.dropdown) {
+ dropdown.hide();
+ }
+ }
+
+ hide() {
+ this.menu.style.display = 'none';
+ this.menu.classList.remove('anim', 'menuIn');
+ this.toggle.setAttribute('aria-expanded', 'false');
+ if (this.moveMenu) {
+ this.menu.style.position = '';
+ this.menu.style.left = '';
+ this.menu.style.top = '';
+ this.menu.style.width = '';
+ this.container.appendChild(this.menu);
+ }
+ this.showing = false;
+ }
+
+ getFocusable() {
+ return Array.from(this.menu.querySelectorAll('[tabindex],[href],button,input:not([type=hidden])'));
+ }
+
+ focusNext() {
+ const focusable = this.getFocusable();
+ const currentIndex = focusable.indexOf(document.activeElement);
+ let newIndex = currentIndex + 1;
+ if (newIndex >= focusable.length) {
+ newIndex = 0;
+ }
+
+ focusable[newIndex].focus();
+ }
+
+ focusPrevious() {
+ const focusable = this.getFocusable();
+ const currentIndex = focusable.indexOf(document.activeElement);
+ let newIndex = currentIndex - 1;
+ if (newIndex < 0) {
+ newIndex = focusable.length - 1;
+ }
+
+ focusable[newIndex].focus();
+ }
+
+ setupListeners() {
+ // Hide menu on option click
+ this.container.addEventListener('click', event => {
+ const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
+ if (possibleChildren.includes(event.target)) {
+ this.hide();
+ }
+ });
+
+ onSelect(this.toggle, event => {
+ event.stopPropagation();
+ this.show(event);
+ if (event instanceof KeyboardEvent) {
+ this.focusNext();
+ }
+ });
+
+ // Keyboard navigation
+ const keyboardNavigation = event => {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
+ this.focusNext();
+ event.preventDefault();
+ } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
+ this.focusPrevious();
+ event.preventDefault();
+ } else if (event.key === 'Escape') {
+ this.hide();
+ this.toggle.focus();
+ event.stopPropagation();
+ }
+ };
+ this.container.addEventListener('keydown', keyboardNavigation);
+ if (this.moveMenu) {
+ this.menu.addEventListener('keydown', keyboardNavigation);
+ }
+
+ // Hide menu on enter press or escape
+ this.menu.addEventListener('keydown ', event => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.hide();
+ }
+ });
+ }
+
+}
+
+export default DropDown;
\ No newline at end of file
toggle() {
this.elem.classList.toggle('open');
+ const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+ this.toggleButton.setAttribute('aria-expanded', expanded);
}
setActiveTab(tabName, openToolbox = false) {
import settingAppColorPicker from "./setting-app-color-picker";
import entityPermissionsEditor from "./entity-permissions-editor";
import templateManager from "./template-manager";
+import newUserPassword from "./new-user-password";
const componentMapping = {
'dropdown': dropdown,
'setting-app-color-picker': settingAppColorPicker,
'entity-permissions-editor': entityPermissionsEditor,
'template-manager': templateManager,
+ 'new-user-password': newUserPassword,
};
window.components = {};
this.markdown.use(mdTasksLists, {label: true});
this.display = this.elem.querySelector('.markdown-display');
+
+ this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea');
this.htmlInput = this.elem.querySelector('input[name=html]');
this.cm = code.markdownEditor(this.input);
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
- this.init();
- // Scroll to text if needed.
- const queryParams = (new URL(window.location)).searchParams;
- const scrollText = queryParams.get('content-text');
- if (scrollText) {
- this.scrollToText(scrollText);
- }
+ this.display.addEventListener('load', () => {
+ this.displayDoc = this.display.contentDocument;
+ this.init();
+ });
+
+ window.$events.emitPublic(elem, 'editor-markdown::setup', {
+ markdownIt: this.markdown,
+ displayEl: this.display,
+ codeMirrorInstance: this.cm,
+ });
}
init() {
let lastClick = 0;
// Prevent markdown display link click redirect
- this.display.addEventListener('click', event => {
+ this.displayDoc.addEventListener('click', event => {
let isDblClick = Date.now() - lastClick < 300;
let link = event.target.closest('a');
this.codeMirrorSetup();
this.listenForBookStackEditorEvents();
+
+ // Scroll to text if needed.
+ const queryParams = (new URL(window.location)).searchParams;
+ const scrollText = queryParams.get('content-text');
+ if (scrollText) {
+ this.scrollToText(scrollText);
+ }
}
// Update the input content and render the display.
updateAndRender() {
- let content = this.cm.getValue();
+ const content = this.cm.getValue();
this.input.value = content;
- let html = this.markdown.render(content);
+ const html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
- this.display.innerHTML = html;
+
+ // Set body content
+ this.displayDoc.body.className = 'page-content';
+ this.displayDoc.body.innerHTML = html;
this.htmlInput.value = html;
+
+ // Copy styles from page head and set custom styles for editor
+ this.loadStylesIntoDisplay();
+ }
+
+ loadStylesIntoDisplay() {
+ if (this.displayStylesLoaded) return;
+ this.displayDoc.documentElement.className = 'markdown-editor-display';
+
+ this.displayDoc.head.innerHTML = '';
+ const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
+ for (let style of styles) {
+ const copy = style.cloneNode(true);
+ this.displayDoc.head.appendChild(copy);
+ }
+
+ this.displayStylesLoaded = true;
}
onMarkdownScroll(lineCount) {
- const elems = this.display.children;
+ const elems = this.displayDoc.body.children;
if (elems.length <= lineCount) return;
const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
}
});
- // Handle images on drag-drop
+ // Handle image & content drag n drop
cm.on('drop', (cm, event) => {
- event.stopPropagation();
- event.preventDefault();
- let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
- cm.setCursor(cursorPos);
- if (!event.dataTransfer || !event.dataTransfer.files) return;
- for (let i = 0; i < event.dataTransfer.files.length; i++) {
- uploadImage(event.dataTransfer.files[i]);
+
+ const templateId = event.dataTransfer.getData('bookstack/template');
+ if (templateId) {
+ const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+ cm.setCursor(cursorPos);
+ event.preventDefault();
+ window.$http.get(`/templates/${templateId}`).then(resp => {
+ const content = resp.data.markdown || resp.data.html;
+ cm.replaceSelection(content);
+ });
+ }
+
+ if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
+ const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
+ cm.setCursor(cursorPos);
+ event.stopPropagation();
+ event.preventDefault();
+ for (let i = 0; i < event.dataTransfer.files.length; i++) {
+ uploadImage(event.dataTransfer.files[i]);
+ }
}
+
});
// Helper to replace editor content
--- /dev/null
+
+class NewUserPassword {
+
+ constructor(elem) {
+ this.elem = elem;
+ this.inviteOption = elem.querySelector('input[name=send_invite]');
+
+ if (this.inviteOption) {
+ this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
+ this.inviteOptionChange();
+ }
+ }
+
+ inviteOptionChange() {
+ const inviting = (this.inviteOption.value === 'true');
+ const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+ for (const input of passwordBoxes) {
+ input.disabled = inviting;
+ }
+ const container = this.elem.querySelector('#password-input-container');
+ if (container) {
+ container.style.display = inviting ? 'none' : 'block';
+ }
+ }
+
+}
+
+export default NewUserPassword;
\ No newline at end of file
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
+
+ window.addEventListener('keyup', event => {
+ if (event.key === 'Escape') {
+ this.hide();
+ }
+ });
+
let closeButtons = elem.querySelectorAll('.popup-header-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
+ hide() { this.toggle(false); }
+ show() { this.toggle(true); }
+
toggle(show = true) {
let start = Date.now();
let duration = 240;
this.container.style.opacity = targetOpacity;
if (elapsedTime > duration) {
this.container.style.display = show ? 'flex' : 'none';
+ if (show) {
+ this.focusOnBody();
+ }
this.container.style.opacity = '';
} else {
requestAnimationFrame(setOpacity.bind(this));
requestAnimationFrame(setOpacity.bind(this));
}
- hide() { this.toggle(false); }
- show() { this.toggle(true); }
+ focusOnBody() {
+ const body = this.container.querySelector('.popup-body');
+ if (body) {
+ body.focus();
+ }
+ }
}
handleAction(event) {
let actionElem = event.target.closest('[action]');
+
if (event.target.matches('a[href^="#"]')) {
const id = event.target.href.split('#')[1];
scrollAndHighlightElement(document.querySelector('#' + id));
}
+
if (actionElem === null) return;
event.preventDefault();
this.colorInput.addEventListener('change', this.updateColor.bind(this));
this.colorInput.addEventListener('input', this.updateColor.bind(this));
this.resetButton.addEventListener('click', event => {
- this.colorInput.value = '#0288D1';
+ this.colorInput.value = '#206ea7';
this.updateColor();
});
}
// Template list item content click
DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
+ // Template list item drag start
+ DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
+
this.setupSearchBox();
}
this.insertTemplate(templateId, 'replace');
}
+ handleTemplateItemDragStart(event, templateItem) {
+ const templateId = templateItem.closest('[template-id]').getAttribute('template-id');
+ event.dataTransfer.setData('bookstack/template', templateId);
+ event.dataTransfer.setData('text/plain', templateId);
+ }
+
handleTemplateActionClick(event, actionButton) {
event.stopPropagation();
stateChange() {
this.input.value = (this.checkbox.checked ? 'true' : 'false');
+
+ // Dispatch change event from hidden input so they can be listened to
+ // like a normal checkbox.
+ const changeEvent = new Event('change');
+ this.input.dispatchEvent(changeEvent);
}
}
this.loadPlugins();
this.tinyMceConfig = this.getTinyMceConfig();
+ window.$events.emitPublic(elem, 'editor-tinymce::pre-init', {config: this.tinyMceConfig});
window.tinymce.init(this.tinyMceConfig);
}
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
+ // Template insertion
+ const templateId = event.dataTransfer.getData('bookstack/template');
+ if (templateId) {
+ event.preventDefault();
+ window.$http.get(`/templates/${templateId}`).then(resp => {
+ editor.selection.setRng(rng);
+ editor.undoManager.transact(function () {
+ editor.execCommand('mceInsertContent', false, resp.data.html);
+ });
+ });
+ }
+
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context));
+ // Custom handler hook
+ window.$events.emitPublic(context.elem, 'editor-tinymce::setup', {editor});
}
};
}
+/**
+ * Used in the function below to store references of clean-up functions.
+ * Used to ensure only one transitionend function exists at any time.
+ * @type {WeakMap<object, any>}
+ */
+const animateStylesCleanupMap = new WeakMap();
+
/**
* Fade out the given element.
* @param {Element} element
* @param {Function|null} onComplete
*/
export function fadeOut(element, animTime = 400, onComplete = null) {
+ cleanupExistingElementAnimation(element);
animateStyles(element, {
opacity: ['1', '0']
}, animTime, () => {
* @param {Number} animTime
*/
export function slideUp(element, animTime = 400) {
+ cleanupExistingElementAnimation(element);
const currentHeight = element.getBoundingClientRect().height;
const computedStyles = getComputedStyle(element);
const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
* @param {Number} animTime - Animation time in ms
*/
export function slideDown(element, animTime = 400) {
+ cleanupExistingElementAnimation(element);
element.style.display = 'block';
const targetHeight = element.getBoundingClientRect().height;
const computedStyles = getComputedStyle(element);
animateStyles(element, animStyles, animTime);
}
-/**
- * Used in the function below to store references of clean-up functions.
- * Used to ensure only one transitionend function exists at any time.
- * @type {WeakMap<object, any>}
- */
-const animateStylesCleanupMap = new WeakMap();
-
/**
* Animate the css styles of an element using FLIP animation techniques.
* Styles must be an object where the keys are style properties, camelcase, and the values
}
element.style.transition = null;
element.removeEventListener('transitionend', cleanup);
+ animateStylesCleanupMap.delete(element);
if (onComplete) onComplete();
};
setTimeout(() => {
- requestAnimationFrame(() => {
- element.style.transition = `all ease-in-out ${animTime}ms`;
- for (let style of styleNames) {
- element.style[style] = styles[style][1];
- }
+ element.style.transition = `all ease-in-out ${animTime}ms`;
+ for (let style of styleNames) {
+ element.style[style] = styles[style][1];
+ }
- if (animateStylesCleanupMap.has(element)) {
- const oldCleanup = animateStylesCleanupMap.get(element);
- element.removeEventListener('transitionend', oldCleanup);
- }
+ element.addEventListener('transitionend', cleanup);
+ animateStylesCleanupMap.set(element, cleanup);
+ }, 15);
+}
- element.addEventListener('transitionend', cleanup);
- animateStylesCleanupMap.set(element, cleanup);
- });
- }, 10);
+/**
+ * Run the active cleanup action for the given element.
+ * @param {Element} element
+ */
+function cleanupExistingElementAnimation(element) {
+ if (animateStylesCleanupMap.has(element)) {
+ const oldCleanup = animateStylesCleanupMap.get(element);
+ oldCleanup();
+ }
}
\ No newline at end of file
}
/**
- * Get a CodeMirror instace to use for the markdown editor.
+ * Get a CodeMirror instance to use for the markdown editor.
* @param {HTMLElement} elem
* @returns {*}
*/
function markdownEditor(elem) {
- let content = elem.textContent;
-
- return CodeMirror(function (elt) {
- elem.parentNode.insertBefore(elt, elem);
- elem.style.display = 'none';
- }, {
+ const content = elem.textContent;
+ const config = {
value: content,
mode: "markdown",
lineNumbers: true,
lineWrapping: true,
theme: getTheme(),
scrollPastEnd: true,
- });
+ };
+
+ window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
+
+ return CodeMirror(function (elt) {
+ elem.parentNode.insertBefore(elt, elem);
+ elem.style.display = 'none';
+ }, config);
}
/**
}
}
+/**
+ * Helper to run an action when an element is selected.
+ * A "select" is made to be accessible, So can be a click, space-press or enter-press.
+ * @param listenerElement
+ * @param callback
+ */
+export function onSelect(listenerElement, callback) {
+ listenerElement.addEventListener('click', callback);
+ listenerElement.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ callback(event);
+ }
+ });
+}
+
/**
* Set a listener on an element for an event emitted by a child
* matching the given childSelector param.
--- /dev/null
+/**
+ * Simple global events manager
+ */
+class Events {
+ constructor() {
+ this.listeners = {};
+ this.stack = [];
+ }
+
+ /**
+ * Emit a custom event for any handlers to pick-up.
+ * @param {String} eventName
+ * @param {*} eventData
+ * @returns {Events}
+ */
+ emit(eventName, eventData) {
+ this.stack.push({name: eventName, data: eventData});
+ if (typeof this.listeners[eventName] === 'undefined') return this;
+ let eventsToStart = this.listeners[eventName];
+ for (let i = 0; i < eventsToStart.length; i++) {
+ let event = eventsToStart[i];
+ event(eventData);
+ }
+ return this;
+ }
+
+ /**
+ * Listen to a custom event and run the given callback when that event occurs.
+ * @param {String} eventName
+ * @param {Function} callback
+ * @returns {Events}
+ */
+ listen(eventName, callback) {
+ if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
+ this.listeners[eventName].push(callback);
+ return this;
+ }
+
+ /**
+ * Emit an event for public use.
+ * Sends the event via the native DOM event handling system.
+ * @param {Element} targetElement
+ * @param {String} eventName
+ * @param {Object} eventData
+ */
+ emitPublic(targetElement, eventName, eventData) {
+ const event = new CustomEvent(eventName, {
+ detail: eventData,
+ bubbles: true
+ });
+ targetElement.dispatchEvent(event);
+ }
+}
+
+export default Events;
\ No newline at end of file
body: data,
};
- if (typeof data === 'object') {
+ if (typeof data === 'object' && !(data instanceof FormData)) {
options.headers = {'Content-Type': 'application/json'};
options.body = JSON.stringify(data);
}
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
let result = null;
- for (const i = 0, len = splitText.length; i < len; i++) {
- const t = splitText[i];
-
+ for (let t of splitText) {
// Parse exact matches
const exactMatches = t.match(exactCountRegex);
if (exactMatches !== null && Number(exactMatches[1]) === count) {
result = (count === 1) ? splitText[0] : splitText[1];
}
- if (result === null) result = splitText[0];
+ if (result === null) {
+ result = splitText[0];
+ }
+
return this.performReplacements(result, replacements);
}
const methods = {
show() {
if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
- this.$refs.overlay.style.display = 'flex';
+ this.$refs.overlay.components.overlay.show();
},
hide() {
- this.$refs.overlay.style.display = 'none';
+ this.$refs.overlay.components.overlay.hide();
},
updateEditorMode(language) {
codeLib.setMode(this.editor, language);
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
@blur="inputBlur"
@keydown="inputKeydown"
+ :aria-label="placeholder"
/>
<ul class="suggestion-box" v-if="showSuggestions">
<li v-for="(suggestion, i) in suggestions"
},
inputKeydown(event) {
- if (event.keyCode === 13) event.preventDefault();
+ if (event.key === 'Enter') event.preventDefault();
if (!this.showSuggestions) return;
// Down arrow
- if (event.keyCode === 40) {
+ if (event.key === 'ArrowDown') {
this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
}
// Up Arrow
- else if (event.keyCode === 38) {
+ else if (event.key === 'ArrowUp') {
this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
}
- // Enter or tab keys
- else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
+ // Enter key
+ else if ((event.key === 'Enter') && !event.shiftKey) {
this.selectSuggestion(this.suggestions[this.active]);
}
// Escape key
- else if (event.keyCode === 27) {
+ else if (event.key === 'Escape') {
this.showSuggestions = false;
}
},
import { fadeOut } from "../../services/animations";
const template = `
- <div class="dropzone-container">
- <div class="dz-message">{{placeholder}}</div>
+ <div class="dropzone-container text-center">
+ <button type="button" class="dz-message">{{placeholder}}</button>
</div>
`;
import draggable from 'vuedraggable';
import autosuggest from './components/autosuggest';
-let data = {
+const data = {
entityId: false,
entityType: null,
tags: [],
const components = {draggable, autosuggest};
const directives = {};
-let methods = {
+const methods = {
addEmptyTag() {
this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'password_confirm' => 'تأكيد كلمة المرور',
- 'password_hint' => 'يجب أن تكون أكثر من 5 حروف',
+ 'password_hint' => 'يجب أن تكون أكثر من 7 حروف',
'forgot_password' => 'نسيت كلمة المرور؟',
'remember_me' => 'تذكرني',
'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
'email' => 'Email',
'password' => 'Heslo',
'password_confirm' => 'Potvrdit heslo',
- 'password_hint' => 'Musí mít víc než 5 znaků',
+ 'password_hint' => 'Musí mít víc než 7 znaků',
'forgot_password' => 'Zapomněli jste heslo?',
'remember_me' => 'Neodhlašovat',
'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',
'email' => 'E-Mail',
'password' => 'Passwort',
'password_confirm' => 'Passwort bestätigen',
- 'password_hint' => 'Mindestlänge: 5 Zeichen',
+ 'password_hint' => 'Mindestlänge: 7 Zeichen',
'forgot_password' => 'Passwort vergessen?',
'remember_me' => 'Angemeldet bleiben',
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
'email' => 'Email',
'password' => 'Password',
'password_confirm' => 'Confirm Password',
- 'password_hint' => 'Must be over 5 characters',
+ 'password_hint' => 'Must be over 7 characters',
'forgot_password' => 'Forgot Password?',
'remember_me' => 'Remember Me',
'ldap_email_hint' => 'Please enter an email to use for this account.',
'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
+
+ // User Invite
+ 'user_invite_email_subject' => 'You have been invited to join :appName!',
+ 'user_invite_email_greeting' => 'An account has been created for you on :appName.',
+ 'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
+ 'user_invite_email_action' => 'Set Account Password',
+ 'user_invite_page_welcome' => 'Welcome to :appName!',
+ 'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',
+ 'user_invite_page_confirm_button' => 'Confirm Password',
+ 'user_invite_success' => 'Password set, you now have access to :appName!'
];
\ No newline at end of file
'add' => 'Add',
// Sort Options
+ 'sort_options' => 'Sort Options',
+ 'sort_direction_toggle' => 'Sort Direction Toggle',
+ 'sort_ascending' => 'Sort Ascending',
+ 'sort_descending' => 'Sort Descending',
'sort_name' => 'Name',
'sort_created_at' => 'Created Date',
'sort_updated_at' => 'Updated Date',
'grid_view' => 'Grid View',
'list_view' => 'List View',
'default' => 'Default',
+ 'breadcrumb' => 'Breadcrumb',
// Header
+ 'profile_menu' => 'Profile Menu',
'view_profile' => 'View Profile',
'edit_profile' => 'Edit Profile',
'pages_delete_confirm' => 'Are you sure you want to delete this page?',
'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
'pages_editing_named' => 'Editing Page :pageName',
+ 'pages_edit_draft_options' => 'Draft Options',
'pages_edit_save_draft' => 'Save Draft',
'pages_edit_draft' => 'Edit Page Draft',
'pages_editing_draft' => 'Editing Draft',
'shelf_tags' => 'Shelf Tags',
'tag' => 'Tag',
'tags' => 'Tags',
+ 'tag_name' => 'Tag Name',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
'tags_add' => 'Add another tag',
+ 'tags_remove' => 'Remove this tag',
'attachments' => 'Attachments',
'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',
'attachments_explain_instant_save' => 'Changes here are saved instantly.',
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+ 'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
*/
return [
- 'password' => 'Passwords must be at least six characters and match the confirmation.',
+ 'password' => 'Passwords must be at least eight characters and match the confirmation.',
'user' => "We can't find a user with that e-mail address.",
'token' => 'This password reset token is invalid.',
'sent' => 'We have e-mailed your password reset link!',
'users_role' => 'User Roles',
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
'users_password' => 'User Password',
- 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
+ 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
+ 'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
+ 'users_send_invite_option' => 'Send user invite email',
'users_external_auth_id' => 'External Authentication ID',
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
'users_password_warning' => 'Only fill the below if you would like to change your password.',
'ru' => 'Русский',
'uk' => 'Українська',
'zh_CN' => '简体中文',
- 'zh_TW' => '繁體中文',
- 'hu' => 'Magyar'
+ 'zh_TW' => '繁體中文',
+ 'hu' => 'Magyar',
+ 'tr' => 'Türkçe',
]
//!////////////////////////////////
];
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'alpha' => 'The :attribute may only contain letters.',
- 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
+ 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
+ 'ends_with' => 'The :attribute must end with one of the following: :values',
'filled' => 'The :attribute field is required.',
+ 'gt' => [
+ 'numeric' => 'The :attribute must be greater than :value.',
+ 'file' => 'The :attribute must be greater than :value kilobytes.',
+ 'string' => 'The :attribute must be greater than :value characters.',
+ 'array' => 'The :attribute must have more than :value items.',
+ ],
+ 'gte' => [
+ 'numeric' => 'The :attribute must be greater than or equal :value.',
+ 'file' => 'The :attribute must be greater than or equal :value kilobytes.',
+ 'string' => 'The :attribute must be greater than or equal :value characters.',
+ 'array' => 'The :attribute must have :value items or more.',
+ ],
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'image_extension' => 'The :attribute must have a valid & supported image extension.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
+ 'ipv4' => 'The :attribute must be a valid IPv4 address.',
+ 'ipv6' => 'The :attribute must be a valid IPv6 address.',
+ 'json' => 'The :attribute must be a valid JSON string.',
+ 'lt' => [
+ 'numeric' => 'The :attribute must be less than :value.',
+ 'file' => 'The :attribute must be less than :value kilobytes.',
+ 'string' => 'The :attribute must be less than :value characters.',
+ 'array' => 'The :attribute must have less than :value items.',
+ ],
+ 'lte' => [
+ 'numeric' => 'The :attribute must be less than or equal :value.',
+ 'file' => 'The :attribute must be less than or equal :value kilobytes.',
+ 'string' => 'The :attribute must be less than or equal :value characters.',
+ 'array' => 'The :attribute must not have more than :value items.',
+ ],
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
],
'no_double_extension' => 'The :attribute must only have a single file extension.',
'not_in' => 'The selected :attribute is invalid.',
+ 'not_regex' => 'The :attribute format is invalid.',
'numeric' => 'The :attribute must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'email' => 'Correo electrónico',
'password' => 'Contraseña',
'password_confirm' => 'Confirmar Contraseña',
- 'password_hint' => 'Debe contener más de 5 caracteres',
+ 'password_hint' => 'Debe contener más de 7 caracteres',
'forgot_password' => '¿Contraseña Olvidada?',
'remember_me' => 'Recordarme',
'ldap_email_hint' => 'Por favor introduzca un mail para utilizar con esta cuenta.',
'email_not_confirmed_click_link' => 'Por favor siga el enlace en el correo electrónico que ha sido enviado durante el proceso de registro.',
'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario que se muestra a continuación.',
'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
+
+ // User Invite
+ 'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
+ 'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
+ 'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
+ 'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',
+ 'user_invite_page_welcome' => '¡Bienvenido a :appName!',
+ 'user_invite_page_text' => 'Para completar la cuenta y tener acceso es necesario que configure una contraseña que se utilizará para entrar en :appName en futuros accesos.',
+ 'user_invite_page_confirm_button' => 'Confirmar Contraseña',
+ 'user_invite_success' => '¡Contraseña guardada, ya tiene acceso a :appName!'
];
\ No newline at end of file
'add' => 'Añadir',
// Sort Options
+ 'sort_options' => 'Opciones de ordenación',
+ 'sort_direction_toggle' => 'Cambiar el Orden',
+ 'sort_ascending' => 'Ordenar Ascendentemente',
+ 'sort_descending' => 'Ordenar Descendentemente',
'sort_name' => 'Nombre',
'sort_created_at' => 'Fecha de Creación',
'sort_updated_at' => 'Fecha de Modificación',
'grid_view' => 'Vista en Cuadrícula',
'list_view' => 'Vista en Lista',
'default' => 'Predeterminada',
+ 'breadcrumb' => 'Rastro de migas de pan',
// Header
+ 'profile_menu' => 'Menú de Perfil',
'view_profile' => 'Ver Perfil',
'edit_profile' => 'Editar Perfil',
'pages_delete_confirm' => '¿Está seguro de borrar esta página?',
'pages_delete_draft_confirm' => '¿Está seguro de que desea borrar este borrador de página?',
'pages_editing_named' => 'Editando página :pageName',
- 'pages_edit_toggle_header' => 'Toggle Título',
+ 'pages_edit_draft_options' => 'Opciones de Borrador',
'pages_edit_save_draft' => 'Guardar borrador',
'pages_edit_draft' => 'Editar borrador de página',
'pages_editing_draft' => 'Editando borrador',
],
'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
'pages_specific' => 'Página específica',
+ 'pages_is_template' => 'Página es plantilla',
// Editor Sidebar
'page_tags' => 'Etiquetas de Página',
'shelf_tags' => 'Etiquetas de Estante',
'tag' => 'Etiqueta',
'tags' => 'Etiquetas',
+ 'tag_name' => 'Nombre de la Etiqueta',
'tag_value' => 'Valor de la etiqueta (Opcional)',
'tags_explain' => "Agrege algunas etiquetas para mejorar la categorización de su contenido. \n Puede asignar un valor a una etiqueta para una organización a mayor detalle.",
'tags_add' => 'Agregar otra etiqueta',
+ 'tags_remove' => 'Eliminar esta etiqueta',
'attachments' => 'Adjuntos',
'attachments_explain' => 'Subir ficheros o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
'attachments_deleted' => 'Adjunto borrado',
'attachments_file_uploaded' => 'Fichero subido éxitosamente',
'attachments_file_updated' => 'Fichero actualizado éxitosamente',
- 'attachments_link_attached' => 'Enlace agregado éxitosamente a la ágina',
+ 'attachments_link_attached' => 'Enlace agregado éxitosamente a la página',
+ 'templates' => 'Plantillas',
+ 'templates_set_as_template' => 'La página es una plantilla',
+ 'templates_explain_set_as_template' => 'Puede ajustar esta página como una plantilla, así su contenido puede emplearse al crear una nueva página. Otros usuarios podrán utilizar esta plantilla si tienen permisos de lectura sobre esta página.',
+ 'templates_replace_content' => 'Reemplazar el contenido de la página',
+ 'templates_append_content' => 'Añadir después del contenido de la página',
+ 'templates_prepend_content' => 'Añadir antes del contenido de la página',
// Profile View
'profile_user_for_x' => 'Usuario para :time',
'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
'social_driver_not_found' => 'Driver social no encontrado',
'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
+ 'invite_token_expired' => 'Este enlace de invitación ha expirado. Puede resetear la contraseña de su cuenta como alternativa.',
// System
'path_not_writable' => 'El fichero no pudo ser subido a la ruta :filePath . Asegúrese de que es escribible por el servidor.',
'app_editor_desc' => 'Seleccione qué editor se usará por todos los usuarios para editar páginas.',
'app_custom_html' => 'Contenido de cabecera HTML personalizado',
'app_custom_html_desc' => 'Cualquier contenido agregado aquí será insertado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas web.',
+ 'app_custom_html_disabled_notice' => 'El contenido personalizado para la cabecera está deshabilitado en esta página de ajustes para permitir que cualquier cambio que rompa la funcionalidad pueda ser revertido.',
'app_logo' => 'Logo de la Aplicación',
'app_logo_desc' => 'Esta imagen debería de ser 43px de altura. <br> Las imágenes grandes serán escaladas.',
'app_primary_color' => 'Color Primario de la Aplicación',
'role_manage_roles' => 'Gestionar roles y permisos de roles',
'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',
'role_manage_own_entity_permissions' => 'Gestionar permisos en libros, capítulos y páginas propias',
+ 'role_manage_page_templates' => 'Administrar plantillas',
'role_manage_settings' => 'Gestionar ajustes de la aplicación',
'role_asset' => 'Permisos de contenido',
'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los contenidos del sistema. Los permisos de Libros, Capítulos y Páginas sobreescribiran estos permisos.',
'users_role_desc' => 'Selecciona los roles a los que será asignado este usuario. Si se asignan varios roles los permisos se acumularán y recibirá todas las habilidades de los roles asignados.',
'users_password' => 'Contraseña de Usuario',
'users_password_desc' => 'Ajusta una contraseña que se utilizará para acceder a la aplicación. Debe ser al menos de 5 caracteres de longitud.',
+ 'users_send_invite_text' => 'Puede enviar una invitación a este usuario por correo electrónico que le permitirá ajustar su propia contraseña, o puede usted ajustar su contraseña.',
+ 'users_send_invite_option' => 'Enviar un correo electrónico de invitación',
'users_external_auth_id' => 'ID externo de autenticación',
'users_external_auth_id_desc' => 'Esta es la ID usada para asociar este usuario con LDAP.',
'users_password_warning' => 'Solo debe rellenar este campo si desea cambiar su contraseña.',
<?php
-
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ *
+ * Cadenas de texto de Actividades
+ * Se usa para todos los textos dentro de los registros de actividad y notificaciones.
+ */
return [
- /**
- * Activity text strings.
- * Is used for all the text within activity logs & notifications.
- */
-
// Pages
'page_create' => 'página creada',
'page_create_notification' => 'Página creada exitosamente',
<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ *
+ * Líneas de lenguaje para autenticación
+ * Las siguientes líneas de lenguaje se utilizan para mostrar mensajes al usuario durante la autenticación.
+ */
return [
- /*
- |--------------------------------------------------------------------------
- | Authentication Language Lines
- |--------------------------------------------------------------------------
- |
- | The following language lines are used during authentication for various
- | messages that we need to display to the user. You are free to modify
- | these language lines according to your application's requirements.
- |
- */
+
'failed' => 'Las credenciales no concuerdan con nuestros registros.',
'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
- /**
- * Login & Register
- */
+ // Login & Register - Ingreso y Registro
'sign_up' => 'Registrarse',
'log_in' => 'Acceder',
'log_in_with' => 'Acceder con :socialDriver',
'sign_up_with' => 'Registrarse con :socialDriver',
- 'logout' => 'Logout',
+ 'logout' => 'Salir',
'name' => 'Nombre',
'username' => 'Nombre de usuario',
'email' => 'Correo electrónico',
'password' => 'Contraseña',
'password_confirm' => 'Confirmar contraseña',
- 'password_hint' => 'Debe contener al menos 5 caracteres',
+ 'password_hint' => 'Debe contener al menos 7 caracteres',
'forgot_password' => '¿Olvidó la contraseña?',
'remember_me' => 'Recordarme',
'ldap_email_hint' => 'Por favor introduzca un correo electrónico para utilizar con esta cuenta.',
'create_account' => 'Crear una cuenta',
+ 'already_have_account' => '¿Ya tiene una cuenta?',
+ 'dont_have_account' => '¿No tiene una cuenta?',
'social_login' => 'Acceso con cuenta Social',
'social_registration' => 'Registro con cuenta Social',
'social_registration_text' => 'Registrar y entrar utilizando otro servicio.',
'register_success' => '¡Gracias por registrarse! Ahora se encuentra registrado y ha accedido a la aplicación.',
- /**
- * Password Reset
- */
+ // Password Reset - Restablecer Contraseña
'reset_password' => 'Restablecer la contraseña',
'reset_password_send_instructions' => 'Introduzca su correo electrónico a continuación y se le enviará un correo electrónico con un enlace para la restauración',
'reset_password_send_button' => 'Enviar enlace de restauración',
'email_reset_not_requested' => 'Si ud. no solicitó un cambio de contraseña, no se requiere ninguna acción.',
- /**
- * Email Confirmation
- */
+ // Email Confirmation - Confirmación de correo electrónico
'email_confirm_subject' => 'Confirme su correo electrónico en :appName',
'email_confirm_greeting' => '¡Gracias por unirse a :appName!',
'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico presionando en el siguiente botón:',
'email_not_confirmed_click_link' => 'Por favor verifique el correo electrónico con el enlace de confirmación que fue enviado luego de registrarse.',
'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario a continuación.',
'email_not_confirmed_resend_button' => 'Reenviar correo electrónico de confirmación',
+
+ // User Invite
+ 'user_invite_email_subject' => 'Lo invitaron a unirse a :appName!',
+ 'user_invite_email_greeting' => 'Se creó una cuenta para usted en :appName.',
+ 'user_invite_email_text' => 'Presione el botón de abajo para establecer una contraseña y tener acceso access:',
+ 'user_invite_email_action' => 'Establecer la contraseña de la cuenta',
+ 'user_invite_page_welcome' => 'Bienvenido a :appName!',
+ 'user_invite_page_text' => 'Para finalizar la cuenta y tener acceso debe establcer una contraseña que utilizará para ingresar a :appName en visitas futuras.',
+ 'user_invite_page_confirm_button' => 'Confirmar Contraseña',
+ 'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!'
];
<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ *
+ * Elementos comunes encontrados en varias áreas de BookStack.
+ */
return [
- /**
- * Buttons
- */
+ // Buttons - Botones
'cancel' => 'Cancelar',
'confirm' => 'Confirmar',
'back' => 'Atrás',
'save' => 'Guardar',
'continue' => 'Continuar',
'select' => 'Seleccionar',
+ 'toggle_all' => 'Alternar todo',
'more' => 'Más',
- /**
- * Form Labels
- */
+ // Form Labels - Etiquetas de Formularios
'name' => 'Nombre',
'description' => 'Descripción',
'role' => 'Rol',
'cover_image' => 'Imagen de cubierta',
'cover_image_description' => 'Esta imagen debe ser de 440x250px aproximadamente.',
- /**
- * Actions
- */
+ // Actions - Acciones
'actions' => 'Acciones',
'view' => 'Ver',
+ 'view_all' => 'Ver todo',
'create' => 'Crear',
'update' => 'Actualizar',
'edit' => 'Editar',
'remove' => 'Remover',
'add' => 'Agregar',
- /**
- * Misc
- */
+ // Sort Options - Opciones de filtro
+ 'sort_options' => 'Opciones de Orden',
+ 'sort_direction_toggle' => 'Cambiar Dirección de Orden',
+ 'sort_ascending' => 'Orden Ascendente',
+ 'sort_descending' => 'Orden Descendente',
+ 'sort_name' => 'Nombre',
+ 'sort_created_at' => 'Fecha de creación',
+ 'sort_updated_at' => 'Fecha de actualización',
+
+ //Misc
'deleted_user' => 'Usuario borrado',
'no_activity' => 'Ninguna actividad para mostrar',
'no_items' => 'No hay items disponibles',
"grid_view" => "Vista de grilla",
"list_view" => "Vista de lista",
'default' => 'Por defecto',
+ 'breadcrumb' => 'Miga de Pan',
- /**
- * Header
- */
+ // Header - Cabecera
+ 'profile_menu' => 'Menu del Perfil',
'view_profile' => 'Ver Perfil',
'edit_profile' => 'Editar Perfil',
- /**
- * Email Content
- */
+ // Layout tabs - Pestañas de visualización
+ 'tab_info' => 'Información',
+ 'tab_content' => 'Contenido',
+
+ // Email Content - Contenido de correo Electrónico
'email_action_help' => 'Si está teniendo problemas haga click en el botón ":actionText", copie y pegue la siguiente URL en su navegador web:',
'email_rights' => 'Todos los derechos reservados',
];
<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ *
+ * Texto utilizado para 'Entities' (Elementos de Estructura de
+ * los Documentos) como Libros, Estantes, Capítulos y Páginas
+ */
return [
- /**
- * Shared
- */
- 'recently_created' => 'Recientemente creado',
- 'recently_created_pages' => 'Páginas recientemente creadas',
- 'recently_updated_pages' => 'Páginas recientemente actualizadas',
- 'recently_created_chapters' => 'Capítulos recientemente creados',
- 'recently_created_books' => 'Libros recientemente creados',
- 'recently_update' => 'Recientemente actualizado',
- 'recently_viewed' => 'Recientemente visto',
+ // Shared - Compartido
+ 'recently_created' => 'Creado recientemente',
+ 'recently_created_pages' => 'Páginas creadas recientemente',
+ 'recently_updated_pages' => 'Páginas actualizadas recientemente',
+ 'recently_created_chapters' => 'Capítulos creados recientemente',
+ 'recently_created_books' => 'Libros creados recientemente',
+ 'recently_created_shelves' => 'Estantes creados recientemente',
+ 'recently_update' => 'Actaulizado recientemente',
+ 'recently_viewed' => 'Visto recientemente',
'recent_activity' => 'Actividad reciente',
'create_now' => 'Crear uno ahora',
'revisions' => 'Revisiones',
'export_pdf' => 'Archivo PDF',
'export_text' => 'Archivo de texto plano',
- /**
- * Permissions and restrictions
- */
+ // Permissions and restrictions - Permisos y Restricciones
'permissions' => 'Permisos',
'permissions_intro' => 'una vez habilitado, Estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
'permissions_enable' => 'Habilitar permisos custom',
'permissions_save' => 'Guardar permisos',
- /**
- * Search
- */
+ // Search - Búsqueda
'search_results' => 'Buscar resultados',
'search_total_results_found' => ':count resultados encontrados|:count total de resultados encontrados',
'search_clear' => 'Limpiar resultados',
'search_set_date' => 'Esablecer fecha',
'search_update' => 'Actualizar búsqueda',
- /**
- * Shelves
- */
+ // Shelves - Estantes
'shelf' => 'Estante',
'shelves' => 'Estantes',
+ 'x_shelves' => ':count Estante|:count Estantes',
'shelves_long' => 'Estantes de libros',
'shelves_empty' => 'No se crearon estantes',
'shelves_create' => 'Crear un estante nuevo',
'shelves_popular' => 'Estantes Populares',
'shelves_new' => 'Estantes Nuevos',
+ 'shelves_new_action' => 'Estante Nuevo',
'shelves_popular_empty' => 'Los estantes más populares aparecerán aquí.',
'shelves_new_empty' => 'Los estantes mas nuevos aparecerán aquí.',
'shelves_save' => 'Guardar estantes',
'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos de este estante a todos los libros contenidos en él. Antes de activarlos, asegúrese que los cambios a los permisos de este estante estén guardados.',
'shelves_copy_permission_success' => 'Se copiaron los permisos del estante a :count libros',
- /**
- * Books
- */
+ // Books - Libros
'book' => 'Libro',
'books' => 'Libros',
'x_books' => ':count Libro|:count Libros',
'books_popular' => 'Libros populares',
'books_recent' => 'Libros recientes',
'books_new' => 'Libros nuevos',
+ 'books_new_action' => 'Libro nuevo',
'books_popular_empty' => 'Los libros más populares aparecerán aquí.',
'books_new_empty' => 'Los libros creados más recientemente aparecerán aquí.',
'books_create' => 'Crear nuevo libro',
'books_permissions_updated' => 'Permisos de libro actualizados',
'books_empty_contents' => 'Ninguna página o capítulo ha sido creada para este libro.',
'books_empty_create_page' => 'Crear una nueva página',
- 'books_empty_or' => 'ó',
'books_empty_sort_current_book' => 'Organizar el libro actual',
'books_empty_add_chapter' => 'Agregar un capítulo',
'books_permissions_active' => 'Permisos de libro activados',
'books_navigation' => 'Navegación de libro',
'books_sort' => 'Organizar contenido de libro',
'books_sort_named' => 'Organizar libro :bookName',
+ 'books_sort_name' => 'Organizar por nombre',
+ 'books_sort_created' => 'Organizar por fecha de creación',
+ 'books_sort_updated' => 'Organizar por fecha de actualización',
+ 'books_sort_chapters_first' => 'Capítulos primero',
+ 'books_sort_chapters_last' => 'Capítulos al final',
'books_sort_show_other' => 'Mostrar otros libros',
'books_sort_save' => 'Guardar nuevo orden',
- /**
- * Chapters
- */
+ // Chapters - Capítulos
'chapter' => 'Capítulo',
'chapters' => 'Capítulos',
'x_chapters' => ':count Capítulo|:count Capítulos',
'chapters_permissions_success' => 'Permisos de capítulo actualizados',
'chapters_search_this' => 'Buscar en este capítulo',
- /**
- * Pages
- */
+ // Pages - Páginas
'page' => 'Página',
'pages' => 'Páginas',
'x_pages' => ':count Página|:count Páginas',
'pages_delete_confirm' => '¿Está seguro de borrar esta página?',
'pages_delete_draft_confirm' => 'Está seguro de que desea borrar este borrador de página?',
'pages_editing_named' => 'Editando página :pageName',
- 'pages_edit_toggle_header' => 'Alternar cabecera',
+ 'pages_edit_draft_options' => 'Opciones de borrador',
'pages_edit_save_draft' => 'Guardar borrador',
'pages_edit_draft' => 'Editar borrador de página',
'pages_editing_draft' => 'Editando borrador',
'pages_revisions_created_by' => 'Creado por',
'pages_revisions_date' => 'Fecha de revisión',
'pages_revisions_number' => '#',
+ 'pages_revisions_numbered' => 'Revisión #:id',
+ 'pages_revisions_numbered_changes' => 'Cambios de Revisión #:id',
'pages_revisions_changelog' => 'Registro de cambios',
'pages_revisions_changes' => 'Cambios',
'pages_revisions_current' => 'Versión actual',
],
'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
'pages_specific' => 'Página Específica',
+ 'pages_is_template' => 'Plantilla de Página',
-
- /**
- * Editor sidebar
- */
+ // Editor sidebar - Barra lateral del editor
'page_tags' => 'Etiquetas de página',
'chapter_tags' => 'Etiquetas de capítulo',
'book_tags' => 'Etiquetas de libro',
'shelf_tags' => 'Shelf Tags',
'tag' => 'Etiqueta',
'tags' => 'Etiquetas',
+ 'tag_name' => 'Nombre de etiqueta',
'tag_value' => 'Valor de la etiqueta (Opcional)',
'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Se puede asignar un valor a una etiqueta para una organizacón con mayor detalle.",
'tags_add' => 'Agregar otra etiqueta',
+ 'tags_remove' => 'Eliminar esta etiqueta',
'attachments' => 'Adjuntos',
'attachments_explain' => 'Subir archivos o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
'attachments_explain_instant_save' => 'Los cambios se guardan de manera instantánea.',
'attachments_file_uploaded' => 'Archivo subido exitosamente',
'attachments_file_updated' => 'Archivo actualizado exitosamente',
'attachments_link_attached' => 'Enlace agregado exitosamente a la página',
+ 'templates' => 'Plantillas',
+ 'templates_set_as_template' => 'La Página es una plantilla',
+ 'templates_explain_set_as_template' => 'Puede establecer esta página como plantilla para que el contenido pueda utilizarse para al crear otras páginas. Otris usuarios podrán utilizar esta plantilla si tienen permisos para ver de esta página.',
+ 'templates_replace_content' => 'Reemplazar el contenido de la página',
+ 'templates_append_content' => 'Incorporar al fina del contenido de la página',
+ 'templates_prepend_content' => 'Incorporar al principio del contenido de la página',
- /**
- * Profile View
- */
+ // Profile View - Vista de Perfil
'profile_user_for_x' => 'Usuario para :time',
'profile_created_content' => 'Contenido creado',
- 'profile_not_created_pages' => ':userName no ha creado ninguna página',
- 'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
- 'profile_not_created_books' => ':userName no ha creado ningún libro',
+ 'profile_not_created_pages' => ':userName no ha creado páginas',
+ 'profile_not_created_chapters' => ':userName no ha creado capítulos',
+ 'profile_not_created_books' => ':userName no ha creado libros',
+ 'profile_not_created_shelves' => ':userName no ha creado estantes',
+
- /**
- * Comments
- */
+ // Comments - Comentarios
'comment' => 'Comentario',
'comments' => 'Comentarios',
'comment_add' => 'Agregar comentario',
'comment_delete_confirm' => '¿Está seguro que quiere borrar este comentario?',
'comment_in_reply_to' => 'En respuesta a :commentId',
- /**
- * Revision
- */
- 'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
+ // Revision - Revisión
+ 'revision_delete_confirm' => '¿Está seguro de que quiere eliminar esta revisión?',
+ 'revision_restore_confirm' => '¿Está seguro de que quiere restaurar esta revisión? Se reemplazará el contenido de la página actual.',
'revision_delete_success' => 'Revisión eliminada',
'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.'
];
'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
'social_driver_not_found' => 'Driver social no encontrado',
'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
+ 'invite_token_expired' => 'El enace de la esta invitación expiró. Puede intentar restablecer la contraseña de su cuenta',
// System
'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
<?php
-
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ *
+ * Cadenas de texto de la configuración
+ * Contiene todas las cadenas de texto usadas en la sección de configuración general
+ * de BookStack, incluyendo usuarios y roles.
+ */
return [
- /**
- * Settings text strings
- * Contains all text strings used in the general settings sections of BookStack
- * including users and roles.
- */
-
+ // Common Messages - Mensajes Comunes
'settings' => 'Ajustes',
'settings_save' => 'Guardar ajustes',
'settings_save_success' => 'Ajustes guardados',
- /**
- * App settings
- */
-
- 'app_settings' => 'Ajustes de Aplicación',
+ // App Settings - Configuraciones de la aplicación
+ 'app_customization' => 'Personalización',
+ 'app_features_security' => 'Características y Seguridad',
'app_name' => 'Nombre de aplicación',
'app_name_desc' => 'Este nombre se muestra en la cabecera y en cualquier correo electrónico de la aplicación',
'app_name_header' => '¿Mostrar el nombre de la aplicación en la cabecera?',
+ 'app_public_access' => 'Acceso Público',
+ 'app_public_access_desc' => 'Habilitar esta opción permitirá a los visitantes, que no estén autenticados, acceder al contenido en la instancia de BookStack.',
+ 'app_public_access_desc_guest' => 'El acceso de visitantes públicos se puede controlar mediante el usuario "Guest/Invitado".',
+ 'app_public_access_toggle' => 'Permitir el acceso público',
'app_public_viewing' => '¿Permitir vista pública?',
'app_secure_images' => '¿Habilitar mayor seguridad para subir imágenes?',
+ 'app_secure_images_toggle' => 'Habilitar seguridad alta para subir imágenes',
'app_secure_images_desc' => 'Por razones de rendimiento, todas las imágenes son públicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los índices de directorios no están habilitados para prevenir el acceso fácil a las imágenes.',
'app_editor' => 'Editor de página',
'app_editor_desc' => 'Seleccione cuál editor será usado por todos los usuarios para editar páginas.',
'app_custom_html' => 'Contenido de cabecera HTML personalizable',
'app_custom_html_desc' => 'Cualquier contenido agregado aquí será agregado al final de la sección <head> de cada página. Esto es útil para sobreescribir estilos o agregar código para analíticas.',
+ 'app_custom_html_disabled_notice' => 'El contenido personailzado para la cabecera HTML está deshabilitado en esta configuración para garantizar que cualquier cambio importante se pueda revertir.',
'app_logo' => 'Logo de la aplicación',
'app_logo_desc' => 'Esta imagen debería ser de 43px en altura. <br>Las imágenes grandes seán escaladas.',
'app_primary_color' => 'Color primario de la aplicación',
'app_homepage_desc' => 'Seleccione una página de inicio para mostrar en lugar de la vista por defecto. Se ignoran los permisos de página para las páginas seleccionadas.',
'app_homepage_select' => 'Seleccione una página',
'app_disable_comments' => 'Deshabilitar comentarios',
+ 'app_disable_comments_toggle' => 'Deshabilitar comentarios',
'app_disable_comments_desc' => 'Deshabilitar comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',
- /**
- * Registration settings
- */
-
+ // Registration settings - Configuraciones de registro
'reg_settings' => 'Ajustes de registro',
- 'reg_allow' => '¿Permitir registro?',
+ 'reg_enable' => 'Habilitar Registro',
+ 'reg_enable_toggle' => 'Habilitar registro',
+ 'reg_enable_desc' => 'Cuando se habilita el registro, el usuario podrá crear su usuario en la aplicación. Con el regsitro, se le otorga un rol de usuario único y por defecto.',
'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
- 'reg_confirm_email' => '¿Requerir correo electrónico de confirmación?',
+ 'reg_email_confirmation' => 'Confirmación de correo electrónico',
+ 'reg_email_confirmation_toggle' => 'Requerir confirmación de correo electrónico',
'reg_confirm_email_desc' => 'Si se utiliza la restricción por dominio, entonces se requerirá la confirmación por correo electrónico y se ignorará el valor a continuación.',
'reg_confirm_restrict_domain' => 'Restringir registro al dominio',
'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los correos electrónicos del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les será enviado un correo elctrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Note que a los usuarios se les permitirá cambiar sus direcciones de correo electrónico luego de un registro éxioso.',
'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
- /**
- * Maintenance settings
- */
-
+ // Maintenance settings - Configuraciones de mantenimiento
'maint' => 'Mantenimiento',
'maint_image_cleanup' => 'Limpiar imágenes',
'maint_image_cleanup_desc' => "Analizar contenido de páginas y revisiones para detectar cuáles imágenes y dibujos están en uso y cuáles son redundantes. Asegúrese de crear un respaldo completo de imágenes y base de datos antes de ejecutar esta tarea.",
'maint_image_cleanup_success' => 'Se encontraron y se eliminaron :count imágenes pontencialmente sin uso!',
'maint_image_cleanup_nothing_found' => 'No se encotraron imágenes sin usar, Nada eliminado!',
- /**
- * Role settings
- */
-
+ // Role settings - Configuraciones de roles
'roles' => 'Roles',
'role_user_roles' => 'Roles de usuario',
'role_create' => 'Crear nuevo rol',
'role_manage_users' => 'Gestionar usuarios',
'role_manage_roles' => 'Gestionar roles y permisos de roles',
'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y páginas',
- 'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
+ 'role_manage_own_entity_permissions' => 'Gestionar permisos en libro
+ s propios, capítulos y páginas',
+ 'role_manage_page_templates' => 'Gestionar las plantillas de páginas',
'role_manage_settings' => 'Gestionar ajustes de activos',
'role_asset' => 'Permisos de activos',
'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y Páginas sobreescribiran estos permisos.',
'role_users' => 'Usuarios en este rol',
'role_users_none' => 'No hay usuarios asignados a este rol',
- /**
- * Users
- */
-
+ // Users - Usuarios
'users' => 'Usuarios',
'user_profile' => 'Perfil de usuario',
'users_add_new' => 'Agregar nuevo usuario',
'users_search' => 'Buscar usuarios',
+ 'users_details' => 'Detalles del usuario',
+ 'users_details_desc' => 'Asigne un nombre de visualización y una dirección de correo electrónico para este usuario. La dirección de correo electrónico se usará pra ingresar a la aplicación.',
+ 'users_details_desc_no_email' => 'Asigne un nombre de visualización a este usuario para que los demás puedan reconocerlo.',
'users_role' => 'Roles de usuario',
+ 'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
+ 'users_password' => 'User Password',
+ 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 5 characters long.',
+ 'users_send_invite_text' => 'Puede optar por enviar a este usuario un correo electrónico de invitación que les permita establecer su propia contraseña; de lo contrario, puede establecerla contraseña usted mismo.',
+ 'users_send_invite_option' => 'Enviar correo electrónico de invitación al usuario.',
'users_external_auth_id' => 'ID externo de autenticación',
+ 'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your LDAP system.',
'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sino que es asignado automáticamente.',
'users_delete' => 'Borrar usuario',
'users_avatar' => 'Avatar del usuario',
'users_avatar_desc' => 'Esta imagen debe ser de aproximadamente 256px por lado.',
'users_preferred_language' => 'Lenguaje preferido',
+ 'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
'users_social_accounts' => 'Cuentas sociales',
'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un acceso rápido y más fácil. Desconectando una cuenta aquí no revoca accesos ya autorizados. Revoque el acceso desde los ajustes de perfil en la cuenta social conectada.',
'users_social_connect' => 'Conectar cuenta',
<?php
-
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the size rules. Feel free to tweak each of these messages here.
+ *
+ * Líneas de validación
+ * Las líneas de lenguaje siguientes contienen los mensajes de error por
+ * defecto usados por el validador de la clase. Algunas de esta reglas tienen
+ * varias versiones, como las reglas de tamaño. Siéntase libre de ajustar cada
+ * uno de los mensajes.
+ */
return [
- /*
- |--------------------------------------------------------------------------
- | Validation Language Lines
- |--------------------------------------------------------------------------
- |
- | The following language lines contain the default error messages used by
- | the validator class. Some of these rules have multiple versions such
- | as the size rules. Feel free to tweak each of these messages here.
- |
- */
-
+ // Standard laravel validation lines - Líneas de validación estándar de laravel
'accepted' => 'El :attribute debe ser aceptado.',
'active_url' => 'El :attribute no es una URl válida.',
'after' => 'El :attribute debe ser una fecha posterior :date.',
'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => ':attribute debe ser una dirección álida.',
+ 'ends_with' => 'El :attribute debe terminar con uno de los siguientes: :values',
'filled' => 'El campo :attribute es requerido.',
+ 'gt' => [
+ 'numeric' => 'El :attribute debe ser mayor que :value.',
+ 'file' => 'El :attribute debe ser mayor que :value kilobytes.',
+ 'string' => 'El :attribute debe ser mayor que :value caracteres.',
+ 'array' => 'El :attribute debe tener más de :value objetos.',
+ ],
+ 'gte' => [
+ 'numeric' => 'El :attribute debe ser mayor o igual a :value.',
+ 'file' => 'El :attribute debe ser mayor o igual a :value kilobytes.',
+ 'string' => 'El :attribute debe ser mayor o igual a :value caracteres.',
+ 'array' => 'El :attribute debe tener :value objetos o más.',
+ ],
'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'El :attribute debe ser una imagen.',
'in' => 'El selected :attribute es inválio.',
+ 'image_extension' => 'El :attribute debe tener una extensión de imagen válida y soportada.',
'integer' => 'El :attribute debe ser un entero.',
'ip' => 'El :attribute debe ser una dirección IP álida.',
+ 'ipv4' => 'El :attribute debe ser una dirección IPv4 válida.',
+ 'ipv6' => 'El :attribute debe ser una dirección IPv6 válida.',
+ 'json' => 'El :attribute debe ser una cadena JSON válida.',
+ 'lt' => [
+ 'numeric' => 'El :attribute debe ser menor que :value.',
+ 'file' => 'El :attribute debe ser menor que :value kilobytes.',
+ 'string' => 'El :attribute debe ser menor que :value caracteres.',
+ 'array' => 'El :attribute debe tener menos de :value objetos.',
+ ],
+ 'lte' => [
+ 'numeric' => 'El :attribute debe ser menor o igual a :value.',
+ 'file' => 'El :attribute debe ser menor o igual a :value kilobytes.',
+ 'string' => 'El :attribute debe ser menor o igual a :value caracteres.',
+ 'array' => 'El :attribute no debe tener más de :value objetos.',
+ ],
'max' => [
'numeric' => ':attribute no puede ser mayor que :max.',
'file' => ':attribute no puede ser mayor que :max kilobytes.',
'string' => ':attribute debe ser al menos :min caracteres.',
'array' => ':attribute debe tener como mínimo :min items.',
],
- 'not_in' => ':attribute seleccionado es inválio.',
+ 'no_double_extension' => 'El :attribute debe tener una única extensión de archivo.',
+ 'not_in' => ':attribute seleccionado es inválido.',
+ 'not_regex' => 'El formato de :attribute es inválido.',
'numeric' => ':attribute debe ser numérico.',
'regex' => ':attribute con formato inválido',
'required' => ':attribute es requerido.',
'string' => 'El atributo :attribute debe ser una cadena.',
'timezone' => 'El atributo :attribute debe ser una zona válida.',
'unique' => 'El atributo :attribute ya ha sido tomado.',
- 'url' => 'El atributo :attribute tiene un formato inválid.',
- 'is_image' => 'El atributo :attribute debe ser una imagen válida.',
-
- /*
- |--------------------------------------------------------------------------
- | Custom Validation Language Lines
- |--------------------------------------------------------------------------
- |
- | Here you may specify custom validation messages for attributes using the
- | convention "attribute.rule" to name the lines. This makes it quick to
- | specify a specific custom language line for a given attribute rule.
- |
- */
+ 'url' => 'El atributo :attribute tiene un formato inválido.',
+ 'uploaded' => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.',
+ // Custom validation lines - Líneas de validación personalizadas
'custom' => [
'password-confirm' => [
'required_with' => 'Confirmación de Password requerida',
],
],
- /*
- |--------------------------------------------------------------------------
- | Custom Validation Attributes
- |--------------------------------------------------------------------------
- |
- | The following language lines are used to swap attribute place-holders
- | with something more reader friendly such as E-Mail Address instead
- | of "email". This simply helps us make messages a little cleaner.
- |
- */
-
+ // Custom validation attributes - Atributos de validación personalizados
'attributes' => [],
];
'email' => 'E-mail',
'password' => 'Mot de passe',
'password_confirm' => 'Confirmez le mot de passe',
- 'password_hint' => 'Doit faire plus de 5 caractères',
- 'forgot_password' => 'Mot de passe oublié ?',
+ 'password_hint' => 'Doit faire plus de 7 caractères',
+ 'forgot_password' => 'Mot de passe oublié ?',
'remember_me' => 'Se souvenir de moi',
- 'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
+ 'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte.",
'create_account' => 'Créer un compte',
- 'already_have_account' => 'Vous avez déjà un compte?',
- 'dont_have_account' => 'Vous n\'avez pas de compte?',
- 'social_login' => 'Social Login',
- 'social_registration' => 'Enregistrement Social',
- 'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
+ 'already_have_account' => 'Vous avez déjà un compte ?',
+ 'dont_have_account' => 'Vous n\'avez pas de compte ?',
+ 'social_login' => 'Connexion avec un réseau social',
+ 'social_registration' => 'Inscription avec un réseau social',
+ 'social_registration_text' => "S'inscrire et se connecter avec un réseau social.",
- 'register_thanks' => 'Merci pour votre enregistrement',
+ 'register_thanks' => 'Merci pour votre inscription !',
'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',
- 'registrations_disabled' => "L'inscription est désactivée pour le moment",
+ 'registrations_disabled' => "Les inscriptions sont désactivées pour le moment",
'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application',
'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
/**
* Password Reset
*/
- 'reset_password' => 'Reset Password',
- 'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé',
+ 'reset_password' => 'Réinitialiser le mot de passe',
+ 'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé.',
'reset_password_send_button' => 'Envoyer un lien de réinitialisation',
'reset_password_sent_success' => 'Un lien de réinitialisation a été envoyé à :email.',
'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
- 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte',
+ 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte.',
'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
* Email Confirmation
*/
'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
- 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
- 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
+ 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
+ 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
'email_confirm_action' => 'Confirmez votre adresse e-mail',
'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
- 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
+ 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
'email_not_confirmed' => 'Adresse e-mail non confirmée',
'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.',
'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',
'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation',
+
+ // User Invite
+ 'user_invite_email_subject' => 'Vous avez été invité(e) à rejoindre :appName !',
+ 'user_invite_email_greeting' => 'Un compte vous a été créé sur :appName.',
+ 'user_invite_email_text' => 'Cliquez sur le bouton ci-dessous pour renseigner le mot de passe et récupérer l\'accès :',
+ 'user_invite_email_action' => 'Renseignez le mot de passe de votre compte',
+ 'user_invite_page_welcome' => 'Bienvenue dans :appName !',
+ 'user_invite_page_text' => 'Pour finaliser votre compte et recevoir l\'accès, vous devez renseigner le mot de passe qui sera utilisé pour la connexion à :appName les prochaines fois.',
+ 'user_invite_page_confirm_button' => 'Confirmez le mot de passe',
+ 'user_invite_success' => 'Mot de passe renseigné, vous avez maintenant accès à :appName !'
];
'description' => 'Description',
'role' => 'Rôle',
'cover_image' => 'Image de couverture',
- 'cover_image_description' => 'Cette image doit être environ 440x250px.',
+ 'cover_image_description' => 'Cette image doit faire environ 440x250 px.',
/**
* Actions
/**
* Sort Options
*/
+ 'sort_options' => 'Options de tri',
+ 'sort_direction_toggle' => 'Inverser la direction du tri',
+ 'sort_ascending' => 'Tri ascendant',
+ 'sort_descending' => 'Tri descendant',
'sort_name' => 'Nom',
'sort_created_at' => 'Date de création',
'sort_updated_at' => 'Date de mise à jour',
'grid_view' => 'Vue en grille',
'list_view' => 'Vue en liste',
'default' => 'Défaut',
+ 'breadcrumb' => 'Fil d\'Ariane',
/**
* Header
*/
+ 'profile_menu' => 'Menu du profil',
'view_profile' => 'Voir le profil',
'edit_profile' => 'Modifier le profil',
+ // Layout tabs
+ 'tab_info' => 'Info',
+ 'tab_content' => 'Contenu',
+
/**
* Email Content
*/
- 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
+ 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
'email_rights' => 'Tous droits réservés',
];
'shelves_delete' => 'Supprimer l\'étagère',
'shelves_delete_named' => 'Supprimer l\'étagère :name',
'shelves_delete_explain' => "Ceci va supprimer l\'étagère nommée \':bookName\'. Les livres contenus dans cette étagère ne seront pas supprimés.",
- 'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
+ 'shelves_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer cette étagère ?',
'shelves_permissions' => 'Permissions de l\'étagère',
'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour',
'shelves_permissions_active' => 'Permissions de l\'étagère activées',
'books_delete' => 'Supprimer un livre',
'books_delete_named' => 'Supprimer le livre :bookName',
'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.',
- 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
+ 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
'books_edit' => 'Modifier le livre',
'books_edit_named' => 'Modifier le livre :bookName',
'books_form_book_name' => 'Nom du livre',
'chapters_delete' => 'Supprimer le chapitre',
'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
- 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
+ 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
'chapters_edit' => 'Modifier le chapitre',
'chapters_edit_named' => 'Modifier le chapitre :chapterName',
'chapters_save' => 'Enregistrer le chapitre',
'pages_delete_draft' => 'Supprimer le brouillon',
'pages_delete_success' => 'Page supprimée',
'pages_delete_draft_success' => 'Brouillon supprimé',
- 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
- 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
+ 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
+ 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
'pages_editing_named' => 'Modification de la page :pageName',
'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
'pages_edit_save_draft' => 'Enregistrer le brouillon',
'start_b' => ':userName a commencé à éditer cette page',
'time_a' => 'depuis la dernière sauvegarde',
'time_b' => 'dans les :minCount dernières minutes',
- 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
+ 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
],
'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.',
'pages_specific' => 'Page Spécifique',
+ 'pages_is_template' => 'Modèle de page',
/**
* Editor sidebar
'attachments_file_uploaded' => 'Fichier ajouté avec succès',
'attachments_file_updated' => 'Fichier mis à jour avec succès',
'attachments_link_attached' => 'Lien attaché à la page avec succès',
+ 'templates' => 'Modèles',
+ 'templates_set_as_template' => 'La page est un modèle',
+ 'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
+ 'templates_replace_content' => 'Remplacer le contenu de la page',
+ 'templates_append_content' => 'Ajouter après le contenu de la page',
+ 'templates_prepend_content' => 'Ajouter devant le contenu de la page',
/**
* Profile View
'comments' => 'Commentaires',
'comment_add' => 'Ajouter un commentaire',
'comment_placeholder' => 'Entrez vos commentaires ici',
- 'comment_count' => '{0} Pas de commentaires|{1} 1 Commentaire|[2,*] :count Commentaires',
+ 'comment_count' => '{0} Pas de commentaires|{1} Un commentaire|[2,*] :count commentaires',
'comment_save' => 'Enregistrer le commentaire',
- 'comment_saving' => 'Enregistrement du commentaire...',
- 'comment_deleting' => 'Suppression du commentaire...',
+ 'comment_saving' => 'Enregistrement du commentaire…',
+ 'comment_deleting' => 'Suppression du commentaire…',
'comment_new' => 'Nouveau commentaire',
'comment_created' => 'commenté :createDiff',
'comment_updated' => 'Mis à jour :updateDiff par :username',
'comment_deleted_success' => 'Commentaire supprimé',
'comment_created_success' => 'Commentaire ajouté',
'comment_updated_success' => 'Commentaire mis à jour',
- 'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
+ 'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
'comment_in_reply_to' => 'En réponse à :commentId',
/**
* Revision
*/
- 'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision?',
+ 'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision ?',
+ 'revision_restore_confirm' => 'Êtes-vous sûr de vouloir restaurer cette révision ? Le contenu courant de la page va être remplacé.',
'revision_delete_success' => 'Révision supprimée',
'revision_cannot_delete_latest' => 'Impossible de supprimer la dernière révision.'
];
'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée',
'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
'social_no_action_defined' => 'Pas d\'action définie',
- 'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
+ 'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
'social_driver_not_found' => 'Pilote de compte social absent',
'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',
+ 'invite_token_expired' => 'Le lien de cette invitation a expiré. Vous pouvez essayer de réinitiliser votre mot de passe.',
// System
'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',
'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
+ 'role_cannot_remove_only_admin' => 'Ceci est le seul compte administrateur. Assignez un nouvel administrateur avant de le supprimer ici.',
// Error pages
'404_page_not_found' => 'Page non trouvée',
'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
'token' => 'Le jeton de réinitialisation est invalide.',
- 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
- 'reset' => 'Votre mot de passe a été réinitialisé !',
+ 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
+ 'reset' => 'Votre mot de passe a été réinitialisé !',
];
'app_features_security' => 'Fonctionnalités et sécurité',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
- 'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
+ 'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
'app_public_access' => 'Accès public',
'app_public_access_desc' => 'L\'activation de cette option permettra aux visiteurs, qui ne sont pas connectés, d\'accéder au contenu de votre instance BookStack.',
'app_public_access_desc_guest' => 'L\'accès pour les visiteurs publics peut être contrôlé par l\'utilisateur "Guest".',
'app_public_access_toggle' => 'Autoriser l\'accès public',
- 'app_public_viewing' => 'Accepter le visionnage public des pages ?',
- 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
+ 'app_public_viewing' => 'Accepter le visionnage public des pages ?',
+ 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
'app_secure_images_toggle' => 'Activer l\'ajout d\'image sécurisé',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_editor' => 'Editeur des pages',
'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur pourra s\'enregistrer en tant qu\'utilisateur de l\'application. Lors de l\'inscription, ils se voient attribuer un rôle par défaut.',
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
'reg_email_confirmation' => 'Confirmation de l\'e-mail',
- 'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
+ 'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?',
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.",
'maint_image_cleanup_ignore_revisions' => 'Ignorer les images dans les révisions',
'maint_image_cleanup_run' => 'Lancer le nettoyage',
- 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
- 'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
- 'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
+ 'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
+ 'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
+ 'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
/**
* Role settings
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
- 'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
+ 'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
- 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
+ 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_asset' => 'Permissions des ressources',
'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
- 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
+ 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
'users_delete_success' => 'Utilisateurs supprimés avec succès',
'users_edit' => 'Modifier l\'utilisateur',
'users_edit_profile' => 'Modifier le profil',
'users_edit_success' => 'Utilisateur mis à jour avec succès',
'users_avatar' => 'Avatar de l\'utilisateur',
- 'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256px.',
+ 'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256 px.',
'users_preferred_language' => 'Langue préférée',
'users_preferred_language_desc' => 'Cette option changera la langue utilisée pour l\'interface utilisateur de l\'application. Ceci n\'affectera aucun contenu créé par l\'utilisateur.',
'users_social_accounts' => 'Comptes sociaux',
'digits' => ':attribute doit être de longueur :digits.',
'digits_between' => ':attribute doit avoir une longueur entre :min et :max.',
'email' => ':attribute doit être une adresse e-mail valide.',
+ 'ends_with' => ':attribute doit se terminer par une des valeurs suivantes : :values',
'filled' => ':attribute est un champ requis.',
+ 'gt' => [
+ 'numeric' => ':attribute doit être plus grand que :value.',
+ 'file' => ':attribute doit être plus grand que :value kilobytes.',
+ 'string' => ':attribute doit être plus grand que :value caractères.',
+ 'array' => ':attribute doit avoir plus que :value éléments.',
+ ],
+ 'gte' => [
+ 'numeric' => ':attribute doit être plus grand ou égal à :value.',
+ 'file' => ':attribute doit être plus grand ou égal à :value kilobytes.',
+ 'string' => ':attribute doit être plus grand ou égal à :value caractères.',
+ 'array' => ':attribute doit avoir :value éléments ou plus.',
+ ],
'exists' => 'L\'attribut :attribute est invalide.',
'image' => ':attribute doit être une image.',
'image_extension' => ':attribute doit avoir une extension d\'image valide et supportée.',
'in' => 'L\'attribut :attribute est invalide.',
'integer' => ':attribute doit être un chiffre entier.',
'ip' => ':attribute doit être une adresse IP valide.',
+ 'ipv4' => ':attribute doit être une adresse IPv4 valide.',
+ 'ipv6' => ':attribute doit être une adresse IPv6 valide.',
+ 'json' => ':attribute doit être une chaine JSON valide.',
+ 'lt' => [
+ 'numeric' => ':attribute doit être plus petit que :value.',
+ 'file' => ':attribute doit être plus petit que :value kilobytes.',
+ 'string' => ':attribute doit être plus petit que :value caractères.',
+ 'array' => ':attribute doit avoir moins de :value éléments.',
+ ],
+ 'lte' => [
+ 'numeric' => ':attribute doit être plus petit ou égal à :value.',
+ 'file' => ':attribute doit être plus petit ou égal à :value kilobytes.',
+ 'string' => ':attribute doit être plus petit ou égal à :value caractères.',
+ 'array' => ':attribute ne doit pas avoir plus de :value éléments.',
+ ],
'max' => [
'numeric' => ':attribute ne doit pas excéder :max.',
'file' => ':attribute ne doit pas excéder :max kilobytes.',
],
'no_double_extension' => ':attribute ne doit avoir qu\'une seule extension de fichier.',
'not_in' => 'L\'attribut sélectionné :attribute est invalide.',
+ 'not_regex' => ':attribute a un format invalide.',
'numeric' => ':attribute doit être un nombre.',
'regex' => ':attribute a un format invalide.',
'required' => ':attribute est un champ requis.',
'email' => 'Email',
'password' => 'Jelszó',
'password_confirm' => 'Jelszó megerősítése',
- 'password_hint' => 'Öt karakternél hosszabbnak kell lennie',
+ 'password_hint' => 'Négy karakternél hosszabbnak kell lennie',
'forgot_password' => 'Elfelejtett jelszó?',
'remember_me' => 'Emlékezzen rám',
'ldap_email_hint' => 'A fiókhoz használt email cím megadása.',
// Email Confirmation
- 'email_confirm_subject' => ':appName alklamazásban beállított email címet meg kell erősíteni',
+ 'email_confirm_subject' => ':appName alkalmazásban beállított email címet meg kell erősíteni',
'email_confirm_greeting' => ':appName köszöni a csatlakozást!',
'email_confirm_text' => 'Az email címet a lenti gombra kattintva lehet megerősíteni:',
'email_confirm_action' => 'Email megerősítése',
'email_not_confirmed_click_link' => 'Rá kell kattintani a regisztráció után nem sokkal elküldött emailben található hivatkozásra.',
'email_not_confirmed_resend' => 'Ha nem érkezik meg a megerősítő email, a lenti űrlap beküldésével újra lehet küldeni.',
'email_not_confirmed_resend_button' => 'Megerősítő email újraküldése',
-];
\ No newline at end of file
+];
'email' => 'Email',
'password' => 'Password',
'password_confirm' => 'Conferma Password',
- 'password_hint' => 'Deve essere più di 5 caratteri',
+ 'password_hint' => 'Deve essere più di 7 caratteri',
'forgot_password' => 'Password dimenticata?',
'remember_me' => 'Ricordami',
'ldap_email_hint' => 'Inserisci un email per usare quest\'account.',
'email' => 'メールアドレス',
'password' => 'パスワード',
'password_confirm' => 'パスワード (確認)',
- 'password_hint' => '5文字以上である必要があります',
+ 'password_hint' => '7文字以上である必要があります',
'forgot_password' => 'パスワードをお忘れですか?',
'remember_me' => 'ログイン情報を保存する',
'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
'email' => '이메일',
'password' => '비밀번호',
'password_confirm' => '비밀번호 (확인)',
- 'password_hint' => '5자 이상이어야 합니다.',
+ 'password_hint' => '7자 이상이어야 합니다.',
'forgot_password' => '비밀번호를 잊으셨습니까?',
'remember_me' => '자동로그인',
'ldap_email_hint' => '이 계정에서 사용하는 이메일을 입력해 주세요.',
'email' => 'Email',
'password' => 'Wachtwoord',
'password_confirm' => 'Wachtwoord Bevestigen',
- 'password_hint' => 'Minimaal 6 tekens',
+ 'password_hint' => 'Minimaal 8 tekens',
'forgot_password' => 'Wachtwoord vergeten?',
'remember_me' => 'Mij onthouden',
'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
'email' => 'E-mail',
'password' => 'Hasło',
'password_confirm' => 'Potwierdzenie hasła',
- 'password_hint' => 'Musi mieć więcej niż 5 znaków',
+ 'password_hint' => 'Musi mieć więcej niż 7 znaków',
'forgot_password' => 'Zapomniałem hasła',
'remember_me' => 'Zapamiętaj mnie',
'ldap_email_hint' => 'Wprowadź adres e-mail dla tego konta.',
'email' => 'E-mail',
'password' => 'Senha',
'password_confirm' => 'Confirmar Senha',
- 'password_hint' => 'Senha deverá ser maior que 5 caracteres',
+ 'password_hint' => 'Senha deverá ser maior que 7 caracteres',
'forgot_password' => 'Esqueceu a senha?',
'remember_me' => 'Lembrar de mim',
'ldap_email_hint' => 'Por favor, digite um e-mail para essa conta.',
<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
return [
- /*
- |--------------------------------------------------------------------------
- | Authentication Language Lines
- |--------------------------------------------------------------------------
- |
- | The following language lines are used during authentication for various
- | messages that we need to display to the user. You are free to modify
- | these language lines according to your application's requirements.
- |
- */
+
'failed' => 'Учетная запись не найдена.',
'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте позже через :seconds секунд.',
- /**
- * Login & Register
- */
+ // Login & Register
'sign_up' => 'Регистрация',
'log_in' => 'Вход',
'log_in_with' => 'Вход с :socialDriver',
'email' => 'Email',
'password' => 'Пароль',
'password_confirm' => 'Подтверждение пароля',
- 'password_hint' => 'Должен быть больше 5 символов',
+ 'password_hint' => 'Должен быть больше 7 символов',
'forgot_password' => 'Забыли пароль?',
'remember_me' => 'Запомнить меня',
'ldap_email_hint' => 'Введите email адрес для данной учетной записи.',
'create_account' => 'Создать аккаунт',
+ 'already_have_account' => 'Уже есть аккаунт?',
+ 'dont_have_account' => 'У вас нет аккаунта?',
'social_login' => 'Вход через Соцсеть',
'social_registration' => 'Регистрация через Соцсеть',
'social_registration_text' => 'Регистрация и вход через другой сервис.',
'register_success' => 'Спасибо за регистрацию! Регистрация и вход в систему выполнены.',
- /**
- * Password Reset
- */
+ //Password Reset
'reset_password' => 'Сброс пароля',
- 'reset_password_send_instructions' => 'Введите свой адрес электронной почты ниже, и вам будет отправлено письмо со ссылкой для сброса пароля.',
+ 'reset_password_send_instructions' => 'Введите свой email ниже, и вам будет отправлено письмо со ссылкой для сброса пароля.',
'reset_password_send_button' => 'Отправить ссылку для сброса',
'reset_password_sent_success' => 'Ссылка для сброса была отправлена на :email.',
'reset_password_success' => 'Ваш пароль был успешно сброшен.',
-
'email_reset_subject' => 'Сбросить ваш :appName пароль',
- 'email_reset_text' => 'Ð\92Ñ\8b полÑ\83Ñ\87или Ñ\8dÑ\82о пиÑ\81Ñ\8cмо, поÑ\82омÑ\83 Ñ\87Ñ\82о вÑ\8b запÑ\80оÑ\81или Ñ\81бÑ\80оÑ\81 паÑ\80олÑ\8f длÑ\8f ваÑ\88ей Ñ\83Ñ\87еÑ\82ной запиÑ\81и.',
+ 'email_reset_text' => 'Вы получили это письмо, потому что запросили сброс пароля для вашей учетной записи.',
'email_reset_not_requested' => 'Если вы не запрашивали сброса пароля, то никаких дополнительных действий не требуется.',
- /**
- * Email Confirmation
- */
+ //Email Confirmation
'email_confirm_subject' => 'Подтвердите ваш почтовый адрес на :appName',
'email_confirm_greeting' => 'Благодарим за участие :appName!',
'email_confirm_text' => 'Пожалуйста, подтвердите ваш email адрес кликнув на кнопку ниже:',
'email_not_confirmed_click_link' => 'Пожалуйста, нажмите на ссылку в письме, которое было отправлено при регистрации.',
'email_not_confirmed_resend' => 'Если вы не можете найти электронное письмо, вы можете снова отправить письмо с подтверждением по форме ниже.',
'email_not_confirmed_resend_button' => 'Переотправить письмо с подтверждением',
+
+ // User Invite
+ 'user_invite_email_subject' => 'Вас приглашают присоединиться к :appName!',
+ 'user_invite_email_greeting' => 'Для вас создан аккаунт в :appName.',
+ 'user_invite_email_text' => 'Нажмите кнопку ниже, чтобы задать пароль и получить доступ:',
+ 'user_invite_email_action' => 'Установить пароль аккаунту.',
+ 'user_invite_page_welcome' => 'Добро пожаловать в :appName!',
+ 'user_invite_page_text' => 'Завершите настройку аккаунта, установите пароль для дальнейшего входа в :appName.',
+ 'user_invite_page_confirm_button' => 'Подтвердите пароль',
+ 'user_invite_success' => 'Пароль установлен, теперь у вас есть доступ к :appName!'
];
<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
return [
- /**
- * Buttons
- */
+ //Buttons
'cancel' => 'Отмена',
'confirm' => 'Применить',
'back' => 'Назад',
'save' => 'Сохранить',
'continue' => 'Продолжить',
'select' => 'Выбрать',
+ 'toggle_all' => 'Переключить все',
'more' => 'Еще',
- /**
- * Form Labels
- */
+ //Form Labels
'name' => 'Имя',
'description' => 'Описание',
'role' => 'Роль',
'cover_image' => 'Обложка',
'cover_image_description' => 'Изображение должно быть размером около 440x250px.',
- /**
- * Actions
- */
+ //Actions
'actions' => 'Действия',
'view' => 'Просмотр',
'view_all' => 'Показать все',
'add' => 'Добавить',
// Sort Options
+ 'sort_options' => 'Параметры сортировки',
+ 'sort_direction_toggle' => 'Переключить направления сортировки',
+ 'sort_ascending' => 'По возрастанию',
+ 'sort_descending' => 'По убыванию',
'sort_name' => 'По имени',
'sort_created_at' => 'По дате создания',
'sort_updated_at' => 'По дате обновления',
- /**
- * Misc
- */
+ //Misc
'deleted_user' => 'Удаленный пользователь',
'no_activity' => 'Нет действий для просмотра',
'no_items' => 'Нет доступных элементов',
'grid_view' => 'Вид сеткой',
'list_view' => 'Вид списком',
'default' => 'По умолчанию',
+ 'breadcrumb' => 'Навигация',
- /**
- * Header
- */
+ //Header
+ 'profile_menu' => 'Меню профиля',
'view_profile' => 'Просмотреть профиль',
'edit_profile' => 'Редактировать профиль',
'tab_info' => 'Информация',
'tab_content' => 'Содержание',
- /**
- * Email Content
- */
+ //Email Content
'email_action_help' => 'Если у вас возникли проблемы с нажатием кнопки \':actionText\', то скопируйте и вставьте указанный URL-адрес в свой веб-браузер:',
'email_rights' => 'Все права зарезервированы',
];
<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
return [
- /**
- * Image Manager
- */
+ //Image Manager
'image_select' => 'Выбрать изображение',
'image_all' => 'Все',
'image_all_title' => 'Простмотр всех изображений',
'image_delete_success' => 'Изображение успешно удалено',
'image_upload_remove' => 'Удалить изображение',
- /**
- * Code editor
- */
+ //Code editor
'code_editor' => 'Изменить код',
'code_language' => 'Язык кода',
'code_content' => 'Содержимое кода',
<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
return [
- /**
- * Shared
- */
+ //Shared
'recently_created' => 'Недавно созданные',
'recently_created_pages' => 'Недавно созданные страницы',
'recently_updated_pages' => 'Недавно обновленные страницы',
'recently_created_chapters' => 'Недавно созданные главы',
'recently_created_books' => 'Недавно созданные книги',
+ 'recently_created_shelves' => 'Недавно созданные полки',
'recently_update' => 'Недавно обновленные',
'recently_viewed' => 'Недавно просмотренные',
'recent_activity' => 'Недавние действия',
'export_pdf' => 'PDF файл',
'export_text' => 'Текстовый файл',
- /**
- * Permissions and restrictions
- */
+ //Permissions and restrictions
'permissions' => 'Разрешения',
'permissions_intro' => 'После включения эти разрешения будут иметь приоритет над любыми установленными полномочиями.',
'permissions_enable' => 'Включение пользовательских разрешений',
'permissions_save' => 'Сохранить разрешения',
- /**
- * Search
- */
+ //Search
'search_results' => 'Результаты поиска',
'search_total_results_found' => ':count результатов найдено|:count всего результатов найдено',
'search_clear' => 'Очистить поиск',
'search_content_type' => 'Тип содержимого',
'search_exact_matches' => 'Точные соответствия',
'search_tags' => 'Поиск по тегам',
+ 'search_options' => 'Параметры',
'search_viewed_by_me' => 'Просмотрено мной',
'search_not_viewed_by_me' => 'Не просматривалось мной',
'search_permissions_set' => 'Набор разрешений',
'search_created_by_me' => 'Создано мной',
'search_updated_by_me' => 'Обновлено мной',
+ 'search_date_options' => 'Параметры даты',
'search_updated_before' => 'Обновлено до',
'search_updated_after' => 'Обновлено после',
'search_created_before' => 'Создано до',
'search_set_date' => 'Установить дату',
'search_update' => 'Обновить поиск',
- /**
- * Books
- */
+ //Shelves
+ 'shelf' => 'Полка',
+ 'shelves' => 'Полки',
+ 'x_shelves' => ':count полок|:count полок',
+ 'shelves_long' => 'Книжные полки',
+ 'shelves_empty' => 'Полки не созданы',
+ 'shelves_create' => 'Создать новую полку',
+ 'shelves_popular' => 'Популярные полки',
+ 'shelves_new' => 'Новые полки',
+ 'shelves_new_action' => 'Новая полка',
+ 'shelves_popular_empty' => 'Популярные полки появятся здесь.',
+ 'shelves_new_empty' => 'Последние созданные полки появятся здесь.',
+ 'shelves_save' => 'Сохранить полку',
+ 'shelves_books' => 'Книги из этой полки',
+ 'shelves_add_books' => 'Добавить книгу в эту полку',
+ 'shelves_drag_books' => 'Перетащите книгу сюда, чтобы добавить на эту полку',
+ 'shelves_empty_contents' => 'На этой полке нет книг',
+ 'shelves_edit_and_assign' => 'Изменить полку для привязки книг',
+ 'shelves_edit_named' => 'Редактировать полку :name',
+ 'shelves_edit' => 'Редактировать книжную полку',
+ 'shelves_delete' => 'Удалить книжную полку',
+ 'shelves_delete_named' => 'Удалить книжную полку :name',
+ 'shelves_delete_explain' => "Это приведет к удалению полки с именем ':name'. Привязанные книги удалены не будут.",
+ 'shelves_delete_confirmation' => 'Вы уверены, что хотите удалить эту полку?',
+ 'shelves_permissions' => 'Доступы к книжной полке',
+ 'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
+ 'shelves_permissions_active' => 'Доступы к книжной полке активны',
+ 'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
+ 'shelves_copy_permissions' => 'Копировать доступы',
+ 'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
+ 'shelves_copy_permission_success' => 'Доступы книжной полки скопированы для :count books',
+
+ //Books
'book' => 'Книга',
'books' => 'Книги',
'x_books' => ':count книга|:count книг',
'books_popular' => 'Популярные книги',
'books_recent' => 'Недавние книги',
'books_new' => 'Новые книги',
+ 'books_new_action' => 'Новая книга',
'books_popular_empty' => 'Здесь появятся самые популярные книги.',
'books_new_empty' => 'Здесь появятся самые последние созданные книги.',
'books_create' => 'Создать новую книгу',
'books_permissions_updated' => 'Разрешения на книгу обновлены',
'books_empty_contents' => 'Для этой книги нет страниц или разделов.',
'books_empty_create_page' => 'Создать новую страницу',
- 'books_empty_or' => 'или',
'books_empty_sort_current_book' => 'Сортировка текущей книги',
'books_empty_add_chapter' => 'Добавить главу',
'books_permissions_active' => 'действующие разрешения на книгу',
'books_navigation' => 'Навигация по книге',
'books_sort' => 'Сортировка содержимого книги',
'books_sort_named' => 'Сортировка книги :bookName',
+ 'books_sort_name' => 'Сортировать по имени',
+ 'books_sort_created' => 'Сортировать по дате создания',
+ 'books_sort_updated' => 'Сортировать по дате обновления',
+ 'books_sort_chapters_first' => 'Сначала главы',
+ 'books_sort_chapters_last' => 'Главы последние',
'books_sort_show_other' => 'Показать другие книги',
'books_sort_save' => 'Сохранить новый порядок',
- /**
- * Shelves
- */
- 'shelf' => 'Полка',
- 'shelves' => 'Полки',
- 'x_shelves' => ':count полок|:count полок',
- 'shelves_long' => 'Книжные полки',
- 'shelves_empty' => 'Полки не созданы',
- 'shelves_create' => 'Создать новую полку',
- 'shelves_popular' => 'Популярные полки',
- 'shelves_new' => 'Новые полки',
- 'shelves_new_action' => 'Новая полка',
- 'shelves_popular_empty' => 'Популярные полки появятся здесь.',
- 'shelves_new_empty' => 'Последние созданные полки появятся здесь.',
- 'shelves_save' => 'Сохранить полку',
- 'shelves_books' => 'Книги из этой полки',
- 'shelves_add_books' => 'Добавить книгу в эту полку',
- 'shelves_drag_books' => 'Перетащите книгу сюда, чтобы добавить на эту полку',
- 'shelves_empty_contents' => 'На этой полке нет книг',
- 'shelves_edit_and_assign' => 'Изменить полку для привязки книг',
- 'shelves_edit_named' => 'Редактировать полку :name',
- 'shelves_edit' => 'Редактировать книжную полку',
- 'shelves_delete' => 'Удалить книжную полку',
- 'shelves_delete_named' => 'Удалить книжную полку :name',
- 'shelves_delete_explain' => "Это приведет к удалению полки с именем ':name'. Привязанные книги удалены не будут.",
- 'shelves_delete_confirmation' => 'Вы уверены, что хотите удалить эту полку?',
- 'shelves_permissions' => 'Доступы к книжной полке',
- 'shelves_permissions_updated' => 'Доступы к книжной полке обновлены',
- 'shelves_permissions_active' => 'Доступы к книжной полке активны',
- 'shelves_copy_permissions_to_books' => 'Наследовать доступы книгам',
- 'shelves_copy_permissions' => 'Копировать доступы',
- 'shelves_copy_permissions_explain' => 'Это применит текущие настройки доступов этой книжной полки ко всем книгам, содержащимся внутри. Перед активацией убедитесь, что все изменения в доступах этой книжной полки сохранены.',
- 'shelves_copy_permission_success' => 'Доступы книжной полки скопированы для :count books',
-
- /**
- * Chapters
- */
+ //Chapters
'chapter' => 'Глава',
'chapters' => 'Главы',
'x_chapters' => ':count глава|:count главы',
'chapters_permissions_success' => 'Разрешения главы обновлены',
'chapters_search_this' => 'Искать в этой главе',
- /**
- * Pages
- */
+ //Pages
'page' => 'Страница',
'pages' => 'Страницы',
'x_pages' => ':count страница|:count страниц',
'pages_revisions_created_by' => 'Создана',
'pages_revisions_date' => 'Дата версии',
'pages_revisions_number' => '#',
+ 'pages_revisions_numbered' => 'Ревизия #:id',
+ 'pages_revisions_numbered_changes' => 'Ревизия #:id изменения',
'pages_revisions_changelog' => 'Список изменений',
'pages_revisions_changes' => 'Изменения',
'pages_revisions_current' => 'Текущая версия',
'message' => ':start :time. Будьте осторожны, чтобы не перезаписывать друг друга!',
],
'pages_draft_discarded' => 'Черновик сброшен, редактор обновлен текущим содержимым страницы',
+ 'pages_specific' => 'Конкретная страница',
+ 'pages_is_template' => 'Шаблон страницы',
- /**
- * Editor sidebar
- */
+ //Editor sidebar
'page_tags' => 'Теги страницы',
'chapter_tags' => 'Теги главы',
'book_tags' => 'Теги книги',
'shelf_tags' => 'Теги полки',
'tag' => 'Тег',
'tags' => 'Теги',
+ 'tag_name' => 'Имя тега',
'tag_value' => 'Значение тега (опционально)',
'tags_explain' => 'Добавьте теги, чтобы лучше классифицировать ваш контент. \n Вы можете присвоить значение тегу для более глубокой организации.',
'tags_add' => 'Добавить тег',
+ 'tags_remove' => 'Удалить этот тэг',
'attachments' => 'Вложение',
'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
'attachments_file_uploaded' => 'Файл успешно загружен',
'attachments_file_updated' => 'Файл успешно обновлен',
'attachments_link_attached' => 'Ссылка успешно присоединена к странице',
+ 'templates' => 'Шаблоны',
+ 'templates_set_as_template' => 'Страница это шаблон',
+ 'templates_explain_set_as_template' => 'Вы можете назначить эту страницу в качестве шаблона, её содержимое будет использоваться при создании других страниц. Пользователи смогут использовать этот шаблон в случае, если имеют разрешения на просмотр этой страницы.',
+ 'templates_replace_content' => 'Заменить содержимое страницы',
+ 'templates_append_content' => 'Добавить к содержанию страницы',
+ 'templates_prepend_content' => 'Добавить в начало содержимого страницы',
- /**
- * Profile View
- */
+ //Profile View
'profile_user_for_x' => 'пользователь уже :time',
'profile_created_content' => 'Созданный контент',
'profile_not_created_pages' => ':userName не создавал страниц',
'profile_not_created_chapters' => ':userName не создавал глав',
'profile_not_created_books' => ':userName не создавал ни одной книги',
+ 'profile_not_created_shelves' => ':userName не создал ни одной полки',
- /**
- * Comments
- */
+ //Comments
'comment' => 'Комментарий',
'comments' => 'Комментарии',
'comment_add' => 'Комментировать',
'comment_deleted_success' => 'Комментарий удален',
'comment_created_success' => 'Комментарий добавлен',
'comment_updated_success' => 'Комментарий обновлен',
- 'comment_delete_confirm' => 'Ð\92Ñ\8b Ñ\83веÑ\80енÑ\8b, Ñ\87Ñ\82о Ñ\85оÑ\82иÑ\82е Ñ\83далить этот комментарий?',
+ 'comment_delete_confirm' => 'Удалить этот комментарий?',
'comment_in_reply_to' => 'В ответ на :commentId',
- /**
- * Revision
- */
- 'revision_delete_confirm' => 'Вы действительно хотите удалить эту ревизию?',
- 'revision_delete_success' => 'Редактирование удалено',
- 'revision_cannot_delete_latest' => 'Не удается удалить последнюю версию.'
+ //Revision
+ 'revision_delete_confirm' => 'Удалить эту ревизию?',
+ 'revision_restore_confirm' => 'Восстановить эту ревизию? Текущее содержимое будет заменено.',
+ 'revision_delete_success' => 'Ревизия удалена',
+ 'revision_cannot_delete_latest' => 'Нельзя удалить последнюю версию.'
];
<?php
-
+/**
+ * Text shown in error messaging.
+ */
return [
- /**
- * Error text strings.
- */
-
// Permissions
'permission' => 'У вас нет доступа к запрашиваемой странице.',
'permissionJson' => 'У вас нет разрешения для запрашиваемого действия.',
'social_account_register_instructions' => 'Если у вас еще нет учетной записи, вы можете зарегистрироваться, используя параметр :socialAccount.',
'social_driver_not_found' => 'Драйвер для Соцсети не найден',
'social_driver_not_configured' => 'Настройки вашего :socialAccount заданы неправильно.',
+ 'invite_token_expired' => 'Срок действия приглашения истек. Вместо этого вы можете попытаться сбросить пароль своей учетной записи.',
// System
'path_not_writable' => 'Невозможно загрузить файл по пути :filePath . Убедитесь что сервер доступен для записи.',
// Entities
'entity_not_found' => 'Объект не найден',
+ 'bookshelf_not_found' => 'Полка не найдена',
'book_not_found' => 'Книга не найдена',
'page_not_found' => 'Страница не найдена',
'chapter_not_found' => 'Глава не найдена',
'role_cannot_be_edited' => 'Невозможно отредактировать данную роль',
'role_system_cannot_be_deleted' => 'Эта роль является системной и не может быть удалена',
'role_registration_default_cannot_delete' => 'Эта роль не может быть удалена, так как она устанолена в качестве роли по умолчанию',
+ 'role_cannot_remove_only_admin' => 'Этот пользователь единственный с правами администратора. Назначьте роль администратора другому пользователю, прежде чем удалить этого.',
// Comments
'comment_list' => 'При получении комментариев произошла ошибка.',
'error_occurred' => 'Произошла ошибка',
'app_down' => ':appName в данный момент не достпуно',
'back_soon' => 'Скоро восстановится.',
+
];
<?php
-
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
return [
- /*
- |--------------------------------------------------------------------------
- | Pagination Language Lines
- |--------------------------------------------------------------------------
- |
- | The following language lines are used by the paginator library to build
- | the simple pagination links. You are free to change them to anything
- | you want to customize your views to better match your application.
- |
- */
-
'previous' => '« Предыдущая',
'next' => 'Следующая »',
<?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 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, such as for an invalid token or invalid new password.
- |
- */
-
- 'password' => 'Пароль должен содержать не менее шести символов для применения.',
- 'user' => 'Невозможно найти пользователя с указанным email адресом.',
- 'token' => 'Этот токен для сброса пароля недействителен.',
- 'sent' => 'Ссылка для сброса пароля была отправлена на электронную почту!',
+ 'password' => 'Пароль должен содержать не менее шести символов и совпадать с подтверждением.',
+ 'user' => 'Пользователь с указаным email отсутствует.',
+ 'token' => 'Токен сброса пароля недействителен.',
+ 'sent' => 'Ссылка для сброса пароля отправлена на email!',
'reset' => 'Ваш пароль был сброшен!',
];
// App Settings
'app_customization' => 'Настройки',
- 'app_features_security' => 'ФÑ\83нкÑ\86ии & Безопасность',
+ 'app_features_security' => 'ФÑ\83нкÑ\86ионал & Безопасность',
'app_name' => 'Имя приложения',
'app_name_desc' => 'Имя отображается в заголовке email отправленных системой.',
'app_name_header' => 'Отображать имя приложения в заголовке',
'app_editor_desc' => 'Выберите, какой редактор будет использоваться всеми пользователями для редактирования страниц.',
'app_custom_html' => 'Пользовательский контент заголовка HTML',
'app_custom_html_desc' => 'Любой контент, добавленный здесь, будет вставлен в нижнюю часть раздела <head> каждой страницы. Это удобно для переопределения стилей или добавления кода аналитики.',
+ 'app_custom_html_disabled_notice' => 'Пользовательский контент заголовка HTML отключен на этой странице, чтобы гарантировать отмену любых критических изменений',
'app_logo' => 'Лого приложения',
'app_logo_desc' => 'Это изображение должно быть 43px в высоту. <br>Большое изображение будет уменьшено.',
'app_primary_color' => 'Основной цвет приложения',
// Registration Settings
'reg_settings' => 'Настройки регистрации',
- 'reg_enable' => 'РазÑ\80еÑ\88иÑ\82Ñ\8c Ñ\80егиÑ\81Ñ\82Ñ\80аÑ\86иÑ\8fÑ\8e',
+ 'reg_enable' => 'Разрешить регистрацию',
'reg_enable_toggle' => 'Разрешить регистрацию',
- 'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самомтоятельно. При регистрации назначается роль пользователя по умолчанию',
+ 'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самостоятельно. При регистрации назначается роль пользователя по умолчанию',
'reg_default_role' => 'Роль пользователя по умолчанию после регистрации',
'reg_email_confirmation' => 'Подтверждение электонной почты',
'reg_email_confirmation_toggle' => 'Требовать подтверждение по электронной почте',
- 'reg_confirm_email_desc' => 'Ð\95Ñ\81ли иÑ\81полÑ\8cзÑ\83еÑ\82Ñ\81Ñ\8f огÑ\80аниÑ\87ение по доменÑ\83, подÑ\82веÑ\80ждение бÑ\83деÑ\82 обÑ\8fзаÑ\82елÑ\8cно, а Ñ\8dÑ\82оÑ\82 пÑ\83нкÑ\82 пÑ\80оигноÑ\80иÑ\80ован.',
+ 'reg_confirm_email_desc' => 'Ð\9fÑ\80и иÑ\81полÑ\8cзовании огÑ\80аниÑ\87ениÑ\8f по доменÑ\83 - подÑ\82веÑ\80ждение обÑ\8fзаÑ\82елÑ\8cно, Ñ\8dÑ\82оÑ\82 пÑ\83нкÑ\82 игноÑ\80иÑ\80Ñ\83еÑ\82Ñ\81Ñ\8f.',
'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',
'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых разрешена регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свои адреса уже после регистрации.',
'reg_confirm_restrict_domain_placeholder' => 'Без ограничений',
'role_manage_roles' => 'Управление ролями и правами на роли',
'role_manage_entity_permissions' => 'Управление правами на все книги, главы и страницы',
'role_manage_own_entity_permissions' => 'Управление разрешениями для собственных книг, разделов и страниц',
+ 'role_manage_page_templates' => 'Управление шаблонами страниц',
'role_manage_settings' => 'Управление настройками приложения',
'role_asset' => 'Разрешение для активации',
'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
'users_role' => 'Роли пользователя',
'users_role_desc' => 'Назначьте роли пользователю. Если назначено несколько ролей, разрешения будут суммироваться и пользователь получит все права назначенных ролей.',
'users_password' => 'Пароль пользователя',
- 'users_password_desc' => 'Установите пароль для входа в приложение. Должно быть не менее 5 символов.',
+ 'users_password_desc' => 'Установите пароль для входа в приложение. Должно быть не менее 6 символов.',
+ 'users_send_invite_text' => 'Вы можете отправить этому пользователю email с приглашением, которое позволит ему установить пароль самостоятельно или задайте пароль сами.',
+ 'users_send_invite_option' => 'Отправить пользователю email с приглашением.',
'users_external_auth_id' => 'Внешний ID аутентификации',
'users_external_auth_id_desc' => 'Этот ID используется для связи с вашей LDAP системой.',
'users_password_warning' => 'Заполните ниже только если вы хотите сменить свой пароль.',
'users_preferred_language' => 'Предпочитаемый язык',
'users_preferred_language_desc' => 'Этот параметр изменит язык интерфейса приложения. Это не влияет на созданный пользователем контент.',
'users_social_accounts' => 'Аккаунты Соцсетей',
- 'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не разрешено. Отменить доступ к настройкам вашего профиля в подключенном социальном аккаунте.',
+ 'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не возможно. Отмените доступ к настройкам вашего профиля в подключенном аккаунте соцсети.',
'users_social_connect' => 'Подключить аккаунт',
'users_social_disconnect' => 'Отключить аккаунт',
'users_social_connected' => ':socialAccount аккаунт упешно подключен к вашему профилю.',
<?php
-
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the size rules. Feel free to tweak each of these messages here.
+ */
return [
- /*
- |--------------------------------------------------------------------------
- | Validation Language Lines
- |--------------------------------------------------------------------------
- |
- | The following language lines contain the default error messages used by
- | the validator class. Some of these rules have multiple versions such
- | as the size rules. Feel free to tweak each of these messages here.
- |
- */
-
+ // Standard laravel validation lines
'accepted' => ':attribute должен быть принят.',
'active_url' => ':attribute не является корректным URL.',
'after' => ':attribute дата должна быть позже :date.',
'filled' => ':attribute поле необходимо.',
'exists' => 'выделенный :attribute некорректен.',
'image' => ':attribute должен быть изображением.',
+ 'image_extension' => ':attribute должен быть исправным и содержать расширение картинки',
'in' => 'выделенный :attribute некорректен.',
'integer' => ':attribute должно быть целое число.',
'ip' => ':attribute должен быть корректным IP адресом.',
'string' => ':attribute должен быть минимум :min символов.',
'array' => ':attribute должен содержать хотя бы :min элементов.',
],
+ 'no_double_extension' => ':attribute должен иметь только одно расширение файла.',
'not_in' => 'Выбранный :attribute некорректен.',
'numeric' => ':attribute должен быть числом.',
'regex' => ':attribute неправильный формат.',
'timezone' => ':attribute должен быть корректным часовым поясом.',
'unique' => ':attribute уже есть.',
'url' => ':attribute имеет неправильный формат.',
+ 'uploaded' => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.',
- /*
- |--------------------------------------------------------------------------
- | Custom Validation Language Lines
- |--------------------------------------------------------------------------
- |
- | Here you may specify custom validation messages for attributes using the
- | convention 'attribute.rule' to name the lines. This makes it quick to
- | specify a specific custom language line for a given attribute rule.
- |
- */
-
+ //Custom validation lines
'custom' => [
'password-confirm' => [
'required_with' => 'Требуется подтверждение пароля',
],
],
- /*
- |--------------------------------------------------------------------------
- | Custom Validation Attributes
- |--------------------------------------------------------------------------
- |
- | The following language lines are used to swap attribute place-holders
- | with something more reader friendly such as E-Mail Address instead
- | of 'email'. This simply helps us make messages a little cleaner.
- |
- */
-
+ //Custom validation attributes
'attributes' => [],
];
'email' => 'Email',
'password' => 'Heslo',
'password_confirm' => 'Potvrdiť heslo',
- 'password_hint' => 'Musí mať viac ako 5 znakov',
+ 'password_hint' => 'Musí mať viac ako 7 znakov',
'forgot_password' => 'Zabudli ste heslo?',
'remember_me' => 'Zapamätať si ma',
'ldap_email_hint' => 'Zadajte prosím email, ktorý sa má použiť pre tento účet.',
'email' => 'E-post',
'password' => 'Lösenord',
'password_confirm' => 'Bekräfta lösenord',
- 'password_hint' => 'Måste vara fler än 5 tecken',
+ 'password_hint' => 'Måste vara fler än 7 tecken',
'forgot_password' => 'Glömt lösenord?',
'remember_me' => 'Kom ihåg mig',
'ldap_email_hint' => 'Vänligen ange en e-postadress att använda till kontot.',
--- /dev/null
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+ // Pages
+ 'page_create' => 'saya oluşturuldu',
+ 'page_create_notification' => 'Sayfa Başarıyla Oluşturuldu',
+ 'page_update' => 'sayfa güncellendi',
+ 'page_update_notification' => 'Sayfa Başarıyla Güncellendi',
+ 'page_delete' => 'sayfa silindi',
+ 'page_delete_notification' => 'Sayfa Başarıyla Silindi',
+ 'page_restore' => 'sayfa kurtarıldı',
+ 'page_restore_notification' => 'Sayfa Başarıyla Kurtarıldı',
+ 'page_move' => 'sayfa taşındı',
+
+ // Chapters
+ 'chapter_create' => 'bölüm oluşturuldu',
+ 'chapter_create_notification' => 'Bölüm Başarıyla Oluşturuldu',
+ 'chapter_update' => 'bölüm güncellendi',
+ 'chapter_update_notification' => 'Bölüm Başarıyla Güncellendi',
+ 'chapter_delete' => 'bölüm silindi',
+ 'chapter_delete_notification' => 'Bölüm Başarıyla Silindi',
+ 'chapter_move' => 'bölüm taşındı',
+
+ // Books
+ 'book_create' => 'kitap oluşturuldu',
+ 'book_create_notification' => 'Kitap Başarıyla Oluşturuldu',
+ 'book_update' => 'kitap güncellendi',
+ 'book_update_notification' => 'Kitap Başarıyla Güncellendi',
+ 'book_delete' => 'kitap silindi',
+ 'book_delete_notification' => 'Kitap Başarıyla Silindi',
+ 'book_sort' => 'kitap düzenlendi',
+ 'book_sort_notification' => 'Kitap Başarıyla Yeniden Sıralandı',
+
+ // Bookshelves
+ 'bookshelf_create' => 'kitaplık oluşturuldu',
+ 'bookshelf_create_notification' => 'Kitaplık Başarıyla Oluşturuldu',
+ 'bookshelf_update' => 'kitaplık güncellendi',
+ 'bookshelf_update_notification' => 'Kitaplık Başarıyla Güncellendi',
+ 'bookshelf_delete' => 'kitaplık silindi',
+ 'bookshelf_delete_notification' => 'Kitaplık Başarıyla Silindi',
+
+ // Other
+ 'commented_on' => 'yorum yaptı',
+];
--- /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' => 'Girilen bilgiler bizdeki kayıtlarla uyuşmuyor.',
+ 'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.',
+
+ // Login & Register
+ 'sign_up' => 'Kayıt Ol',
+ 'log_in' => 'Giriş Yap',
+ 'log_in_with' => ':socialDriver ile giriş yap',
+ 'sign_up_with' => ':socialDriver ile kayıt ol',
+ 'logout' => 'Çıkış Yap',
+
+ 'name' => 'İsim',
+ 'username' => 'Kullanıcı Adı',
+ 'email' => 'Email',
+ 'password' => 'Şifre',
+ 'password_confirm' => 'Şifreyi onayla',
+ 'password_hint' => 'En az 5 karakter olmalı',
+ 'forgot_password' => 'Şifrenizi mi unuttunuz?',
+ 'remember_me' => 'Beni Hatırla',
+ 'ldap_email_hint' => 'Hesabı kullanmak istediğiniz e-mail adresinizi giriniz.',
+ 'create_account' => 'Hesap Oluştur',
+ 'already_have_account' => 'Zaten bir hesabınız var mı?',
+ 'dont_have_account' => 'Hesabınız yok mu?',
+ 'social_login' => 'Diğer Servisler ile Giriş Yap',
+ 'social_registration' => 'Diğer Servisler ile Kayıt Ol',
+ 'social_registration_text' => 'Diğer servisler ile kayıt ol ve giriş yap.',
+
+ 'register_thanks' => 'Kayıt olduğunuz için teşekkürler!',
+ 'register_confirm' => 'Lütfen e-posta adresinizi kontrol edin ve gelen doğrulama bağlantısına tıklayınız. :appName.',
+ 'registrations_disabled' => 'Kayıt olma özelliği geçici olarak kısıtlanmıştır',
+ 'registration_email_domain_invalid' => 'Bu e-mail sağlayıcısının bu uygulamaya erişim izni yoktur.',
+ 'register_success' => 'Artık kayıtlı bir kullanıcı olarak giriş yaptınız.',
+
+
+ // Password Reset
+ 'reset_password' => 'Parolayı Sıfırla',
+ 'reset_password_send_instructions' => 'Aşağıya e-mail adresinizi girdiğinizde parola yenileme bağlantısı mail adresinize gönderilecektir.',
+ 'reset_password_send_button' => '>Sıfırlama Bağlantısını Gönder',
+ 'reset_password_sent_success' => 'Sıfırlama bağlantısı :email adresinize gönderildi.',
+ 'reset_password_success' => 'Parolanız başarıyla sıfırlandı.',
+ 'email_reset_subject' => ':appName şifrenizi sıfırlayın.',
+ 'email_reset_text' => ' Parola sıfırlama isteğinde bulunduğunuz için bu maili görüntülüyorsunuz.',
+ 'email_reset_not_requested' => 'Eğer bu parola sıfırlama isteğinde bulunmadıysanız herhangi bir işlem yapmanıza gerek yoktur.',
+
+
+ // Email Confirmation
+ 'email_confirm_subject' => ':appName için girdiğiniz mail adresiniz onaylayınız',
+ 'email_confirm_greeting' => ':appName\'e katıldığınız için teşekkürler!',
+ 'email_confirm_text' => 'Lütfen e-mail adresinizi aşağıda bulunan butona tıklayarak onaylayınız:',
+ 'email_confirm_action' => 'E-Maili Onayla',
+ 'email_confirm_send_error' => 'e-mail onayı gerekli fakat sistem mail göndermeyi başaramadı. Yöneticiniz ile görüşüp kurulumlarda bir sorun olmadığını doğrulayın.',
+ 'email_confirm_success' => 'e-mail adresiniz onaylandı!',
+ 'email_confirm_resent' => 'Doğrulama maili gönderildi, lütfen gelen kutunuzu kontrol ediniz...',
+
+ 'email_not_confirmed' => 'E-mail Adresi Doğrulanmadı',
+ 'email_not_confirmed_text' => 'Sağlamış olduğunuz e-mail adresi henüz doğrulanmadı.',
+ 'email_not_confirmed_click_link' => 'Lütfen kayıt olduktan kısa süre sonra size gönderilen maildeki bağlantıya tıklayın ve mail adresinizi onaylayın.',
+ 'email_not_confirmed_resend' => 'Eğer gelen maili bulamadıysanız aşağıdaki formu tekrar doldurarak onay mailini kendinize tekrar gönderebilirsiniz.',
+ 'email_not_confirmed_resend_button' => 'Doğrulama Mailini Yeniden Yolla',
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+ // Buttons
+ 'cancel' => 'İptal',
+ 'confirm' => 'Onayla',
+ 'back' => 'Geri',
+ 'save' => 'Kaydet',
+ 'continue' => 'Devam',
+ 'select' => 'Seç',
+ 'toggle_all' => 'Hepsini Değiştir',
+ 'more' => 'Daha Fazla',
+
+ // Form Labels
+ 'name' => 'İsim',
+ 'description' => 'Açıklama',
+ 'role' => 'Rol',
+ 'cover_image' => 'Kapak resmi',
+ 'cover_image_description' => 'Bu resim yaklaşık 440x250px boyutlarında olmalıdır.',
+
+ // Actions
+ 'actions' => 'Aksiyonlar',
+ 'view' => 'Görüntüle',
+ 'view_all' => 'Hepsini Görüntüle',
+ 'create' => 'Oluştur',
+ 'update' => 'Güncelle',
+ 'edit' => 'Düzenle',
+ 'sort' => 'Sırala',
+ 'move' => 'Taşı',
+ 'copy' => 'Kopyala',
+ 'reply' => 'Yanıtla',
+ 'delete' => 'Sil',
+ 'search' => 'Ara',
+ 'search_clear' => 'Aramayı Temizle',
+ 'reset' => 'Sıfırla',
+ 'remove' => 'Kaldır',
+ 'add' => 'Ekle',
+
+ // Sort Options
+ 'sort_name' => 'İsim',
+ 'sort_created_at' => 'Oluşturulma Tarihi',
+ 'sort_updated_at' => 'Güncellenme Tarihi',
+
+ // Misc
+ 'deleted_user' => 'Silinmiş Kullanıcı',
+ 'no_activity' => 'Gösterilecek aktivite yok',
+ 'no_items' => 'Kullanılabilir öge yok',
+ 'back_to_top' => 'Başa dön',
+ 'toggle_details' => 'Detayları değiştir',
+ 'toggle_thumbnails' => 'Küçük resimleri değiştir',
+ 'details' => 'Detaylar',
+ 'grid_view' => 'Grid görünümü',
+ 'list_view' => 'Liste görünümü',
+ 'default' => 'Varsayılan',
+
+ // Header
+ 'view_profile' => 'Profili Görüntüle',
+ 'edit_profile' => 'Profili Düzenle',
+
+ // Layout tabs
+ 'tab_info' => 'Bilgi',
+ 'tab_content' => 'İçerik',
+
+ // Email Content
+ 'email_action_help' => 'Eğer ":actionText" butonuna tıklamakta zorluk çekiyorsanız, aşağıda bulunan linki kopyalayıp tarayıcınıza yapıştırabilirsiniz.',
+ 'email_rights' => 'Bütün hakları saklıdır',
+];
--- /dev/null
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+ // Image Manager
+ 'image_select' => 'Görsel Seç',
+ 'image_all' => 'Tümü',
+ 'image_all_title' => 'Tüm görselleri temizle',
+ 'image_book_title' => 'Bu kitaba ait görselleri görüntüle',
+ 'image_page_title' => 'Bu sayfaya ait görselleri görüntüle',
+ 'image_search_hint' => 'Görsel adı ile ara',
+ 'image_uploaded' => ':uploadedDate tarihinde yüklendi',
+ 'image_load_more' => 'Daha Fazla ',
+ 'image_image_name' => 'Görsel Adı',
+ 'image_delete_used' => 'Bu görsel aşağıda bulunan görsellerde kullanılmış.',
+ 'image_delete_confirm' => 'Gerçekten bu görseli silmek istiyorsanız sil tuşuna basınız.',
+ 'image_select_image' => 'Görsel Seç',
+ 'image_dropzone' => 'Görselleri buraya sürükle veya seçmek için buraya tıkla',
+ 'images_deleted' => 'Görseller Silindi',
+ 'image_preview' => 'Görsel Önizleme',
+ 'image_upload_success' => 'Görsel başarıyla yüklendi',
+ 'image_update_success' => 'Görsel başarıyla güncellendi',
+ 'image_delete_success' => 'Görsel başarıyla silindi',
+ 'image_upload_remove' => 'Kaldır',
+
+ // Code Editor
+ 'code_editor' => 'Kodu Güncelle',
+ 'code_language' => 'Kod Dil',
+ 'code_content' => 'Kod İçeriği',
+ 'code_save' => 'Kodu Kaydet',
+];
--- /dev/null
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+ // Shared
+ 'recently_created' => 'Yakın Zamanda Oluşturuldu',
+ 'recently_created_pages' => 'Yakın Zamanda Oluşturulmuş Sayfalar',
+ 'recently_updated_pages' => 'Yakın Zamanda Güncellenmiş Sayfalar',
+ 'recently_created_chapters' => 'Yakın Zamanda Oluşturulmuş Bölümler',
+ 'recently_created_books' => 'Yakın Zamanda Olşturulmuş Kitaplar',
+ 'recently_created_shelves' => 'Yakın Zamanda Oluşturulmuş Kitaplıklar',
+ 'recently_update' => 'Yakın Zamanda Güncellenmiş',
+ 'recently_viewed' => 'Yakın Zamanda Görüntülenmiş',
+ 'recent_activity' => 'Son Hareketler',
+ 'create_now' => 'Hemen bir tane oluştur',
+ 'revisions' => 'Revizyonlar',
+ 'meta_revision' => 'Revizyon #:revisionCount',
+ 'meta_created' => 'Oluşturuldu :timeLength',
+ 'meta_created_name' => ':user tarafından :timeLength tarihinde oluşturuldu',
+ 'meta_updated' => 'Güncellendi :timeLength',
+ 'meta_updated_name' => ':user tarafından :timeLength tarihinde güncellendi',
+ 'entity_select' => 'Öğe Seçme',
+ 'images' => 'Görseller',
+ 'my_recent_drafts' => 'Son Taslaklarım',
+ 'my_recently_viewed' => 'Son Görüntülemelerim',
+ 'no_pages_viewed' => 'Herhangi bir sayfa görüntülemediniz',
+ 'no_pages_recently_created' => 'Yakın zamanda bir sayfa oluşturulmadı',
+ 'no_pages_recently_updated' => 'Yakın zamanda bir sayfa güncellenmedi',
+ 'export' => 'Dışa Aktar',
+ 'export_html' => 'Contained Web Dosyası',
+ 'export_pdf' => 'PDF Dosyası',
+ 'export_text' => 'Düz Metin Dosyası',
+
+ // Permissions and restrictions
+ 'permissions' => 'İzinler',
+ 'permissions_intro' => 'Etkinleştirildikten sonra bu izinler diğer diğer bütün izinlerden öncelikli olacaktır.',
+ 'permissions_enable' => 'Özelleştirilmiş Yetkileri Etkinleştir',
+ 'permissions_save' => 'İzinleri Kaydet',
+
+ // Search
+ 'search_results' => 'Arama Sonuçları',
+ 'search_total_results_found' => ':count sonuç bulundu |:count toplam sonuç bulundu',
+ 'search_clear' => 'Aramaları Temizle',
+ 'search_no_pages' => 'Bu aramayla herhangi bir sonuç eşleşmedi',
+ 'search_for_term' => ':term için ara',
+ 'search_more' => 'Daha Fazla Sonuç',
+ 'search_filters' => 'Arama Filtreleri',
+ 'search_content_type' => 'İçerik Türü',
+ 'search_exact_matches' => 'Tam Eşleşmeler',
+ 'search_tags' => 'Etiket Aramaları',
+ 'search_options' => 'Ayarlar',
+ 'search_viewed_by_me' => 'Benim tarafımdan görüntülendi',
+ 'search_not_viewed_by_me' => 'Benim tarafımdan görüntülenmedi',
+ 'search_permissions_set' => 'İzinler ayarlandı',
+ 'search_created_by_me' => 'Benim tarafımdan oluşturuldu',
+ 'search_updated_by_me' => 'Benim tarafımdan güncellendi',
+ 'search_date_options' => 'Tarih Seçenekleri',
+ 'search_updated_before' => 'Önce güncellendi',
+ 'search_updated_after' => 'Sonra güncellendi',
+ 'search_created_before' => 'Önce oluşturuldu',
+ 'search_created_after' => 'Sonra oluşturuldu',
+ 'search_set_date' => 'Tarih Ayarla',
+ 'search_update' => 'Aramayı Güncelle',
+
+ // Shelves
+ 'shelf' => 'Kitaplık',
+ 'shelves' => 'Kitaplıklar',
+ 'x_shelves' => ':count Kitaplık|:count Kitaplıklar',
+ 'shelves_long' => 'Kitaplıklar',
+ 'shelves_empty' => 'Hiç kitaplık oluşturulmadı',
+ 'shelves_create' => 'Yeni Kitaplık Oluştur',
+ 'shelves_popular' => 'Popüler Kitaplıklar',
+ 'shelves_new' => 'Yeni Kitaplıklar',
+ 'shelves_new_action' => 'Yeni Kitaplık',
+ 'shelves_popular_empty' => 'En popüler kitaplıklar burada görüntülenecek.',
+ 'shelves_new_empty' => 'En son oluşturulmuş kitaplıklar burada görüntülenecek.',
+ 'shelves_save' => 'Kitaplığı Kaydet',
+ 'shelves_books' => 'Bu kitaplıktaki kitaplar',
+ 'shelves_add_books' => 'Bu kitaplığa kitap ekle',
+ 'shelves_drag_books' => 'Bu kitaplığa kitap eklemek için kitapları buraya sürükle',
+ 'shelves_empty_contents' => 'Bu kitaplığa henüz hiç bir kitap atanmamış',
+ 'shelves_edit_and_assign' => 'Kitaplığa kitap eklemek için güncelle',
+ 'shelves_edit_named' => ':name Kitaplığını Güncelle',
+ 'shelves_edit' => 'Kitaplığı Güncelle',
+ 'shelves_delete' => 'Kitaplığı Sil',
+ 'shelves_delete_named' => ':name Kitaplığını Sil',
+ 'shelves_delete_explain' => "Bu işlem :name kitaplığını silecektir. İçerdiği kitaplar silinmeyecektir.",
+ 'shelves_delete_confirmation' => 'Bu kitaplığı silmek istediğinizden emin misiniz?',
+ 'shelves_permissions' => 'Kitaplık İzinleri',
+ 'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',
+ 'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',
+ 'shelves_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala',
+ 'shelves_copy_permissions' => 'İzinleri Kopyala',
+ 'shelves_copy_permissions_explain' => 'Bu işlem sonucunda kitaplığınızın izinleri içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden bu kitaplığa ait izinleri kaydettiğinizden emin olun.',
+ 'shelves_copy_permission_success' => 'Kitaplık izinleri :count adet kitaba kopyalandı',
+
+ // Books
+ 'book' => 'Kitap',
+ 'books' => 'Kitaplar',
+ 'x_books' => ':count Kitap|:count Kitaplar',
+ 'books_empty' => 'Hiç kitap oluşturulmadı',
+ 'books_popular' => 'Popüler Kitaplar',
+ 'books_recent' => 'En Son Kitaplar',
+ 'books_new' => 'Yeni Kitaplar',
+ 'books_new_action' => 'Yeni Kitap',
+ 'books_popular_empty' => 'En popüler kitaplar burada görüntülenecek.',
+ 'books_new_empty' => 'En yeni kitaplar burada görüntülenecek.',
+ 'books_create' => 'Yeni Kitap Oluştur',
+ 'books_delete' => 'Kitabı Sil',
+ 'books_delete_named' => ':bookName kitabını sil',
+ 'books_delete_explain' => 'Bu işlem \':bookName\' kitabını silecek. Bütün sayfa ve bölümler silinecektir.',
+ 'books_delete_confirmation' => 'Bu kitabı silmek istediğinizden emin misiniz?',
+ 'books_edit' => 'Kitabı Güncelle',
+ 'books_edit_named' => ':bookName Kitabını Güncelle',
+ 'books_form_book_name' => 'Kitap Adı',
+ 'books_save' => 'Kitabı Kaydet',
+ 'books_permissions' => 'Kitap İzinleri',
+ 'books_permissions_updated' => 'Kitap İzinleri Güncellendi',
+ 'books_empty_contents' => 'Bu kitaba ait sayfa veya bölüm oluşturulmadı.',
+ 'books_empty_create_page' => 'Yeni sayfa oluştur',
+ 'books_empty_sort_current_book' => 'Mevcut kitabı sırala',
+ 'books_empty_add_chapter' => 'Yeni bölüm ekle',
+ 'books_permissions_active' => 'Kitap İzinleri Aktif',
+ 'books_search_this' => 'Bu kitapta ara',
+ 'books_navigation' => 'Kitap Navigasyonu',
+ 'books_sort' => 'Kitap İçeriklerini Sırala',
+ 'books_sort_named' => ':bookName Kitabını Sırala',
+ 'books_sort_name' => 'İsme Göre Sırala',
+ 'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala',
+ 'books_sort_updated' => 'Güncellenme Tarihine Göre Sırala',
+ 'books_sort_chapters_first' => 'Önce Bölümler',
+ 'books_sort_chapters_last' => 'En Son Bölümler',
+ 'books_sort_show_other' => 'Diğer Kitapları Göster',
+ 'books_sort_save' => 'Yeni Düzeni Kaydet',
+
+ // Chapters
+ 'chapter' => 'Bölüm',
+ 'chapters' => 'Bölümler',
+ 'x_chapters' => ':count Bölüm|:count Bölümler',
+ 'chapters_popular' => 'Popüler Bölümler',
+ 'chapters_new' => 'Yeni Bölüm',
+ 'chapters_create' => 'Yeni Bölüm Oluştur',
+ 'chapters_delete' => 'Bölümü Sil',
+ 'chapters_delete_named' => ':chapterName Bölümünü Sil',
+ 'chapters_delete_explain' => 'Bu işlem \':chapterName\' kitabını silecek. Bütün sayfalar silinecek ve direkt olarak ana kitab eklenecektir.',
+ 'chapters_delete_confirm' => 'Bölümü silmek istediğinizden emin misiniz?',
+ 'chapters_edit' => 'Bölümü Güncelle',
+ 'chapters_edit_named' => ':chapterName Bölümünü Güncelle',
+ 'chapters_save' => 'Bölümü Kaydet',
+ 'chapters_move' => 'Bölümü Taşı',
+ 'chapters_move_named' => ':chapterName Bölümünü Taşı',
+ 'chapter_move_success' => 'Bölüm :bookName Kitabına Taşındı',
+ 'chapters_permissions' => 'Bölüm İzinleri',
+ 'chapters_empty' => 'Bu bölümde henüz bir sayfa yok.',
+ 'chapters_permissions_active' => 'Bölüm İzinleri Aktif',
+ 'chapters_permissions_success' => 'Bölüm İzinleri Güncellendi',
+ 'chapters_search_this' => 'Bu bölümü ara',
+
+ // Pages
+ 'page' => 'Sayfa',
+ 'pages' => 'Sayfalar',
+ 'x_pages' => ':count Sayfa|:count Sayfalar',
+ 'pages_popular' => 'Popüler Sayfalar',
+ 'pages_new' => 'Yeni Sayfa',
+ 'pages_attachments' => 'Ekler',
+ 'pages_navigation' => 'Sayfa Navigasyonu',
+ 'pages_delete' => 'Sayfayı Sil',
+ 'pages_delete_named' => ':pageName Sayfasını Sil',
+ 'pages_delete_draft_named' => ':pageName Taslak Sayfasını Sil',
+ 'pages_delete_draft' => 'Taslak Sayfayı Sil',
+ 'pages_delete_success' => 'Sayfa silindi',
+ 'pages_delete_draft_success' => 'Taslak sayfa silindi',
+ 'pages_delete_confirm' => 'Bu sayfayı silmek istediğinizden emin misiniz?',
+ 'pages_delete_draft_confirm' => 'Bu taslak sayfayı silmek istediğinizden emin misiniz?',
+ 'pages_editing_named' => ':pageName Sayfası Düzenleniyor',
+ 'pages_edit_save_draft' => 'Taslağı Kaydet',
+ 'pages_edit_draft' => 'Taslak Sayfasını Düzenle',
+ 'pages_editing_draft' => 'Taslak Düzenleniyor',
+ 'pages_editing_page' => 'Sayfa Düzenleniyor',
+ 'pages_edit_draft_save_at' => 'Taslak kaydedildi ',
+ 'pages_edit_delete_draft' => 'Taslağı Sl',
+ 'pages_edit_discard_draft' => 'Taslağı Yoksay',
+ 'pages_edit_set_changelog' => 'Değişiklik Logunu Kaydet',
+ 'pages_edit_enter_changelog_desc' => 'Yaptığınız değişiklikler hakkında kısa bir bilgilendirme ekleyin',
+ 'pages_edit_enter_changelog' => 'Değişim Günlüğü Ekleyin',
+ 'pages_save' => 'Sayfayı Kaydet',
+ 'pages_title' => 'Sayfa Başlığı',
+ 'pages_name' => 'Sayfa İsmi',
+ 'pages_md_editor' => 'Editör',
+ 'pages_md_preview' => 'Önizleme',
+ 'pages_md_insert_image' => 'Görsel Ekle',
+ 'pages_md_insert_link' => 'Öge Linki Ekle',
+ 'pages_md_insert_drawing' => 'Çizim Ekle',
+ 'pages_not_in_chapter' => 'Sayfa Bu Bölümde Değil',
+ 'pages_move' => 'Sayfayı Taşı',
+ 'pages_move_success' => 'Sayfa ":parentName"\'a taşındı',
+ 'pages_copy' => 'Sayfayı Kopyala',
+ 'pages_copy_desination' => 'Kopyalanacak Hedef',
+ 'pages_copy_success' => 'Sayfa başarıyla kopyalandı',
+ 'pages_permissions' => 'Sayfa İzinleri',
+ 'pages_permissions_success' => 'Sayfa izinleri güncellendi',
+ 'pages_revision' => 'Revizyon',
+ 'pages_revisions' => 'Sayfa Revizyonları',
+ 'pages_revisions_named' => ':pageName için Sayfa Revizyonları',
+ 'pages_revision_named' => ':pageName için Sayfa Revizyonu',
+ 'pages_revisions_created_by' => 'Oluşturan',
+ 'pages_revisions_date' => 'Revizyon Tarihi',
+ 'pages_revisions_number' => '#',
+ 'pages_revisions_numbered' => 'Revizyon #:id',
+ 'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',
+ 'pages_revisions_changelog' => 'Değişim Günlüğü',
+ 'pages_revisions_changes' => 'Değişiklikler',
+ 'pages_revisions_current' => 'Mevcut Versiyon',
+ 'pages_revisions_preview' => 'Önizleme',
+ 'pages_revisions_restore' => 'Kurtar',
+ 'pages_revisions_none' => 'Bu sayfaya ait revizyon yok',
+ 'pages_copy_link' => 'Linki kopyala',
+ 'pages_edit_content_link' => 'İçeriği Düzenle',
+ 'pages_permissions_active' => 'Sayfa İzinleri Aktif',
+ 'pages_initial_revision' => 'İlk Yayın',
+ 'pages_initial_name' => 'Yeni Sayfa',
+ 'pages_editing_draft_notification' => 'Şu anda :timeDiff tarhinde kaydedilmiş olan taslağı düzenlemektesiniz.',
+ 'pages_draft_edited_notification' => 'Bu sayfa son girişinizden bu yana güncellendi. Değişiklikleri yoksayıp, kaydetmeden çıkmanız önerilir.',
+ 'pages_draft_edit_active' => [
+ 'start_a' => ':count kullanıcı bu sayfayı düzenlemeye başladı',
+ 'start_b' => ':userName kullanıcısı bu sayfayı düzenlemeye başladı',
+ 'time_a' => 'sayfa son güncellendiğinden beri',
+ 'time_b' => 'son :minCount dakikada',
+ 'message' => ':start :time. Birbirinizin düzenlemelerinin çakışmamasına dikkat edin!',
+ ],
+ 'pages_draft_discarded' => 'Taslak yok sayıldı, editör mevcut sayfa içeriği ile güncellendi',
+ 'pages_specific' => 'Özel Sayfa',
+
+ // Editor Sidebar
+ 'page_tags' => 'Sayfa Etiketleri',
+ 'chapter_tags' => 'Bölüm Etiketleri',
+ 'book_tags' => 'Kitap Etiketleri',
+ 'shelf_tags' => 'Kitaplık Etiketleri',
+ 'tag' => 'Etiket',
+ 'tags' => 'Etiketler',
+ 'tag_value' => 'Etiket İçeriği (Opsiyonel)',
+ 'tags_explain' => "İçeriğini daha iyi kategorize etmek için bazı etiketler ekle. Etiketlere değer atayarak daha derin bir organizasyon yapısına sahip olabilirsin.",
+ 'tags_add' => 'Başka etiket ekle',
+ 'attachments' => 'Ekler',
+ 'attachments_explain' => 'Sayfanızda göstermek için bazı dosyalar yükleyin veya bazı bağlantılar ekleyin. Bunlar sayfanın sidebarında görülebilir.',
+ 'attachments_explain_instant_save' => 'Burada yapılan değişiklikler anında kaydedilir.',
+ 'attachments_items' => 'Eklenmiş Ögeler',
+ 'attachments_upload' => 'Dosya Yükle',
+ 'attachments_link' => 'Link Ekle',
+ 'attachments_set_link' => 'Link Düzenle',
+ 'attachments_delete_confirm' => 'Eki gerçekten silmek istiyor musunuz?',
+ 'attachments_dropzone' => 'Dosyaları buraya sürükle veya eklemek için buraya tıkla',
+ 'attachments_no_files' => 'Hiç bir dosya yüklenmedi',
+ 'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz link ekleyebilirsiniz. Bu başka bir sayfaya veya buluttaki bir dosyanın linki olabilir.',
+ 'attachments_link_name' => 'Bağlantı Adı',
+ 'attachment_link' => 'Ek linki',
+ 'attachments_link_url' => 'Dosya linki',
+ 'attachments_link_url_hint' => 'Dosyanın veya sitenin url adres',
+ 'attach' => 'Ekle',
+ 'attachments_edit_file' => 'Dosyayı Düzenle',
+ 'attachments_edit_file_name' => 'Dosya Adı',
+ 'attachments_edit_drop_upload' => 'Dosyaları sürükle veya yüklemek için buraya tıkla',
+ 'attachments_order_updated' => 'Ek sırası güncellendi',
+ 'attachments_updated_success' => 'Ek detayları güncellendi',
+ 'attachments_deleted' => 'Ek silindi',
+ 'attachments_file_uploaded' => 'Dosya başarıyla yüklendi',
+ 'attachments_file_updated' => 'Dosya başarıyla güncellendi',
+ 'attachments_link_attached' => 'Link sayfaya başarıyla eklendi',
+
+ // Profile View
+ 'profile_user_for_x' => 'Kullanıcı :time',
+ 'profile_created_content' => 'Oluşturulan İçerik',
+ 'profile_not_created_pages' => ':userName herhangi bir sayfa oluşturmadı',
+ 'profile_not_created_chapters' => ':userName herhangi bir bölüm oluşturmadı',
+ 'profile_not_created_books' => ':userName herhangi bir kitap oluşturmadı',
+ 'profile_not_created_shelves' => ':userName herhangi bir kitaplık oluşturmadı',
+
+ // Comments
+ 'comment' => 'Yorum',
+ 'comments' => 'Yorumlar',
+ 'comment_add' => 'Yorum Ekle',
+ 'comment_placeholder' => 'Buraya yorum ekle',
+ 'comment_count' => '{0} Yorum Yok|{1} 1 Yorum|[2,*] :count Yorun',
+ 'comment_save' => 'Yorum Kaydet',
+ 'comment_saving' => 'Yorum kaydediliyor...',
+ 'comment_deleting' => 'Yorum siliniyor...',
+ 'comment_new' => 'Yeni Yorum',
+ 'comment_created' => 'yorum yaptı :createDiff',
+ 'comment_updated' => ':username tarafından :updateDiff önce güncellendi',
+ 'comment_deleted_success' => 'Yorum silindi',
+ 'comment_created_success' => 'Yorum eklendi',
+ 'comment_updated_success' => 'Yorum güncellendi',
+ 'comment_delete_confirm' => 'Bu yorumu silmek istediğinizden emin misiniz?',
+ 'comment_in_reply_to' => ':commentId yorumuna yanıt olarak',
+
+ // Revision
+ 'revision_delete_confirm' => 'Bu revizyonu silmek istediğinizden emin misiniz?',
+ 'revision_restore_confirm' => 'Bu revizyonu yeniden yüklemek istediğinizden emin misiniz? Mevcut sayfa içeriği değiştirilecektir.',
+ 'revision_delete_success' => 'Revizyon silindi',
+ 'revision_cannot_delete_latest' => 'Son revizyon silinemez.'
+];
\ No newline at end of file
--- /dev/null
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+ // Permissions
+ 'permission' => 'Bu sayfaya erişme yetkiniz yok.',
+ 'permissionJson' => 'Bu işlemi yapmak için yetkiniz yo.',
+
+ // Auth
+ 'error_user_exists_different_creds' => ':email adresi farklı kullanıcı bilgileri ile zaten kullanımda.',
+ 'email_already_confirmed' => 'E-mail halihazırda onaylanmış, giriş yapmayı dene.',
+ 'email_confirmation_invalid' => 'Bu doğrulama tokenı daha önce kullanılmış veya geçerli değil, lütfen tekrar kayıt olmayı deneyin.',
+ 'email_confirmation_expired' => 'Doğrulama token\'ının süresi geçmiş, yeni bir mail gönderildi.',
+ 'ldap_fail_anonymous' => 'Anonim LDAP girişi başarısız oldu',
+ 'ldap_fail_authed' => 'Verdiğiniz bilgiler ile LDAP girişi başarısız oldu.',
+ 'ldap_extension_not_installed' => 'LDAP PHP eklentisi yüklenmedi',
+ 'ldap_cannot_connect' => 'LDAP sunucusuna bağlanılamadı, ilk bağlantı başarısız oldu',
+ 'social_no_action_defined' => 'Bir aksiyon tanımlanmadı',
+ 'social_login_bad_response' => ":socialAccount girişi sırasında hata oluştu: \n:error",
+ 'social_account_in_use' => 'Bu :socialAccount zaten kullanımda, :socialAccount hesabıyla giriş yapmayı deneyin.',
+ 'social_account_email_in_use' => ':email adresi zaten kullanımda. Eğer zaten bir hesabınız varsa :socialAccount hesabınızı profil ayarları kısmından bağlayabilirsiniz.',
+ 'social_account_existing' => 'Bu :socialAccount zaten profilinize eklenmiş.',
+ 'social_account_already_used_existing' => 'Bu :socialAccount başka bir kullanıcı tarafından kullanılıyor.',
+ 'social_account_not_used' => 'Bu :socialAccount hesabı hiç bir kullanıcıya bağlı değil. Lütfen profil ayarlarına gidiniz ve bağlayınız. ',
+ 'social_account_register_instructions' => 'Hala bir hesabınız yoksa :socialAccount ile kayıt olabilirsiniz.',
+ 'social_driver_not_found' => 'Social driver bulunamadı',
+ 'social_driver_not_configured' => ':socialAccount ayarlarınız doğru bir şekilde ayarlanmadı.',
+
+ // System
+ 'path_not_writable' => ':filePath dosya yolu yüklenemedi. Sunucuya yazılabilir olduğundan emin olun.',
+ 'cannot_get_image_from_url' => ':url\'den görsel alınamadı',
+ 'cannot_create_thumbs' => 'Sunucu küçük resimleri oluşturamadı. Lütfen GD PHP eklentisinin yüklü olduğundan emin olun.',
+ 'server_upload_limit' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+ 'uploaded' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük boyutta dosya yüklemeyi deneyiniz.',
+ 'image_upload_error' => 'Görsel yüklenirken bir hata oluştu',
+ 'image_upload_type_error' => 'Yüklemeye çalıştığınız dosya türü geçerli değildir',
+ 'file_upload_timeout' => 'Dosya yüklemesi zaman aşımına uğradı',
+
+ // Attachments
+ 'attachment_page_mismatch' => 'Ek güncellemesi sırasında sayfa uyuşmazlığı yaşandı',
+ 'attachment_not_found' => 'Ek bulunamadı',
+
+ // Pages
+ 'page_draft_autosave_fail' => 'Taslak kaydetme başarısız. Sayfanızı kaydetmeden önce internet bağlantınız olduğundan emin olun',
+ 'page_custom_home_deletion' => 'Bu sayfa anasayfa olarak ayarlandığı için silinemez',
+
+ // Entities
+ 'entity_not_found' => 'Eleman bulunamadı',
+ 'bookshelf_not_found' => 'Kitaplık bulunamadı',
+ 'book_not_found' => 'Kitap bulunamadı',
+ 'page_not_found' => 'Sayfa bulunamadı',
+ 'chapter_not_found' => 'Bölüm bulunamadı',
+ 'selected_book_not_found' => 'Seçilen kitap bulunamadı',
+ 'selected_book_chapter_not_found' => 'Seçilen kitap veya bölüm bulunamadı',
+ 'guests_cannot_save_drafts' => 'Misafirler taslak kaydedemezler',
+
+ // Users
+ 'users_cannot_delete_only_admin' => 'Tek olan yöneticiyi silemezsiniz',
+ 'users_cannot_delete_guest' => 'Misafir kullanıyıcıyı silemezsiniz',
+
+ // Roles
+ 'role_cannot_be_edited' => 'Bu rol düzenlenemez',
+ 'role_system_cannot_be_deleted' => 'Bu bir yönetici rolüdür ve silinemez',
+ 'role_registration_default_cannot_delete' => 'Bu rol varsayılan yönetici rolü olarak atandığı için silinemez ',
+ 'role_cannot_remove_only_admin' => 'Bu kullanıcı yönetici rolü olan tek kullanıcı olduğu için silinemez. Bu kullanıcıyı silmek için önce başka bir kullanıcıya yönetici rolü atayın.',
+
+ // Comments
+ 'comment_list' => 'Yorumlar yüklenirken bir hata oluştu.',
+ 'cannot_add_comment_to_draft' => 'Taslaklara yorum ekleyemezsiniz.',
+ 'comment_add' => 'Yorum eklerken/güncellerken bir hata olıuştu.',
+ 'comment_delete' => 'Yorum silinirken bir hata oluştu.',
+ 'empty_comment' => 'Boş bir yorum eklenemez.',
+
+ // Error pages
+ '404_page_not_found' => 'Sayfa Bulunamadı',
+ 'sorry_page_not_found' => 'Üzgünüz, aradığınız sayfa bulunamıyor.',
+ 'return_home' => 'Anasayfaya dön',
+ 'error_occurred' => 'Bir Hata Oluştu',
+ 'app_down' => ':appName şu anda inaktif',
+ 'back_soon' => 'En kısa zamanda aktif hale gelecek.',
+
+];
--- /dev/null
+<?php
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
+return [
+
+ 'previous' => '« Önceki',
+ 'next' => 'Sonraki »',
+
+];
--- /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' => 'Parolanız en az 6 karakterden oluşmalı ve doğrulama parolası ile eşleşmelidir. ',
+ 'user' => "Bu e-mail adresi ile ilişkilendirilmiş bir kullanıcı bulamadık.",
+ 'token' => 'Parola yenileme tokeni geçerli değil.',
+ 'sent' => 'Parola sıfırlanma bağlantısını e-mail adresinize gönderdik!',
+ 'reset' => 'Parolanız sıfırlandı!',
+
+];
--- /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' => 'Ayarlar',
+ 'settings_save' => 'Ayarları Kaydet',
+ 'settings_save_success' => 'Ayarlar Kaydedildi',
+
+ // App Settings
+ 'app_customization' => 'Özelleştirme',
+ 'app_features_security' => 'Özellikler & Güvenlik',
+ 'app_name' => 'Uygulama Adı',
+ 'app_name_desc' => 'Bu isim başlıkta ve sistem tarafında gönderilen tüm mesajlarda gösterilecektir.',
+ 'app_name_header' => 'İsmi başlıkta göster',
+ 'app_public_access' => 'Açık Erişim',
+ 'app_public_access_desc' => 'Bu özelliği aktif etmek giriş yapmamış misafir kullanıcıların sizin BookStack uygulamanıza erişmesini sağlar',
+ 'app_public_access_desc_guest' => 'Kayıtlı olmayan kullanıcılar için erişim yetkisi "Guest" kullanıcısı üzerinden düzenlenebilir.',
+ 'app_public_access_toggle' => 'Açık erişime izin ver',
+ 'app_public_viewing' => 'Herkese açık görüntülenmeye izin verilsin mi?',
+ 'app_secure_images' => 'Daha Yüksek Güvenlikli Görsel Yüklemeleri',
+ 'app_secure_images_toggle' => 'Daha yüksek güveblikli görsel yüklemelerine izin ver',
+ 'app_secure_images_desc' => 'Performans sebepleri nedeniyle bütün görseller halka açık. Bu opsiyon rastgele ve tahmin edilmesi zor dizileri görsel linklerinin önüne ekler. Dizin indexlerinin kapalı olduğundan emin olun.',
+ 'app_editor' => 'Sayfa Editörü',
+ 'app_editor_desc' => 'Sayfa düzenlemesi yapılırken hangi editörün kullanılacağını seçin.',
+ 'app_custom_html' => 'Özel HTML Head İçeriği',
+ 'app_custom_html_desc' => 'Buraya eklenecek olan içerik <head> taginin en sonuna eklenecektir. Bu stilleri override ederken veya analytics eklerken faydalı bir kullanım şeklidir.',
+ 'app_custom_html_disabled_notice' => 'Yapılan hatalı değişikliklerin geriye alınabilmesi için bu sayfada özel HTML head içeriği kapalı.',
+ 'app_logo' => 'Uygulama Logosu',
+ 'app_logo_desc' => 'Bu görsel 43px yüksekliğinde olmalı. <br>Büyük görseller ölçeklenecektir.',
+ 'app_primary_color' => 'Uygulamanın Birincil Rengi',
+ 'app_primary_color_desc' => 'Bu bir hex değeri olmalıdır. <br>Varsayılan rengi seçmek için boş bırakın.',
+ 'app_homepage' => 'Uygulama Anasayfası',
+ 'app_homepage_desc' => 'Anasayfada görünmesi için bir view seçin. Sayfa izinleri seçili sayfalar için yok sayılacaktır.',
+ 'app_homepage_select' => 'Sayfa seçiniz',
+ 'app_disable_comments' => 'Yorumları Engelle',
+ 'app_disable_comments_toggle' => 'Yorumları engelle',
+ 'app_disable_comments_desc' => 'Yorumları uygulamadaki bütün sayfalar için engelle. <br> Mevcut yorumlar gösterilmeyecektir.',
+
+ // Registration Settings
+ 'reg_settings' => 'Kayıt',
+ 'reg_enable' => 'Kaydolmaya İzin Ver',
+ 'reg_enable_toggle' => 'Kaydolmaya izin ver',
+ 'reg_enable_desc' => 'Kayıt olmaya izin verdiğinizde kullanıcılar kendilerini uygulamaya kaydedebilecekler. Kayıt olduktan sonra kendilerine varsayılan kullanıcı rolü atanacaktır.',
+ 'reg_default_role' => 'Kayıt olduktan sonra varsayılan kullanıcı rolü',
+ 'reg_email_confirmation' => 'Email Doğrulama',
+ 'reg_email_confirmation_toggle' => 'E-mail onayı gerektir',
+ 'reg_confirm_email_desc' => 'Eğer domain kısıtlaması kullanılıyorsa o zaman email doğrulaması gereklidir ve bu seçenek yok sayılacaktır.',
+ 'reg_confirm_restrict_domain' => 'Domain Kısıtlaması',
+ 'reg_confirm_restrict_domain_desc' => 'Kısıtlamak istediğiniz email domainlerini vigül ile ayırarak yazınız. Kullanıcılara uygulamaya erişmeden önce adreslerini doğrulamak için bir mail gönderilecektir. <br> Kullanıcılar başarıyla kaydolduktan sonra email adreslerini değiştiremeyeceklerdir.',
+ 'reg_confirm_restrict_domain_placeholder' => 'Hiçbir kısıtlama tanımlanmamış',
+
+ // Maintenance settings
+ 'maint' => 'Bakım',
+ 'maint_image_cleanup' => 'Görsel Temizliği',
+ 'maint_image_cleanup_desc' => "Sayfaları ve revizyon içeriklerini tarayarak hangi gösel ve çizimlerin kullanımda olduğunu ve hangilerinin gereksiz olduğunu tespit eder. Bunu başlatmadan veritabanı ve görsellerin tam bir yedeğinin alındığından emin olun.",
+ 'maint_image_cleanup_ignore_revisions' => 'Revizyonlardaki görselleri yoksay',
+ 'maint_image_cleanup_run' => 'Temizliği Başlat',
+ 'maint_image_cleanup_warning' => ':count potansiyel kullanılmayan görsel bulundu. Bu görselleri silmek istediğinizden emin misiniz?',
+ 'maint_image_cleanup_success' => ':count potanisyel kullanılmayan görsel bulundu ve silindi!',
+ 'maint_image_cleanup_nothing_found' => 'Kullanılmayan görsel bulunamadı ve birşey silinmedi!',
+
+ // Role Settings
+ 'roles' => 'Roller',
+ 'role_user_roles' => 'Kullanıcı Rolleri',
+ 'role_create' => 'Yeni Rol Oluştur',
+ 'role_create_success' => 'Rol Başarıyla Oluşturuldu',
+ 'role_delete' => 'Rolü Sil',
+ 'role_delete_confirm' => 'Bu işlem \':roleName\' rolünü silecektir.',
+ 'role_delete_users_assigned' => 'Bu role atanmış :userCount adet kullanıcı var. Eğer bu kullanıcıların rollerini değiştirmek istiyorsanız aşağıdan yeni bir rol seçin.',
+ 'role_delete_no_migration' => "Kullanıcıları taşıma",
+ 'role_delete_sure' => 'Bu rolü silmek istediğinizden emin misiniz?',
+ 'role_delete_success' => 'Rol başarıyla silindi',
+ 'role_edit' => 'Rolü Düzenle',
+ 'role_details' => 'Rol Detayları',
+ 'role_name' => 'Rol Adı',
+ 'role_desc' => 'Rolün Kısa Tanımı',
+ 'role_external_auth_id' => 'Harici Authentication ID\'leri',
+ 'role_system' => 'Sistem Yetkileri',
+ 'role_manage_users' => 'Kullanıcıları yönet',
+ 'role_manage_roles' => 'Rolleri ve rol izinlerini yönet',
+ 'role_manage_entity_permissions' => 'Bütün kitap, bölüm ve sayfa izinlerini yönet',
+ 'role_manage_own_entity_permissions' => 'Sahip olunan kitap, bölüm ve sayfaların izinlerini yönet',
+ 'role_manage_settings' => 'Uygulama ayarlarını yönet',
+ 'role_asset' => 'Asset Yetkileri',
+ 'role_asset_desc' => 'Bu izinleri assetlere sistem içinden varsayılan erişimi kontrol eder. Kitaplar, bölümler ve sayfaların izinleri bu izinleri override eder.',
+ 'role_asset_admins' => 'Yöneticilere otomatik olarak bütün içeriğe erişim yetkisi verilir fakat bu opsiyonlar UI özelliklerini gösterir veya gizler.',
+ 'role_all' => 'Hepsi',
+ 'role_own' => 'Sahip Olunan',
+ 'role_controlled_by_asset' => 'Yükledikleri asset tarafından kontrol ediliyor',
+ 'role_save' => 'Rolü Kaydet',
+ 'role_update_success' => 'Rol başarıyla güncellendi',
+ 'role_users' => 'Bu roldeki kullanıcılar',
+ 'role_users_none' => 'Bu role henüz bir kullanıcı atanmadı',
+
+ // Users
+ 'users' => 'Kullanıcılar',
+ 'user_profile' => 'Kullanıcı Profili',
+ 'users_add_new' => 'Yeni Kullanıcı Ekle',
+ 'users_search' => 'Kullanıcıları Ara',
+ 'users_details' => 'Kullanıcı Detayları',
+ 'users_details_desc' => 'Bu kullanıcı için gösterilecek bir isim ve mail adresi belirleyin. Bu e-mail adresi kullanıcı tarafından giriş yaparken kullanılacak.',
+ 'users_details_desc_no_email' => 'Diğer kullanıcılar tarafından tanınabilmesi için bir isim belirleyin.',
+ 'users_role' => 'Kullanıcı Rolleri',
+ 'users_role_desc' => 'Bu kullanıcının hangi rollere atanabileceğini belirleyin. Eğer bir kullanıcıya birden fazla rol atanırsa, kullanıcı bütün rollerin özelliklerini kullanabilir.',
+ 'users_password' => 'Kullanıcı Parolası',
+ 'users_password_desc' => 'Kullanıcının giriş yaparken kullanacağı bir parola belirleyin. Parola en az 5 karakter olmalıdır.',
+ 'users_external_auth_id' => 'Harici Authentication ID\'si',
+ 'users_external_auth_id_desc' => 'Bu ID kullanıcı LDAP sunucu ile bağlantı kurarken kullanılır.',
+ 'users_password_warning' => 'Sadece parolanızı değiştirmek istiyorsanız aşağıyı doldurunuz.',
+ 'users_system_public' => 'Bu kullanıcı sizin uygulamanızı ziyaret eden bütün misafir kullanıcıları temsil eder. Giriş yapmak için kullanılamaz, otomatik olarak atanır.',
+ 'users_delete' => 'Kullanıcı Sil',
+ 'users_delete_named' => ':userName kullanıcısını sil ',
+ 'users_delete_warning' => 'Bu işlem \':userName\' kullanıcısını sistemden tamamen silecektir.',
+ 'users_delete_confirm' => 'Bu kullanıcıyı tamamen silmek istediğinize emin misiniz?',
+ 'users_delete_success' => 'Kullanıcılar başarıyla silindi.',
+ 'users_edit' => 'Kullanıcıyı Güncelle',
+ 'users_edit_profile' => 'Profili Düzenle',
+ 'users_edit_success' => 'Kullanıcı başarıyla güncellendi',
+ 'users_avatar' => 'Kullanıcı Avatarı',
+ 'users_avatar_desc' => 'Bu kullanıcıyı temsil eden bir görsel seçin. Yaklaşık 256px kare olmalıdır.',
+ 'users_preferred_language' => 'Tercih Edilen Dil',
+ 'users_preferred_language_desc' => 'Bu seçenek kullanıcı arayüzünün dilini değiştirecektir. Herhangi bir kullanıcı içeriğini etkilemeyecektir.',
+ 'users_social_accounts' => 'Sosyal Hesaplar',
+ 'users_social_accounts_info' => 'Burada diğer hesaplarınızı ekleyerek daha hızlı ve kolay giriş sağlayabilirsiniz. Bir hesabın bağlantısını kesmek daha önce edilnilen erişiminizi kaldırmaz. Profil ayarlarınızdan bağlı sosyal hesabınızın erişimini kaldırınız.',
+ 'users_social_connect' => 'Hesap Bağla',
+ 'users_social_disconnect' => 'Hesabın Bağlantısını Kes',
+ 'users_social_connected' => ':socialAccount hesabı profilinize başarıyla bağlandı.',
+ 'users_social_disconnected' => ':socialAccount hesabınızın profilinizle ilişiği başarıyla kesildi.',
+];
--- /dev/null
+<?php
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the size rules. Feel free to tweak each of these messages here.
+ */
+return [
+
+ // Standard laravel validation lines
+ 'accepted' => ':attribute kabul edilmelidir.',
+ 'active_url' => ':attribute geçerli bir URL adresi değildir.',
+ 'after' => ':attribute :date tarihinden sonra bir tarih olmalıdır.',
+ 'alpha' => ':attribute sadece harflerden oluşabilir.',
+ 'alpha_dash' => ':attribute sadece harf, rakam ve tirelerden oluşabilir.',
+ 'alpha_num' => ':attribute sadece harf ve rakam oluşabilir.',
+ 'array' => ':attribute array olmalıdır..',
+ 'before' => ':attribute :date tarihinden önce bir tarih olmalıdır.',
+ 'between' => [
+ 'numeric' => ':attribute, :min ve :max değerleri arasında olmalıdır.',
+ 'file' => ':attribute, :min ve :max kilobyte boyutları arasında olmalıdır.',
+ 'string' => ':attribute, :min ve :max karakter arasında olmalıdır.',
+ 'array' => ':attribute :min ve :max öge arasında olmalıdır.',
+ ],
+ 'boolean' => ':attribute true veya false olmalıdır.',
+ 'confirmed' => ':attribute doğrulaması eşleşmiyor.',
+ 'date' => ':attribute geçerli bir tarih değil.',
+ 'date_format' => ':attribute formatı :format\'ına uymuyor.',
+ 'different' => ':attribute be :other birbirinden farklı olmalıdır.',
+ 'digits' => ':attribute :digits basamaklı olmalıdır.',
+ 'digits_between' => ':attribute :min ve :max basamaklı olmalıdır.',
+ 'email' => ':attribute geçerli bir e-mail adresi olmalıdır.',
+ 'filled' => ':attribute gerekli bir alandır.',
+ 'exists' => 'Seçilen :attribute geçerli bir alan değildir.',
+ 'image' => ':attribute bir görsel olmalıdır.',
+ 'image_extension' => ':attribute geçerli ve desteklenen bir görsel uzantısı değildir.',
+ 'in' => 'Seçilen :attribute geçerli değildir.',
+ 'integer' => ':attribute bir integer değeri olmalıdır.',
+ 'ip' => ':attribute geçerli bir IP adresi olmalıdır.',
+ 'max' => [
+ 'numeric' => ':attribute, :max değerinden büyük olmamalıdır.',
+ 'file' => ':attribute, :max kilobyte boyutundan büyük olmamalıdır.',
+ 'string' => ':attribute, :max karakter boyutundan büyük olmamalıdır.',
+ 'array' => ':attribute, en fazla :max öge içermelidir.',
+ ],
+ 'mimes' => ':attribute :values dosya tipinde olmalıdır.',
+ 'min' => [
+ 'numeric' => ':attribute, :min değerinden az olmamalıdır.',
+ 'file' => ':attribute, :min kilobyte boyutundan küçük olmamalıdır.',
+ 'string' => ':attribute, :min karakter boyutundan küçük olmamalıdır.',
+ 'array' => ':attribute, en az :min öge içermelidir.',
+ ],
+ 'no_double_extension' => ':attribute sadece tek bir dosya tipinde olmalıdır.',
+ 'not_in' => 'Seçili :attribute geçerli değildir.',
+ 'numeric' => ':attribute rakam olmalıdır.',
+ 'regex' => ':attribute formatı geçerli değildir.',
+ 'required' => 'The :attribute field is required. :attribute alanı gereklidir.',
+ 'required_if' => ':other alanı :value değerinde ise :attribute alanı gereklidir.',
+ 'required_with' => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir.',
+ 'required_with_all' => 'Eğer :values değeri geçerli ise :attribute alanı gereklidir. ',
+ 'required_without' => 'Eğer :values değeri geçerli değil ise :attribute alanı gereklidir.',
+ 'required_without_all' => 'Eğer :values değerlerinden hiçbiri geçerli değil ise :attribute alanı gereklidir.',
+ 'same' => ':attribute ve :other eşleşmelidir.',
+ 'size' => [
+ 'numeric' => ':attribute, :size boyutunda olmalıdır.',
+ 'file' => ':attribute, :size kilobyte boyutunda olmalıdır.',
+ 'string' => ':attribute, :size karakter uzunluğunda olmalıdır.',
+ 'array' => ':attribute, :size sayıda öge içermelidir.',
+ ],
+ 'string' => ':attribute string olmalıdır.',
+ 'timezone' => ':attribute geçerli bir alan olmalıdır.',
+ 'unique' => ':attribute daha önce alınmış.',
+ 'url' => ':attribute formatı geçerli değil.',
+ 'uploaded' => 'Dosya yüklemesi başarısız oldu. Server bu boyutta dosyaları kabul etmiyor olabilir.',
+
+ // Custom validation lines
+ 'custom' => [
+ 'password-confirm' => [
+ 'required_with' => 'Parola onayı gereklidir.',
+ ],
+ ],
+
+ // Custom validation attributes
+ 'attributes' => [],
+];
'email' => 'Email',
'password' => 'Пароль',
'password_confirm' => 'Підтвердження пароля',
- 'password_hint' => 'Має бути більше 5 символів',
+ 'password_hint' => 'Має бути більше 7 символів',
'forgot_password' => 'Забули пароль?',
'remember_me' => 'Запам’ятати мене',
'ldap_email_hint' => 'Введіть email для цього облікового запису.',
'email' => 'Email地址',
'password' => '密码',
'password_confirm' => '确认密码',
- 'password_hint' => '必须超过5个字符',
+ 'password_hint' => '必须超过7个字符',
'forgot_password' => '忘记密码?',
'remember_me' => '记住我',
'ldap_email_hint' => '请输入用于此帐户的电子邮件。',
'email' => 'Email位址',
'password' => '密碼',
'password_confirm' => '確認密碼',
- 'password_hint' => '必須超過5個字元',
+ 'password_hint' => '必須超過7個字元',
'forgot_password' => '忘記密碼?',
'remember_me' => '記住我',
'ldap_email_hint' => '請輸入用於此帳號的電子郵件。',
}
}
-.bookshelf-grid-item .grid-card-content h2 a {
- color: $color-bookshelf;
- fill: $color-bookshelf;
-}
-
.book-grid-item .grid-card-footer {
p.small {
font-size: .8em;
button {
+ background-color: transparent;
+ border: 0;
font-size: 100%;
}
-@mixin generate-button-colors($textColor, $backgroundColor) {
- background-color: $backgroundColor;
- color: $textColor;
- fill: $textColor;
- border: 1px solid $backgroundColor;
- &:hover {
- background-color: lighten($backgroundColor, 8%);
- color: $textColor;
- }
- &:active {
- background-color: darken($backgroundColor, 8%);
- }
- &:focus {
- background-color: lighten($backgroundColor, 4%);
- box-shadow: $bs-light;
- color: $textColor;
- }
-}
-
-// Button Specific Variables
-$button-border-radius: 2px;
-
.button {
text-decoration: none;
font-size: 0.85rem;
display: inline-block;
font-weight: 400;
outline: 0;
- border-radius: $button-border-radius;
+ border-radius: 2px;
cursor: pointer;
- transition: background-color ease-in-out 120ms, box-shadow ease-in-out 120ms;
+ transition: background-color ease-in-out 120ms,
+ filter ease-in-out 120ms,
+ box-shadow ease-in-out 120ms;
box-shadow: none;
- background-color: $primary;
+ background-color: var(--color-primary);
color: #FFF;
fill: #FFF;
text-transform: uppercase;
- border: 1px solid $primary;
+ border: 1px solid var(--color-primary);
vertical-align: top;
- &:hover, &:focus {
+ &:hover, &:focus, &:active {
+ background-color: var(--color-primary);
text-decoration: none;
+ color: #FFFFFF;
+ }
+ &:hover {
+ box-shadow: $bs-light;
+ filter: brightness(110%);
+ }
+ &:focus {
+ outline: 1px dotted currentColor;
+ outline-offset: -$-xs;
+ box-shadow: none;
+ filter: brightness(90%);
}
&:active {
- background-color: darken($primary, 8%);
+ outline: 0;
}
}
-.button.primary {
- @include generate-button-colors(#FFFFFF, $primary);
-}
+
.button.outline {
background-color: transparent;
- color: #888;
- fill: #888;
- border: 1px solid #DDD;
+ color: #666;
+ fill: currentColor;
+ border: 1px solid #CCC;
&:hover, &:focus, &:active {
+ border: 1px solid #CCC;
box-shadow: none;
- background-color: #EEE;
+ background-color: #F2F2F2;
+ filter: none;
+ }
+ &:active {
+ border-color: #BBB;
+ background-color: #DDD;
+ color: #666;
+ box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
}
}
user-select: none;
font-size: 0.75rem;
line-height: 1.4em;
- &:focus, &:active {
+ color: var(--color-primary);
+ fill: var(--color-primary);
+ &:active {
outline: 0;
}
&:hover {
text-decoration: none;
}
+ &:hover, &:focus {
+ color: var(--color-primary);
+ fill: var(--color-primary);
+ }
}
.button.block {
.button[disabled] {
background-color: #BBB;
cursor: default;
+ border-color: #CCC;
&:hover {
background-color: #BBB;
cursor: default;
+/**
+ * Background colors
+ */
+
+.primary-background {
+ background-color: var(--color-primary) !important;
+}
+.primary-background-light {
+ background-color: var(--color-primary-light);
+}
/*
* Status text colors
* Style text colors
*/
.text-primary, .text-primary:hover, .text-primary-hover:hover {
- color: $primary !important;
- fill: $primary !important;
+ color: var(--color-primary) !important;
+ fill: var(--color-primary) !important;
}
.text-muted {
- color: lighten($text-dark, 26%) !important;
- fill: lighten($text-dark, 26%) !important;
- &.small, .small {
- color: lighten($text-dark, 32%) !important;
- fill: lighten($text-dark, 32%) !important;
- }
+ color: #575757 !important;
+ fill: #575757 !important;
}
/*
* Entity text colors
*/
.text-bookshelf, .text-bookshelf:hover {
- color: $color-bookshelf;
- fill: $color-bookshelf;
+ color: var(--color-bookshelf);
+ fill: var(--color-bookshelf);
}
.text-book, .text-book:hover {
- color: $color-book;
- fill: $color-book;
+ color: var(--color-book);
+ fill: var(--color-book);
}
.text-page, .text-page:hover {
- color: $color-page;
- fill: $color-page;
+ color: var(--color-page);
+ fill: var(--color-page);
}
.text-page.draft, .text-page.draft:hover {
- color: $color-page-draft;
- fill: $color-page-draft;
+ color: var(--color-page-draft);
+ fill: var(--color-page-draft);
}
.text-chapter, .text-chapter:hover {
- color: $color-chapter;
- fill: $color-chapter;
+ color: var(--color-chapter);
+ fill: var(--color-chapter);
}
/*
background-color: #FFFFFF;
}
.bg-book {
- background-color: $color-book;
+ background-color: var(--color-book);
}
.bg-chapter {
- background-color: $color-chapter;
+ background-color: var(--color-chapter);
}
.bg-shelf {
- background-color: $color-bookshelf;
+ background-color: var(--color-bookshelf);
}
\ No newline at end of file
.popup-content {
overflow-y: auto;
}
+ &:focus {
+ outline: 0;
+ }
}
.popup-footer button, .popup-header-close {
padding: 8px $-m;
}
}
-.popup-footer {
- margin-top: 1px;
-}
body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: 444px;
min-height: 444px;
}
}
+.nav-tabs {
+ text-align: center;
+ a, .tab-item {
+ padding: $-m;
+ display: inline-block;
+ color: #666;
+ fill: #666;
+ cursor: pointer;
+ &.selected {
+ border-bottom: 2px solid var(--color-primary);
+ }
+ }
+}
+
.image-picker .none {
display: none;
}
opacity: 0;
transition: opacity ease-in-out 120ms;
}
- &:hover .actions {
+ &:hover .actions, &:focus-within .actions {
opacity: 1;
}
}
}
a { color: #666; }
span {
- color: #888;
padding-left: $-xxs;
}
}
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
}
&:focus {
- outline: 0;
+ border-color: var(--color-primary);
+ outline: 1px solid var(--color-primary);
}
}
}
.markdown-display {
- padding: 0 $-m 0;
margin-left: -1px;
- overflow-y: scroll;
- &.page-content {
- margin: 0 auto;
- width: 100%;
- max-width: 100%;
+}
+
+.markdown-editor-display {
+ background-color: #FFFFFF;
+ body {
+ background-color: #FFFFFF;
+ padding-left: 16px;
+ padding-right: 16px;
}
[drawio-diagram]:hover {
- outline: 2px solid $primary;
+ outline: 2px solid var(--color-primary);
}
}
margin-left: -$-m;
margin-right: -$-m;
padding: $-s $-m;
+ display: block;
+ width: calc(100% + 32px);
+ text-align: left;
}
.collapse-title, .collapse-title label {
cursor: pointer;
button {
background-color: transparent;
border: none;
- color: $primary;
+ fill: #666;
padding: 0;
cursor: pointer;
position: absolute;
left: 8px;
- top: 9.5px;
+ top: 9px;
}
input {
display: block;
- padding-left: $-l;
+ padding-left: $-l + 4px;
width: 300px;
max-width: 100%;
}
background-color: #BBB;
max-width: 100%;
}
+
+.custom-file-input {
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
+ height: 1px;
+ border: 0;
+ clip: rect(0, 0, 0, 0);
+}
+.custom-file-input:focus + label {
+ border-color: var(--color-primary);
+ outline: 1px solid var(--color-primary);
+}
\ No newline at end of file
display: block;
z-index: 11;
top: 0;
- background-color: $primary-dark;
color: #fff;
fill: #fff;
border-bottom: 1px solid #DDD;
}
.user-name {
vertical-align: top;
- padding-top: $-m;
position: relative;
- top: -3px;
display: inline-block;
cursor: pointer;
> * {
}
}
+.header *, .primary-background * {
+ outline-color: #FFF;
+}
.header-search {
color: #EEE;
z-index: 2;
padding-left: 40px;
+ &:focus {
+ outline: none;
+ border: 1px solid rgba(255, 255, 255, 0.6);
+ }
}
button {
fill: #EEE;
::-moz-placeholder { /* Firefox 19+ */
color: #DDD;
}
- :-ms-input-placeholder { /* IE 10+ */
- color: #DDD;
- }
- :-moz-placeholder { /* Firefox 18- */
- color: #DDD;
- }
@include between($l, $xl) {
max-width: 200px;
}
line-height: 0.8;
margin: -2px 0 0;
}
- &:hover {
+ &:hover, &:focus-within {
opacity: 1;
}
}
.action-buttons .dropdown-container:last-child a {
padding-left: $-xs;
}
-}
-
-.nav-tabs {
- text-align: center;
- a, .tab-item {
- padding: $-m;
- display: inline-block;
- color: #666;
- fill: #666;
- cursor: pointer;
- &.selected {
- border-bottom: 2px solid $primary;
- }
- }
}
\ No newline at end of file
* {
box-sizing: border-box;
+ outline-color: #444444;
+}
+
+*:focus {
+ outline-style: dotted;
}
html {
min-height: 0;
max-width: 100%;
position: relative;
+ overflow-y: hidden;
}
.flex {
.tri-layout-mobile-tabs {
display: none;
}
- .tri-layout-left-contents > div, .tri-layout-right-contents > div {
+ .tri-layout-left-contents > *, .tri-layout-right-contents > * {
opacity: 0.6;
transition: opacity ease-in-out 120ms;
&:hover {
opacity: 1;
}
+ &:focus-within {
+ opacity: 1;
+ }
}
+
}
@include smaller-than($m) {
.chapter-expansion-toggle {
border-radius: 0 4px 4px 0;
padding: $-xs $-m;
+ width: 100%;
+ text-align: left;
}
.chapter-expansion-toggle:hover {
background-color: rgba(0, 0, 0, 0.06);
}
.sort-box {
margin-bottom: $-m;
- border: 2px solid rgba($color-book, 0.6);
padding: $-m $-xl;
- border-radius: 4px;
+ position: relative;
+ &::before {
+ pointer-events: none;
+ content: '';
+ border-radius: 4px;
+ opacity: 0.5;
+ border: 2px solid var(--color-book);
+ display: block;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ position: absolute;
+ }
}
.sort-box-options {
display: flex;
border: 1px solid #DDD;
margin-top: -1px;
min-height: 38px;
- &.text-chapter {
- border-left: 2px solid $color-chapter;
- }
- &.text-page {
- border-left: 2px solid $color-page;
- }
+ }
+ li.text-page, li.text-chapter {
+ border-left: 2px solid currentColor;
}
li:first-child {
margin-top: $-xs;
display: grid;
grid-template-columns: min-content 1fr;
grid-column-gap: $-m;
- color: #888;
- fill: #888;
font-size: 0.9em;
}
.card .activity-list-item {
margin-top: 0;
}
.page.draft .text-page {
- color: $color-page-draft;
- fill: $color-page-draft;
+ color: var(--color-page-draft);
+ fill: var(--color-page-draft);
}
> .dropdown-container {
display: block;
background-color: transparent;
border-color: rgba(0, 0, 0, 0.1);
}
+ &:focus {
+ background-color: #eee;
+ outline: 1px dotted #666;
+ outline-offset: -2px;
+ }
}
.entity-list-item-path-sep {
display: block;
padding: $-xs $-m;
color: #555;
- fill: #555;
+ fill: currentColor;
white-space: nowrap;
- &:hover {
+ &:hover, &:focus {
text-decoration: none;
- background-color: #EEE;
+ background-color: var(--color-primary-light);
+ color: var(--color-primary);
+ }
+ &:focus {
+ outline: 1px solid var(--color-primary);
+ outline-offset: -2px;
}
svg {
margin-right: $-s;
}
a.button {
margin: 0;
- color: #FFF;
}
.svg-icon {
width: 1.2em;
padding: 0;
margin: 0;
}
- .tabs > span {
+ .tabs > button {
display: block;
cursor: pointer;
padding: $-s $-m;
line-height: 1.6;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
- &.open .tabs > span.active {
+ &.open .tabs > button.active {
fill: #444;
background-color: rgba(0, 0, 0, 0.1);
}
* Link styling
*/
a {
- color: $primary;
+ color: var(--color-primary);
+ fill: var(--color-primary);
cursor: pointer;
text-decoration: none;
- transition: color ease-in-out 80ms;
+ transition: filter ease-in-out 80ms;
line-height: 1.6;
&:hover {
text-decoration: underline;
- color: darken($primary, 20%);
}
&.icon {
display: inline-block;
position: relative;
display: inline-block;
}
+ &:focus img:only-child {
+ outline: 2px dashed var(--color-primary);
+ outline-offset: 2px;
+ }
}
.blended-links a {
blockquote {
display: block;
position: relative;
- border-left: 4px solid $primary;
+ border-left: 4px solid var(--color-primary);
background-color: #F8F8F8;
padding: $-s $-m $-s $-xl;
&:before {
}
span.highlight {
- //background-color: rgba($primary, 0.2);
font-weight: bold;
padding: 2px 4px;
}
.page-content.mce-content-body {
padding-top: 16px;
+ outline: none;
}
// Fix to prevent 'No color' option from not being clickable.
$fs-s: 12px;
// Colours
-$primary: #0288D1;
-$primary-dark: #0288D1;
-$secondary: #cf4d03;
+:root {
+ --color-primary: #206ea7;
+ --color-primary-light: rgba(32,110,167,0.15);
+
+ --color-page: #206ea7;
+ --color-page-draft: #7e50b1;
+ --color-chapter: #af4d0d;
+ --color-book: #077b70;
+ --color-bookshelf: #a94747;
+}
+
$positive: #0f7d15;
$negative: #ab0f0e;
-$info: $primary;
-$warning: $secondary;
-$primary-faded: rgba(21, 101, 192, 0.15);
-
-// Item Colors
-$color-bookshelf: #af5a5a;
-$color-book: #009688;
-$color-chapter: #d7804a;
-$color-page: $primary;
-$color-page-draft: #9A60DA;
+$info: #0288D1;
+$warning: #cf4d03;
// Text colours
$text-dark: #444;
@import "lists";
@import "pages";
+
+html, body {
+ background-color: #FFF;
+}
+
body {
font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
- background-color: #FFF;
margin: 0;
padding: 0;
}
--- /dev/null
+@import "variables";
+
+header {
+ display: none;
+}
+
+html, body {
+ font-size: 12px;
+ background-color: #FFF;
+}
+
+.page-content {
+ margin: 0 auto;
+}
+
+.print-hidden {
+ display: none !important;
+}
+
+.tri-layout-container {
+ grid-template-columns: 1fr;
+ grid-template-areas: "b";
+ margin-left: 0;
+ margin-right: 0;
+ display: block;
+}
+
+.card {
+ box-shadow: none;
+}
+
+.content-wrap.card {
+ padding-left: 0;
+ padding-right: 0;
+}
\ No newline at end of file
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
margin-right: 4px;
- background-color: $color-page;
+ background-color: var(--color-page);
animation-delay: 0.3s;
}
> div:first-child {
left: -($loadingSize+$-xs);
- background-color: $color-book;
+ background-color: var(--color-book);
animation-delay: 0s;
}
> div:last-of-type {
left: $loadingSize+$-xs;
- background-color: $color-chapter;
+ background-color: var(--color-chapter);
animation-delay: 0.6s;
}
> span {
// Back to top link
$btt-size: 40px;
[back-to-top] {
- background-color: $primary;
+ background-color: var(--color-primary);
position: fixed;
bottom: $-m;
right: $-l;
margin-bottom: 0;
}
.entity-list-item.selected {
- background-color: rgba(0, 0, 0, 0.15) !important;
+ background-color: rgba(0, 0, 0, 0.05) !important;
}
.loading {
height: 400px;
.list-sort {
display: inline-grid;
margin-left: $-s;
- grid-template-columns: 120px 40px;
+ grid-template-columns: minmax(120px, max-content) 40px;
+ font-size: 0.9rem;
border: 2px solid #DDD;
border-radius: 4px;
}
.list-sort-label {
font-weight: bold;
display: inline-block;
- color: #888;
+ color: #555;
}
.list-sort-type {
text-align: left;
<div class="form-group">
<label for="username">{{ trans('auth.username') }}</label>
- @include('form.text', ['name' => 'username', 'tabindex' => 1])
+ @include('form.text', ['name' => 'username', 'autofocus' => true])
</div>
@if(session('request-email', false) === true)
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email', 'tabindex' => 1])
+ @include('form.text', ['name' => 'email'])
<span class="text-neg">
{{ trans('auth.ldap_email_hint') }}
</span>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password', 'tabindex' => 1])
+ @include('form.password', ['name' => 'password'])
</div>
\ No newline at end of file
<div class="form-group">
<label for="email">{{ trans('auth.email') }}</label>
- @include('form.text', ['name' => 'email', 'tabindex' => 1])
+ @include('form.text', ['name' => 'email', 'autofocus' => true])
</div>
<div class="form-group">
<label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password', 'tabindex' => 1])
+ @include('form.password', ['name' => 'password'])
<span class="block small mt-s">
<a href="{{ url('/password/email') }}">{{ trans('auth.forgot_password') }}</a>
</span>
--- /dev/null
+@extends('simple-layout')
+
+@section('content')
+
+ <div class="container very-small mt-xl">
+ <div class="card content-wrap auto-height">
+ <h1 class="list-heading">{{ trans('auth.user_invite_page_welcome', ['appName' => setting('app-name')]) }}</h1>
+ <p>{{ trans('auth.user_invite_page_text', ['appName' => setting('app-name')]) }}</p>
+
+ <form action="{{ url('/register/invite/' . $token) }}" method="POST" class="stretch-inputs">
+ {!! csrf_field() !!}
+
+ <div class="form-group">
+ <label for="password">{{ trans('auth.password') }}</label>
+ @include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
+ </div>
+
+ <div class="text-right">
+ <button class="button">{{ trans('auth.user_invite_page_confirm_button') }}</button>
+ </div>
+
+ </form>
+
+ </div>
+ </div>
+
+@stop
<div class="my-l"> </div>
<div class="card content-wrap auto-height">
- <h1 class="list-heading">{{ title_case(trans('auth.log_in')) }}</h1>
+ <h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
<form action="{{ url('/login') }}" method="POST" id="login-form" class="mt-l">
{!! csrf_field() !!}
'name' => 'remember',
'checked' => false,
'value' => 'on',
- 'tabindex' => 1,
'label' => trans('auth.remember_me'),
])
</div>
<div class="text-right">
- <button class="button primary" tabindex="1">{{ title_case(trans('auth.log_in')) }}</button>
+ <button class="button">{{ Str::title(trans('auth.log_in')) }}</button>
</div>
</div>
</div>
<div class="from-group text-right mt-m">
- <button class="button primary">{{ trans('auth.reset_password_send_button') }}</button>
+ <button class="button">{{ trans('auth.reset_password_send_button') }}</button>
</div>
</form>
</div>
<div class="from-group text-right mt-m">
- <button class="button primary">{{ trans('auth.reset_password') }}</button>
+ <button class="button">{{ trans('auth.reset_password') }}</button>
</div>
</form>
<div class="my-l"> </div>
<div class="card content-wrap auto-height">
- <h1 class="list-heading">{{ title_case(trans('auth.sign_up')) }}</h1>
+ <h1 class="list-heading">{{ Str::title(trans('auth.sign_up')) }}</h1>
<form action="{{ url("/register") }}" method="POST" class="mt-l stretch-inputs">
{!! csrf_field() !!}
<a href="{{ url('/login') }}">{{ trans('auth.already_have_account') }}</a>
</div>
<div class="from-group text-right">
- <button class="button primary">{{ trans('auth.create_account') }}</button>
+ <button class="button">{{ trans('auth.create_account') }}</button>
</div>
</div>
@endif
</div>
<div class="form-group text-right mt-m">
- <button type="submit" class="button primary">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
+ <button type="submit" class="button">{{ trans('auth.email_not_confirmed_resend_button') }}</button>
</div>
</form>
<!DOCTYPE html>
-<html class="@yield('body-class')">
+<html lang="{{ config('app.lang') }}" class="@yield('body-class')">
<head>
<title>{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}</title>
@include('partials.notifications')
@include('common.header')
- <section id="content" class="block">
+ <div id="content" class="block">
@yield('content')
- </section>
+ </div>
- <div back-to-top class="primary-background">
+ <div back-to-top class="primary-background print-hidden">
<div class="inner">
@icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
</div>
@endif
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.books_create') }}</h1>
<form action="{{ isset($bookshelf) ? $bookshelf->getUrl('/create-book') : url('/books') }}" method="POST" enctype="multipart/form-data">
@include('books.form')
</form>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
{!! csrf_field() !!}
<input type="hidden" name="_method" value="DELETE">
<a href="{{$book->getUrl()}}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
</form>
</div>
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.books_edit') }}</h1>
<form action="{{ $book->getUrl() }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_method" value="PUT">
@include('books.form', ['model' => $book])
</form>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
<!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $book->name }}</title>
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
@if($bookChild->isA('chapter'))
- <p>{{ $bookChild->text }}</p>
+ <p>{{ $bookChild->description }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)
</div>
<div class="form-group" collapsible id="logo-control">
- <div class="collapse-title text-primary" collapsible-trigger>
- <label for="user-avatar">{{ trans('common.cover_image') }}</label>
- </div>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+ <label>{{ trans('common.cover_image') }}</label>
+ </button>
<div class="collapse-content" collapsible-content>
<p class="small">{{ trans('common.cover_image_description') }}</p>
</div>
<div class="form-group" collapsible id="tags-control">
- <div class="collapse-title text-primary" collapsible-trigger>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
<label for="tag-manager">{{ trans('entities.book_tags') }}</label>
- </div>
+ </button>
<div class="collapse-content" collapsible-content>
@include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
</div>
<div class="form-group text-right">
<a href="{{ isset($book) ? $book->getUrl() : url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.books_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.books_save') }}</button>
</div>
\ No newline at end of file
-<div class="content-wrap mt-m card">
+<main class="content-wrap mt-m card">
<div class="grid half v-center no-row-gap">
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
<div class="text-m-right my-m">
- @include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])
+ @include('partials.sort', ['options' => [
+ 'name' => trans('common.sort_name'),
+ 'created_at' => trans('common.sort_created_at'),
+ 'updated_at' => trans('common.sort_updated_at'),
+ ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
</div>
</div>
<a href="{{ url("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_now') }}</a>
@endif
@endif
-</div>
\ No newline at end of file
+</main>
\ No newline at end of file
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.books_permissions') }}</h1>
@include('form.entity-permissions', ['model' => $book])
- </div>
+ </main>
</div>
@stop
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="break-text" v-pre>{{$book->name}}</h1>
<div class="book-content" v-show="!searching">
<p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p>
</div>
@include('partials.entity-dashboard-search-results')
- </div>
+ </main>
@stop
<hr class="primary-background">
- <div dropdown class="dropdown-container">
- <div dropdown-toggle class="icon-list-item">
- <span>@icon('export')</span>
- <span>{{ trans('entities.export') }}</span>
- </div>
- <ul class="wide dropdown-menu">
- <li><a href="{{ $book->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{ $book->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{ $book->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
- </ul>
- </div>
+ @include('partials.entity-export-menu', ['entity' => $book])
</div>
</div>
<input book-sort-input type="hidden" name="sort-tree">
<div class="list text-right">
<a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button class="button primary" type="submit">{{ trans('entities.books_sort_save') }}</button>
+ <button class="button" type="submit">{{ trans('entities.books_sort_save') }}</button>
</div>
</form>
</div>
</div>
<div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h2 class="list-heading mb-m">{{ trans('entities.books_sort_show_other') }}</h2>
@include('components.entity-selector', ['name' => 'books_list', 'selectorSize' => 'compact', 'entityTypes' => 'book', 'entityPermission' => 'update', 'showAdd' => true])
- </div>
+ </main>
</div>
</div>
<div class="chapter-child-menu">
- <p chapter-toggle class="text-muted @if($bookChild->matchesOrContains($current)) open @endif">
+ <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>
- </p>
- <ul class="sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">
+ </button>
+ <ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
@foreach($bookChild->pages as $childPage)
- <li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}">
+ <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>
@endforeach
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.chapters_create') }}</h1>
<form action="{{ $book->getUrl('/create-chapter') }}" method="POST">
@include('chapters.form')
</form>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
<div class="text-right">
<a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
</div>
</form>
</div>
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="list-heading">{{ trans('entities.chapters_edit') }}</h1>
<form action="{{ $chapter->getUrl() }}" method="POST">
<input type="hidden" name="_method" value="PUT">
@include('chapters.form', ['model' => $chapter])
</form>
- </div>
+ </main>
</div>
<!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $chapter->name }}</title>
</div>
<div class="form-group" collapsible id="logo-control">
- <div class="collapse-title text-primary" collapsible-trigger>
- <label for="user-avatar">{{ trans('entities.chapter_tags') }}</label>
- </div>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+ <label for="tags">{{ trans('entities.chapter_tags') }}</label>
+ </button>
<div class="collapse-content" collapsible-content>
@include('components.tag-manager', ['entity' => isset($chapter)?$chapter:null, 'entityType' => 'chapter'])
</div>
<div class="form-group text-right">
<a href="{{ isset($chapter) ? $chapter->getUrl() : $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.chapters_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.chapters_save') }}</button>
</div>
<div class="chapter chapter-expansion">
<span class="icon text-chapter">@icon('page')</span>
<div class="content">
- <div chapter-toggle class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></div>
+ <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>
<div class="inset-list">
<div class="entity-list-item-children">
@include('partials.entity-list', ['entities' => $chapter->pages])
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.chapters_move') }}</h1>
<form action="{{ $chapter->getUrl('/move') }}" method="POST">
<div class="form-group text-right">
<a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.chapters_move') }}</button>
+ <button type="submit" class="button">{{ trans('entities.chapters_move') }}</button>
</div>
</form>
- </div>
+ </main>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.chapters_permissions') }}</h1>
@include('form.entity-permissions', ['model' => $chapter])
- </div>
+ </main>
</div>
@stop
@section('body')
- <div class="mb-m">
+ <div class="mb-m print-hidden">
@include('partials.breadcrumbs', ['crumbs' => [
$chapter->book,
$chapter,
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<h1 class="break-text" v-pre>{{ $chapter->name }}</h1>
<div class="chapter-content" v-show="!searching">
<p v-pre class="text-muted break-text">{!! nl2br(e($chapter->description)) !!}</p>
</div>
@include('partials.entity-dashboard-search-results')
- </div>
+ </main>
@stop
<hr class="primary-background"/>
- <div dropdown class="dropdown-container">
- <div dropdown-toggle class="icon-list-item">
- <span>@icon('export')</span>
- <span>{{ trans('entities.export') }}</span>
- </div>
- <ul class="wide dropdown-menu">
- <li><a href="{{ $chapter->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{ $chapter->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{ $chapter->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
- </ul>
- </div>
+ @include('partials.entity-export-menu', ['entity' => $chapter])
</div>
</div>
@stop
<div class="comment-box mb-m" comment="{{ $comment->id }}" local-id="{{$comment->local_id}}" parent-id="{{$comment->parent_id}}" id="comment{{$comment->local_id}}">
<div class="header p-s">
- <div class="grid half no-gap v-center">
- <div class="meta">
- <a href="#comment{{$comment->local_id}}" class="text-muted">#{{$comment->local_id}}</a>
+ <div class="grid half left-focus no-gap v-center">
+ <div class="meta text-muted text-small">
+ <a href="#comment{{$comment->local_id}}">#{{$comment->local_id}}</a>
@if ($comment->createdBy)
<img width="50" src="{{ $comment->createdBy->getAvatar(50) }}" class="avatar" alt="{{ $comment->createdBy->name }}">
</div>
<div class="actions text-right">
@if(userCan('comment-update', $comment))
- <button type="button" class="text-button" action="edit" title="{{ trans('common.edit') }}">@icon('edit')</button>
+ <button type="button" class="text-button" action="edit" aria-label="{{ trans('common.edit') }}" title="{{ trans('common.edit') }}">@icon('edit')</button>
@endif
@if(userCan('comment-create-all'))
- <button type="button" class="text-button" action="reply" title="{{ trans('common.reply') }}">@icon('reply')</button>
+ <button type="button" class="text-button" action="reply" aria-label="{{ trans('common.reply') }}" title="{{ trans('common.reply') }}">@icon('reply')</button>
@endif
@if(userCan('comment-delete', $comment))
<div dropdown class="dropdown-container">
- <button type="button" dropdown-toggle class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
- <ul class="dropdown-menu">
+ <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" class="text-button" title="{{ trans('common.delete') }}">@icon('delete')</button>
+ <ul class="dropdown-menu" role="menu">
<li class="px-m text-small text-muted pb-s">{{trans('entities.comment_delete_confirm')}}</li>
- <li><a action="delete" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</a></li>
+ <li><button action="delete" type="button" class="text-button text-neg" >@icon('delete'){{ trans('common.delete') }}</button></li>
</ul>
</div>
@endif
</div>
<div class="form-group text-right">
<button type="button" class="button outline" action="closeUpdateForm">{{ trans('common.cancel') }}</button>
- <button type="submit" class="button primary">{{ trans('entities.comment_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
</div>
<div class="form-group loading" style="display: none;">
@include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
-<div page-comments page-id="{{ $page->id }}" class="comments-list">
+<section page-comments page-id="{{ $page->id }}" class="comments-list" aria-label="{{ trans('entities.comments') }}">
@exposeTranslations([
'entities.comment_updated_success',
<div comment-count-bar class="grid half left-focus v-center no-row-gap">
<h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
- @if (count($page->comments) === 0)
+ @if (count($page->comments) === 0 && userCan('comment-create-all'))
<div class="text-m-right" comment-add-button-container>
<button type="button" action="addComment"
class="button outline">{{ trans('entities.comment_add') }}</button>
@if(userCan('comment-create-all'))
@include('comments.create')
- @endif
- @if (count($page->comments) > 0)
- <div class="text-right" comment-add-button-container>
- <button type="button" action="addComment"
- class="button outline">{{ trans('entities.comment_add') }}</button>
- </div>
+ @if (count($page->comments) > 0)
+ <div class="text-right" comment-add-button-container>
+ <button type="button" action="addComment"
+ class="button outline">{{ trans('entities.comment_add') }}</button>
+ </div>
+ @endif
@endif
-</div>
\ No newline at end of file
+</section>
\ No newline at end of file
<div class="form-group text-right">
<button type="button" class="button outline"
action="hideForm">{{ trans('common.cancel') }}</button>
- <button type="submit" class="button primary">{{ trans('entities.comment_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.comment_save') }}</button>
</div>
<div class="form-group loading" style="display: none;">
@include('partials.loading-icon', ['text' => trans('entities.comment_saving')])
<div class="header-search hide-under-l">
@if (hasAppAccess())
- <form action="{{ url('/search') }}" method="GET" class="search-box">
- <button id="header-search-box-button" type="submit">@icon('search') </button>
- <input id="header-search-box-input" type="text" name="term" tabindex="2" placeholder="{{ trans('common.search') }}" value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+ <form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
+ <button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
+ <input id="header-search-box-input" type="text" name="term"
+ aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
+ value="{{ isset($searchTerm) ? $searchTerm : '' }}">
</form>
@endif
</div>
<div class="text-right">
- <div class="header-links">
+ <nav class="header-links" >
<div class="links text-center">
@if (hasAppAccess())
<a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
@if(signedInUser())
<?php $currentUser = user(); ?>
<div class="dropdown-container" dropdown>
- <span class="user-name hide-under-l" dropdown-toggle>
+ <span class="user-name py-s hide-under-l" dropdown-toggle
+ aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.profile_menu') }}" tabindex="0">
<img class="avatar" src="{{$currentUser->getAvatar(30)}}" alt="{{ $currentUser->name }}">
<span class="name">{{ $currentUser->getShortName(9) }}</span> @icon('caret-down')
</span>
- <ul class="dropdown-menu">
+ <ul class="dropdown-menu" role="menu">
<li>
<a href="{{ url("/user/{$currentUser->id}") }}">@icon('user'){{ trans('common.view_profile') }}</a>
</li>
</ul>
</div>
@endif
- </div>
+ </nav>
</div>
</div>
@section('body')
<div class="mt-m">
- <div class="content-wrap card">
+ <main class="content-wrap card">
<div class="page-content" page-display="{{ $customHomepage->id }}">
@include('pages.page-display', ['page' => $customHomepage])
</div>
- </div>
+ </main>
</div>
@stop
<div id="code-editor">
<div overlay ref="overlay" v-cloak @click="hide()">
- <div class="popup-body" @click.stop>
+ <div class="popup-body" tabindex="-1" @click.stop>
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.code_editor') }}</div>
</div>
<div class="form-group">
- <button type="button" class="button primary" @click="save()">{{ trans('components.code_save') }}</button>
+ <button type="button" class="button" @click="save()">{{ trans('components.code_save') }}</button>
</div>
</div>
$value
$checked
$label
-$tabindex
--}}
<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
<input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif>
- <span tabindex="{{ $tabindex ?? '0' }}"
- role="checkbox"
+ <span tabindex="0" role="checkbox"
aria-checked="{{ $checked ? 'true' : 'false' }}"
class="custom-checkbox text-primary">@icon('check')</span>
<span class="label">{{$label}}</span>
<div id="entity-selector-wrap">
<div overlay entity-selector-popup>
- <div class="popup-body small">
+ <div class="popup-body small" tabindex="-1">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('entities.entity_select') }}</div>
<button type="button" class="popup-header-close">x</button>
</div>
@include('components.entity-selector', ['name' => 'entity-selector'])
<div class="popup-footer">
- <button type="button" disabled="true" class="button entity-link-selector-confirm primary corner-button">{{ trans('common.select') }}</button>
+ <button type="button" disabled="true" class="button entity-link-selector-confirm corner-button">{{ trans('common.select') }}</button>
</div>
</div>
</div>
$key - Unique key for checking existing stored state.
--}}
<?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<a expand-toggle="{{ $target }}"
+<button type="button" expand-toggle="{{ $target }}"
expand-toggle-update-endpoint="{{ url('/settings/users/'. $currentUser->id .'/update-expansion-preference/' . $key) }}"
expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
class="text-muted icon-list-item text-primary">
<span>@icon('expand-text')</span>
<span>{{ trans('common.toggle_details') }}</span>
-</a>
+</button>
@if($isOpen)
@push('head')
<style>
])
<div overlay v-cloak @click="hide">
- <div class="popup-body" @click.stop="">
+ <div class="popup-body" tabindex="-1" @click.stop>
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.image_select') }}</div>
<button type="button" class="button icon outline" @click="deleteImage">@icon('delete')</button>
</div>
- <button class="button primary anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
+ <button class="button anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
{{ trans('components.image_select_image') }}
</button>
<div class="clearfix"></div>
</div>
<div class="text-center">
+ <input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
<label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
- <input type="file" class="hidden" accept="image/*" name="{{ $name }}" id="{{ $name }}">
<input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
@if(isset($removeName))
<input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
<div class="tags">
<p class="text-muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
-
<draggable :options="{handle: '.handle'}" :list="tags" element="div">
<div v-for="(tag, i) in tags" :key="tag.key" class="card drag-card">
<div class="handle" >@icon('grip')</div>
<div>
<autosuggest url="{{ url('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
- v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
+ v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_name') }}"/>
</div>
<div>
<autosuggest url="{{ url('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"/>
</div>
- <div v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</div>
+ <button type="button" aria-label="{{ trans('entities.tags_remove') }}" v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</button>
</div>
</draggable>
<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
<input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
<input type="checkbox" @if($value) checked="checked" @endif>
- <span tabindex="{{ $tabindex ?? '0' }}"
- role="checkbox"
+ <span tabindex="0" role="checkbox"
aria-checked="{{ $value ? 'true' : 'false' }}"
class="custom-checkbox text-primary">@icon('check')</span>
<span class="label">{{ $label }}</span>
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.pages_popular') }}</h3>
<div class="px-m">
- @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'page'), 'style' => 'compact'])
+ @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['page']), 'style' => 'compact'])
</div>
</div>
</div>
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.books_popular') }}</h3>
<div class="px-m">
- @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'book'), 'style' => 'compact'])
+ @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['book']), 'style' => 'compact'])
</div>
</div>
</div>
<div class="card mb-xl">
<h3 class="card-title">{{ trans('entities.chapters_popular') }}</h3>
<div class="px-m">
- @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, 'chapter'), 'style' => 'compact'])
+ @include('partials.entity-list', ['entities' => Views::getPopular(10, 0, ['chapter']), 'style' => 'compact'])
</div>
</div>
</div>
<a href="#" permissions-table-toggle-all class="text-small ml-m text-primary">{{ trans('common.toggle_all') }}</a>
</th>
</tr>
- @foreach($roles as $role)
+ @foreach(\BookStack\Auth\Role::restrictable() as $role)
<tr>
<td width="33%" class="pt-m">
{{ $role->display_name }}
<div class="text-right">
<a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.permissions_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.permissions_save') }}</button>
</div>
</form>
\ No newline at end of file
<input type="password" id="{{ $name }}" name="{{ $name }}"
@if($errors->has($name)) class="text-neg" @endif
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
- @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
@if(old($name)) value="{{ old($name)}}" @endif>
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
<input type="text" id="{{ $name }}" name="{{ $name }}"
@if($errors->has($name)) class="text-neg" @endif
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
- @if(isset($disabled) && $disabled) disabled="disabled" @endif
- @if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
+ @if($autofocus ?? false) autofocus @endif
+ @if($disabled ?? false) disabled="disabled" @endif
@if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif>
@if($errors->has($name))
<div class="text-neg text-small">{{ $errors->first($name) }}</div>
-@endif
\ No newline at end of file
+@endif
<div class="tab-container">
<div class="nav-tabs">
- <div @click="tab = 'list'" :class="{selected: tab === 'list'}" class="tab-item">{{ trans('entities.attachments_items') }}</div>
- <div @click="tab = 'file'" :class="{selected: tab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
- <div @click="tab = 'link'" :class="{selected: tab === 'link'}" class="tab-item">{{ trans('entities.attachments_link') }}</div>
+ <button type="button" @click="tab = 'list'" :class="{selected: tab === 'list'}"
+ class="tab-item">{{ trans('entities.attachments_items') }}</button>
+ <button type="button" @click="tab = 'file'" :class="{selected: tab === 'file'}"
+ class="tab-item">{{ trans('entities.attachments_upload') }}</button>
+ <button type="button" @click="tab = 'link'" :class="{selected: tab === 'link'}"
+ class="tab-item">{{ trans('entities.attachments_link') }}</button>
</div>
<div v-show="tab === 'list'">
<draggable style="width: 100%;" :options="{handle: '.handle'}" @change="fileSortUpdate" :list="files" element="div">
<div v-if="file.deleting">
<span class="text-neg small">{{ trans('entities.attachments_delete_confirm') }}</span>
<br>
- <span class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</span>
+ <button type="button" class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</button>
</div>
</div>
- <div @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</div>
- <div @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</div>
+ <button type="button" @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</button>
+ <button type="button" @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</button>
</div>
</draggable>
<p class="small text-muted" v-if="files.length === 0">
<input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" v-model="file.link">
<p class="small text-neg" v-for="error in errors.link.link" v-text="error"></p>
</div>
- <button @click.prevent="attachNewLink(file)" class="button primary">{{ trans('entities.attach') }}</button>
+ <button @click.prevent="attachNewLink(file)" class="button">{{ trans('entities.attach') }}</button>
</div>
</div>
<div class="tab-container">
<div class="nav-tabs">
- <div @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</div>
- <div @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</div>
+ <button type="button" @click="editTab = 'file'" :class="{selected: editTab === 'file'}" class="tab-item">{{ trans('entities.attachments_upload') }}</button>
+ <button type="button" @click="editTab = 'link'" :class="{selected: editTab === 'link'}" class="tab-item">{{ trans('entities.attachments_set_link') }}</button>
</div>
<div v-if="editTab === 'file'">
<dropzone :upload-url="getUploadUrl(fileToEdit)" :uploaded-to="pageId" placeholder="{{ trans('entities.attachments_edit_drop_upload') }}" @success="uploadSuccessUpdate"></dropzone>
</div>
<button type="button" class="button outline" @click="cancelEdit">{{ trans('common.back') }}</button>
- <button @click.enter.prevent="updateFile(fileToEdit)" class="button primary">{{ trans('common.save') }}</button>
+ <button @click.enter.prevent="updateFile(fileToEdit)" class="button">{{ trans('common.save') }}</button>
</div>
</div>
</div>
<div class="form-group" collapsible>
- <div class="collapse-title text-primary" collapsible-trigger>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
<label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
- </div>
+ </button>
<div class="collapse-content" collapsible-content>
@include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
</div>
<div class="form-group text-right">
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.pages_copy') }}</button>
+ <button type="submit" class="button">{{ trans('entities.pages_copy') }}</button>
</div>
</form>
<input type="hidden" name="_method" value="DELETE">
<div class="form-group text-right">
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
</div>
</form>
</div>
@section('body')
<div class="container small pt-xl">
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ $title }}</h1>
<div class="book-contents">
<div class="text-center">
{!! $pages->links() !!}
</div>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
<div editor-toolbox class="floating-toolbox">
<div class="tabs primary-background-light">
- <span toolbox-toggle>@icon('caret-left-circle')</span>
- <span toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</span>
+ <button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
+ <button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
@if(userCan('attachment-create-all'))
- <span toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</span>
+ <button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
@endif
- <span toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</span>
+ <button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
</div>
<div toolbox-tab-content="tags">
<!doctype html>
-<html lang="en">
+<html lang="{{ config('app.lang') }}">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>{{ $page->name }}</title>
<div class="text-center px-m py-xs">
<div v-show="draftsEnabled" dropdown dropdown-move-menu class="dropdown-container draft-display text">
- <a dropdown-toggle class="text-primary text-button"><span class="faded-text" v-text="draftText"></span> @icon('more')</a>
+ <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" title="{{ trans('entities.pages_edit_draft_options') }}" class="text-primary text-button"><span class="faded-text" v-text="draftText"></span> @icon('more')</button>
@icon('check-circle', ['class' => 'text-pos draft-notification svg-icon', ':class' => '{visible: draftUpdated}'])
- <ul class="dropdown-menu">
+ <ul class="dropdown-menu" role="menu">
<li>
- <a @click="saveDraft()" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</a>
+ <button type="button" @click="saveDraft()" class="text-pos">@icon('save'){{ trans('entities.pages_edit_save_draft') }}</button>
</li>
<li v-if="isNewDraft">
<a href="{{ $model->getUrl('/delete') }}" class="text-neg">@icon('delete'){{ trans('entities.pages_edit_delete_draft') }}</a>
</li>
<li v-if="isUpdateDraft">
- <a type="button" @click="discardDraft" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</a>
+ <button type="button" @click="discardDraft" class="text-neg">@icon('cancel'){{ trans('entities.pages_edit_discard_draft') }}</button>
</li>
</ul>
</div>
<div class="action-buttons px-m py-xs" v-cloak>
<div dropdown dropdown-move-menu class="dropdown-container">
- <a dropdown-toggle class="text-primary text-button">@icon('edit') <span v-text="changeSummaryShort"></span></a>
+ <button type="button" dropdown-toggle aria-haspopup="true" aria-expanded="false" class="text-primary text-button">@icon('edit') <span v-text="changeSummaryShort"></span></button>
<ul class="wide dropdown-menu">
<li class="px-l py-m">
<p class="text-muted pb-s">{{ trans('entities.pages_edit_enter_changelog_desc') }}</p>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.pages_new') }}</h1>
<form action="{{ $parent->getUrl('/create-guest-page') }}" method="POST">
{!! csrf_field() !!}
<div class="form-group text-right">
<a href="{{ $parent->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.continue') }}</button>
+ <button type="submit" class="button">{{ trans('common.continue') }}</button>
</div>
</form>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
<div markdown-input class="flex flex-fill">
<textarea id="markdown-editor-input" name="markdown" rows="5"
- @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{htmlspecialchars( old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown))}}@endif</textarea>
+ @if($errors->has('markdown')) class="text-neg" @endif>@if(isset($model) || old('markdown')){{ old('markdown') ? old('markdown') : ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>
</div>
</div>
<div class="editor-toolbar">
<div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
</div>
- <div class="markdown-display page-content">
- </div>
+ <iframe srcdoc="" class="markdown-display" sandbox="allow-same-origin"></iframe>
</div>
<input type="hidden" name="html"/>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.pages_move') }}</h1>
<form action="{{ $page->getUrl('/move') }}" method="POST">
<div class="form-group text-right">
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.pages_move') }}</button>
+ <button type="submit" class="button">{{ trans('entities.pages_move') }}</button>
</div>
</form>
- </div>
+ </main>
</div>
@stop
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.pages_permissions') }}</h1>
@include('form.entity-permissions', ['model' => $page])
- </div>
+ </main>
</div>
@stop
</div>
@if(userCan('page-update', $page))
<a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
- class="button outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
+ class="button primary outline icon heading-edit-icon ml-s px-s" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
@endif
</div>
</div>
\ No newline at end of file
@section('body')
- <div class="mb-m">
+ <div class="mb-m print-hidden">
@include('partials.breadcrumbs', ['crumbs' => [
$page->$book,
$page->chapter,
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<div class="page-content page-revision">
@include('pages.page-display')
</div>
- </div>
+ </main>
@stop
\ No newline at end of file
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.pages_revisions') }}</h1>
@if(count($page->revisions) > 0)
@else
<a href="{{ $revision->getUrl() }}" target="_blank">{{ trans('entities.pages_revisions_preview') }}</a>
<span class="text-muted"> | </span>
- <a href="{{ $revision->getUrl('restore') }}"></a>
<div dropdown class="dropdown-container">
- <a dropdown-toggle>{{ trans('entities.pages_revisions_restore') }}</a>
- <ul class="dropdown-menu">
+ <a dropdown-toggle href="#" aria-haspopup="true" aria-expanded="false">{{ trans('entities.pages_revisions_restore') }}</a>
+ <ul class="dropdown-menu" role="menu">
<li class="px-m py-s"><small class="text-muted">{{trans('entities.revision_restore_confirm')}}</small></li>
<li>
<form action="{{ $revision->getUrl('/restore') }}" method="POST">
</div>
<span class="text-muted"> | </span>
<div dropdown class="dropdown-container">
- <a dropdown-toggle>{{ trans('common.delete') }}</a>
- <ul class="dropdown-menu">
+ <a dropdown-toggle href="#" aria-haspopup="true" aria-expanded="false">{{ trans('common.delete') }}</a>
+ <ul class="dropdown-menu" role="menu">
<li class="px-m py-s"><small class="text-muted">{{trans('entities.revision_delete_confirm')}}</small></li>
<li>
<form action="{{ $revision->getUrl('/delete/') }}" method="POST">
@else
<p>{{ trans('entities.pages_revisions_none') }}</p>
@endif
- </div>
+ </main>
</div>
@section('body')
- <div class="mb-m">
+ <div class="mb-m print-hidden">
@include('partials.breadcrumbs', ['crumbs' => [
$page->book,
$page->hasChapter() ? $page->chapter : null,
]])
</div>
- <div class="content-wrap card">
+ <main class="content-wrap card">
<div class="page-content" page-display="{{ $page->id }}">
@include('pages.pointer', ['page' => $page])
@include('pages.page-display')
</div>
- </div>
+ </main>
@if ($commentsEnabled)
- <div class="container small p-none comments-container mb-l">
+ <div class="container small p-none comments-container mb-l print-hidden">
@include('comments.comments', ['page' => $page])
<div class="clearfix"></div>
</div>
@endif
@if (isset($pageNav) && count($pageNav))
- <div id="page-navigation" class="mb-xl">
+ <nav id="page-navigation" class="mb-xl" aria-label="{{ trans('entities.pages_navigation') }}">
<h5>{{ trans('entities.pages_navigation') }}</h5>
<div class="body">
<div class="sidebar-page-nav menu">
@endforeach
</div>
</div>
- </div>
+ </nav>
@endif
@include('partials.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
<hr class="primary-background"/>
{{--Export--}}
- <div dropdown class="dropdown-container block">
- <div dropdown-toggle class="icon-list-item">
- <span>@icon('export')</span>
- <span>{{ trans('entities.export') }}</span>
- </div>
- <ul class="dropdown-menu wide">
- <li><a href="{{ $page->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
- <li><a href="{{ $page->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
- <li><a href="{{ $page->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
- </ul>
- </div>
+ @include('partials.entity-export-menu', ['entity' => $page])
</div>
</div>
{{ $templates->links() }}
@foreach($templates as $template)
- <div class="card template-item border-card p-m mb-m" draggable="true" template-id="{{ $template->id }}">
+ <div class="card template-item border-card p-m mb-m" tabindex="0"
+ aria-label="{{ trans('entities.templates_replace_content') }} - {{ $template->name }}"
+ draggable="true" template-id="{{ $template->id }}">
<div class="template-item-content" title="{{ trans('entities.templates_replace_content') }}">
<div>{{ $template->name }}</div>
<div class="text-muted">{{ trans('entities.meta_updated', ['timeLength' => $template->updated_at->diffForHumans()]) }}</div>
<div class="template-item-actions">
<button type="button"
title="{{ trans('entities.templates_prepend_content') }}"
+ aria-label="{{ trans('entities.templates_prepend_content') }} - {{ $template->name }}"
template-action="prepend">@icon('chevron-up')</button>
<button type="button"
title="{{ trans('entities.templates_append_content') }}"
+ aria-label="{{ trans('entities.templates_append_content') }} -- {{ $template->name }}"
template-action="append">@icon('chevron-down')</button>
</div>
</div>
])
<textarea id="html-editor" name="html" rows="5" v-pre
- @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{htmlspecialchars( old('html') ? old('html') : $model->html)}}@endif</textarea>
+ @if($errors->has('html')) class="text-neg" @endif>@if(isset($model) || old('html')){{ old('html') ? old('html') : $model->html }}@endif</textarea>
</div>
@if($errors->has('html'))
-<div id="book-tree" class="book-tree mb-xl" v-pre>
+<nav id="book-tree" class="book-tree mb-xl" v-pre aria-label="{{ trans('entities.books_navigation') }}">
<h5>{{ trans('entities.books_navigation') }}</h5>
<ul class="sidebar-page-list mt-xs menu entity-list">
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<div class="entity-list-item no-hover">
- <span class="icon text-chapter">
-
- </span>
+ <span role="presentation" class="icon text-chapter"></span>
<div class="content">
- @include('chapters.child-menu', ['chapter' => $bookChild, 'current' => $current])
+ @include('chapters.child-menu', [
+ 'chapter' => $bookChild,
+ 'current' => $current,
+ 'isOpen' => $bookChild->matchesOrContains($current)
+ ])
</div>
</div>
</li>
@endforeach
</ul>
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
<div class="breadcrumb-listing" dropdown breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
- <div class="breadcrumb-listing-toggle" dropdown-toggle>
+ <div class="breadcrumb-listing-toggle" dropdown-toggle
+ aria-haspopup="true" aria-expanded="false" tabindex="0">
<div class="separator">@icon('chevron-right')</div>
</div>
- <div dropdown-menu class="breadcrumb-listing-dropdown card">
+ <div dropdown-menu class="breadcrumb-listing-dropdown card" role="menu">
<div class="breadcrumb-listing-search">
@icon('search')
- <input autocomplete="off" type="text" name="entity-search">
+ <input autocomplete="off" type="text" name="entity-search" placeholder="{{ trans('common.search') }}" aria-label="{{ trans('common.search') }}">
</div>
@include('partials.loading-icon')
<div class="breadcrumb-listing-entity-list px-m"></div>
-<div class="breadcrumbs text-center">
+<nav class="breadcrumbs text-center" aria-label="{{ trans('common.breadcrumb') }}">
<?php $breadcrumbCount = 0; ?>
{{-- Show top level books item --}}
- @if (count($crumbs) > 0 && array_first($crumbs) instanceof \BookStack\Entities\Book)
+ @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\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 && array_first($crumbs) instanceof \BookStack\Entities\Bookshelf)
+ @if (count($crumbs) > 0 && ($crumbs[0] ?? null) instanceof \BookStack\Entities\Bookshelf)
<a href="{{ url('/shelves') }}" class="text-bookshelf icon-list-item outline-hover">
<span>@icon('bookshelf')</span>
<span>{{ trans('entities.shelves') }}</span>
@endif
<?php $breadcrumbCount++; ?>
@endforeach
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
<style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}">
- .primary-background {
- background-color: {{ setting('app-color') }} !important;
+ :root {
+ --color-primary: {{ setting('app-color') }};
+ --color-primary-light: {{ setting('app-color-light') }};
}
- .primary-background-light {
- background-color: {{ setting('app-color-light') }};
- }
- .button.primary, .button.primary:hover, .button.primary:active, .button.primary:focus {
- background-color: {{ setting('app-color') }};
- border-color: {{ setting('app-color') }};
- }
- .nav-tabs a.selected, .nav-tabs .tab-item.selected {
- border-bottom-color: {{ setting('app-color') }};
- }
- .text-primary, .text-primary-hover:hover, .text-primary:hover {
- color: {{ setting('app-color') }} !important;
- fill: {{ setting('app-color') }} !important;
- }
-
- a, a:hover, a:focus, .text-button, .text-button:hover, .text-button:focus {
- color: {{ setting('app-color') }};
- fill: {{ setting('app-color') }};
- }
-</style>
+</style>
\ No newline at end of file
<div class="mb-xl">
- <form v-on:submit.prevent="searchBook" class="search-box flexible">
- <input v-model="searchTerm" v-on:change="checkSearchForm" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
- <button type="submit">@icon('search')</button>
- <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch" type="button">@icon('close')</button>
+ <form v-on:submit.prevent="searchBook" class="search-box flexible" role="search">
+ <input v-model="searchTerm" v-on:change="checkSearchForm" type="text" aria-label="{{ trans('entities.books_search_this') }}" name="term" placeholder="{{ trans('entities.books_search_this') }}">
+ <button type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
+ <button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch"
+ type="button" aria-label="{{ trans('common.search_clear') }}">@icon('close')</button>
</form>
</div>
\ No newline at end of file
--- /dev/null
+<div dropdown class="dropdown-container" id="export-menu">
+ <div dropdown-toggle class="icon-list-item"
+ aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('entities.export') }}" tabindex="0">
+ <span>@icon('export')</span>
+ <span>{{ trans('entities.export') }}</span>
+ </div>
+ <ul class="wide dropdown-menu" role="menu">
+ <li><a href="{{ $entity->getUrl('/export/html') }}" target="_blank">{{ trans('entities.export_html') }} <span class="text-muted float right">.html</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/pdf') }}" target="_blank">{{ trans('entities.export_pdf') }} <span class="text-muted float right">.pdf</span></a></li>
+ <li><a href="{{ $entity->getUrl('/export/plaintext') }}" target="_blank">{{ trans('entities.export_text') }} <span class="text-muted float right">.txt</span></a></li>
+ </ul>
+</div>
\ No newline at end of file
<?php $type = $entity->getType(); ?>
<a href="{{ $entity->getUrl() }}" class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item" data-entity-type="{{$type}}" data-entity-id="{{$entity->id}}">
- <span class="icon text-{{$type}}">@icon($type)</span>
+ <span role="presentation" class="icon text-{{$type}}">@icon($type)</span>
<div class="content">
<h4 class="entity-list-item-name break-text">{{ $entity->name }}</h4>
{{ $slot ?? '' }}
-<div notification="success" style="display: none;" data-autohide class="pos" @if(session()->has('success')) data-show @endif>
+<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
@icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span>
</div>
-<div notification="warning" style="display: none;" class="warning" @if(session()->has('warning')) data-show @endif>
+<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
@icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span>
</div>
-<div notification="error" style="display: none;" class="neg" @if(session()->has('error')) data-show @endif>
+<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
@icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span>
</div>
<div class="list-sort">
<div class="list-sort-type dropdown-container" dropdown>
- <div dropdown-toggle>{{ $options[$selectedSort] }}</div>
+ <div dropdown-toggle aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
<ul class="dropdown-menu">
@foreach($options as $key => $label)
<li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}">{{ $label }}</a></li>
@endforeach
</ul>
</div>
- <div class="list-sort-dir" data-sort-dir>
+ <button href="#" class="list-sort-dir" type="button" data-sort-dir
+ aria-label="{{ trans('common.sort_direction_toggle') }} - {{ $order === 'asc' ? trans('common.sort_ascending') : trans('common.sort_descending') }}" tabindex="0">
@icon($order === 'desc' ? 'sort-up' : 'sort-down')
- </div>
+ </button>
</div>
</form>
</div>
\ No newline at end of file
</table>
- <button type="submit" class="button primary">{{ trans('entities.search_update') }}</button>
+ <button type="submit" class="button">{{ trans('entities.search_update') }}</button>
</form>
</div>
</div>
<div class="form-group text-right">
- <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+ <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
</div>
</form>
</div>
<p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
</div>
<div setting-app-color-picker class="text-m-right">
- <input type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#0288D1">
+ <input type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
<input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
<br>
<button type="button" class="text-button text-muted mt-s mx-s" setting-app-color-picker-reset>{{ trans('common.reset') }}</button>
</div>
<div class="form-group text-right">
- <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+ <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
</div>
</form>
</div>
</div>
<div class="form-group text-right">
- <button type="submit" class="button primary">{{ trans('settings.settings_save') }}</button>
+ <button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
</div>
</form>
</div>
-<div class="active-link-list">
+<nav class="active-link-list">
@if($currentUser->can('settings-manage'))
<a href="{{ url('/settings') }}" @if($selected == 'settings') class="active" @endif>@icon('settings'){{ trans('settings.settings') }}</a>
<a href="{{ url('/settings/maintenance') }}" @if($selected == 'maintenance') class="active" @endif>@icon('spanner'){{ trans('settings.maint') }}</a>
@if($currentUser->can('user-roles-manage'))
<a href="{{ url('/settings/roles') }}" @if($selected == 'roles') class="active" @endif>@icon('lock-open'){{ trans('settings.roles') }}</a>
@endif
-</div>
\ No newline at end of file
+</nav>
\ No newline at end of file
<div>
<div class="form-group text-right">
<a href="{{ url("/settings/roles/{$role->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
</div>
</div>
</div>
@if (isset($role) && $role->id)
<a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
@endif
- <button type="submit" class="button primary">{{ trans('settings.role_save') }}</button>
+ <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
</div>
</div>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.shelves_create') }}</h1>
<form action="{{ url("/shelves") }}" method="POST" enctype="multipart/form-data">
@include('shelves.form', ['shelf' => null, 'books' => $books])
</form>
- </div>
+ </main>
</div>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.shelves_edit') }}</h1>
<form action="{{ $shelf->getUrl() }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_method" value="PUT">
@include('shelves.form', ['model' => $shelf])
</form>
- </div>
+ </main>
</div>
@stop
\ No newline at end of file
<div class="form-group">
<label for="books">{{ trans('entities.shelves_books') }}</label>
<input type="hidden" id="books-input" name="books"
- value="{{ isset($shelf) ? $shelf->books->implode('id', ',') : '' }}">
+ value="{{ isset($shelf) ? $shelf->visibleBooks->implode('id', ',') : '' }}">
<div class="scroll-box" shelf-sort-assigned-books data-instruction="{{ trans('entities.shelves_drag_books') }}">
- @if (isset($shelfBooks) && count($shelfBooks) > 0)
- @foreach ($shelfBooks as $book)
+ @if (count($shelf->visibleBooks ?? []) > 0)
+ @foreach ($shelf->visibleBooks as $book)
<div data-id="{{ $book->id }}" class="scroll-box-item">
<a href="{{ $book->getUrl() }}" class="text-book">@icon('book'){{ $book->name }}</a>
</div>
<div class="form-group" collapsible id="logo-control">
- <div class="collapse-title text-primary" collapsible-trigger>
- <label for="user-avatar">{{ trans('common.cover_image') }}</label>
- </div>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+ <label>{{ trans('common.cover_image') }}</label>
+ </button>
<div class="collapse-content" collapsible-content>
<p class="small">{{ trans('common.cover_image_description') }}</p>
</div>
<div class="form-group" collapsible id="tags-control">
- <div class="collapse-title text-primary" collapsible-trigger>
+ <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
<label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
- </div>
+ </button>
<div class="collapse-content" collapsible-content>
@include('components.tag-manager', ['entity' => $shelf ?? null, 'entityType' => 'bookshelf'])
</div>
<div class="form-group text-right">
<a href="{{ isset($shelf) ? $shelf->getUrl() : url('/shelves') }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('entities.shelves_save') }}</button>
+ <button type="submit" class="button">{{ trans('entities.shelves_save') }}</button>
</div>
\ No newline at end of file
-<div class="content-wrap mt-m card">
+<main class="content-wrap mt-m card">
<div class="grid half v-center">
<h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
@endif
@endif
-</div>
+</main>
]])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="break-text">{{$shelf->name}}</h1>
<div class="book-content">
<p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
- @if(count($books) > 0)
+ @if(count($shelf->visibleBooks) > 0)
<div class="entity-list">
- @foreach($books as $book)
+ @foreach($shelf->visibleBooks as $book)
@include('books.list-item', ['book' => $book])
@endforeach
</div>
</div>
@endif
</div>
- </div>
+ </main>
@stop
@section('content')
- <div class="tri-layout-mobile-tabs text-primary" >
+ <div class="tri-layout-mobile-tabs text-primary print-hidden">
<div class="grid half no-break no-gap">
<div class="tri-layout-mobile-tab px-m py-s" tri-layout-mobile-tab="info">
{{ trans('common.tab_info') }}
<div class="tri-layout-container" tri-layout @yield('container-attrs') >
<div class="tri-layout-left print-hidden pt-m" id="sidebar">
- <div class="tri-layout-left-contents">
+ <aside class="tri-layout-left-contents">
@yield('left')
- </div>
+ </aside>
</div>
<div class="@yield('body-wrap-classes') tri-layout-middle">
</div>
<div class="tri-layout-right print-hidden pt-m">
- <div class="tri-layout-right-contents">
+ <aside class="tri-layout-right-contents">
@yield('right')
- </div>
+ </aside>
</div>
</div>
@include('settings.navbar', ['selected' => 'users'])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<h1 class="list-heading">{{ trans('settings.users_add_new') }}</h1>
<form action="{{ url("/settings/users/create") }}" method="post">
<div class="form-group text-right">
<a href="{{ url($currentUser->can('users-manage') ? "/settings/users" : "/") }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button class="button primary" type="submit">{{ trans('common.save') }}</button>
+ <button class="button" type="submit">{{ trans('common.save') }}</button>
</div>
</form>
- </div>
+ </main>
</div>
@stop
<input type="hidden" name="_method" value="DELETE">
<a href="{{ url("/settings/users/{$user->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
- <button type="submit" class="button primary">{{ trans('common.confirm') }}</button>
+ <button type="submit" class="button">{{ trans('common.confirm') }}</button>
</form>
</div>
</div>
@include('settings.navbar', ['selected' => 'users'])
</div>
- <div class="card content-wrap">
+ <section class="card content-wrap">
<h1 class="list-heading">{{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}</h1>
<form action="{{ url("/settings/users/{$user->id}") }}" method="post" enctype="multipart/form-data">
{!! csrf_field() !!}
@if($authMethod !== 'system')
<a href="{{ url("/settings/users/{$user->id}/delete") }}" class="button outline">{{ trans('settings.users_delete') }}</a>
@endif
- <button class="button primary" type="submit">{{ trans('common.save') }}</button>
+ <button class="button" type="submit">{{ trans('common.save') }}</button>
</div>
</form>
- </div>
+ </section>
@if($currentUser->id === $user->id && count($activeSocialDrivers) > 0)
- <div class="card content-wrap auto-height">
+ <section class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
<p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
<div class="container">
<div class="grid third">
@foreach($activeSocialDrivers as $driver => $enabled)
<div class="text-center mb-m">
- <div>@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
+ <div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
<div>
@if($user->hasSocialAccount($driver))
- <a href="{{ url("/login/service/{$driver}/detach") }}" class="button small outline">{{ trans('settings.users_social_disconnect') }}</a>
+ <a href="{{ url("/login/service/{$driver}/detach") }}" aria-label="{{ trans('settings.users_social_disconnect') }} - {{ $driver }}"
+ class="button small outline">{{ trans('settings.users_social_disconnect') }}</a>
@else
- <a href="{{ url("/login/service/{$driver}") }}" class="button small outline">{{ trans('settings.users_social_connect') }}</a>
+ <a href="{{ url("/login/service/{$driver}") }}" aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}"
+ class="button small outline">{{ trans('settings.users_social_connect') }}</a>
@endif
</div>
</div>
@endforeach
</div>
</div>
- </div>
+ </section>
@endif
</div>
@endif
@if($authMethod === 'standard')
- <div>
+ <div new-user-password>
<label class="setting-list-label">{{ trans('settings.users_password') }}</label>
- <p class="small">{{ trans('settings.users_password_desc') }}</p>
- @if(isset($model))
+
+ @if(!isset($model))
<p class="small">
- {{ trans('settings.users_password_warning') }}
+ {{ trans('settings.users_send_invite_text') }}
</p>
+
+ @include('components.toggle-switch', [
+ 'name' => 'send_invite',
+ 'value' => old('send_invite', 'true') === 'true',
+ 'label' => trans('settings.users_send_invite_option')
+ ])
+
@endif
- <div class="grid half mt-m gap-xl">
- <div>
- <label for="password">{{ trans('auth.password') }}</label>
- @include('form.password', ['name' => 'password'])
- </div>
- <div>
- <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
- @include('form.password', ['name' => 'password-confirm'])
+
+ <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+ <p class="small">{{ trans('settings.users_password_desc') }}</p>
+ @if(isset($model))
+ <p class="small">
+ {{ trans('settings.users_password_warning') }}
+ </p>
+ @endif
+ <div class="grid half mt-m gap-xl">
+ <div>
+ <label for="password">{{ trans('auth.password') }}</label>
+ @include('form.password', ['name' => 'password'])
+ </div>
+ <div>
+ <label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
+ @include('form.password', ['name' => 'password-confirm'])
+ </div>
</div>
</div>
+
</div>
@endif
\ No newline at end of file
@include('settings.navbar', ['selected' => 'users'])
</div>
- <div class="card content-wrap">
+ <main class="card content-wrap">
<div class="grid right-focus v-center">
<h1 class="list-heading">{{ trans('settings.users') }}</h1>
<div>
{{ $users->links() }}
</div>
- </div>
+ </main>
</div>
<div class="grid right-focus reverse-collapse">
<div>
- <div id="recent-user-activity" class="mb-xl">
+ <section id="recent-user-activity" class="mb-xl">
<h5>{{ trans('entities.recent_activity') }}</h5>
@include('partials.activity-list', ['activity' => $activity])
- </div>
+ </section>
</div>
<div>
- <div class="card content-wrap auto-height">
+ <section class="card content-wrap auto-height">
<div class="grid half v-center">
<div>
<div class="mr-m float left">
</div>
</div>
- </div>
+ </section>
- <div class="card content-wrap auto-height book-contents">
+ <section class="card content-wrap auto-height book-contents">
<h2 id="recent-pages" class="list-heading">
{{ trans('entities.recently_created_pages') }}
@if (count($recentlyCreated['pages']) > 0)
@else
<p class="text-muted">{{ trans('entities.profile_not_created_pages', ['userName' => $user->name]) }}</p>
@endif
- </div>
+ </section>
- <div class="card content-wrap auto-height book-contents">
+ <section class="card content-wrap auto-height book-contents">
<h2 id="recent-chapters" class="list-heading">
{{ trans('entities.recently_created_chapters') }}
@if (count($recentlyCreated['chapters']) > 0)
@else
<p class="text-muted">{{ trans('entities.profile_not_created_chapters', ['userName' => $user->name]) }}</p>
@endif
- </div>
+ </section>
- <div class="card content-wrap auto-height book-contents">
+ <section class="card content-wrap auto-height book-contents">
<h2 id="recent-books" class="list-heading">
{{ trans('entities.recently_created_books') }}
@if (count($recentlyCreated['books']) > 0)
@else
<p class="text-muted">{{ trans('entities.profile_not_created_books', ['userName' => $user->name]) }}</p>
@endif
- </div>
+ </section>
- <div class="card content-wrap auto-height book-contents">
+ <section class="card content-wrap auto-height book-contents">
<h2 id="recent-shelves" class="list-heading">
{{ trans('entities.recently_created_shelves') }}
@if (count($recentlyCreated['shelves']) > 0)
@else
<p class="text-muted">{{ trans('entities.profile_not_created_shelves', ['userName' => $user->name]) }}</p>
@endif
- </div>
+ </section>
</div>
</div>
</div>
-
-
@stop
\ No newline at end of file
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
+<html lang="{{ config('app.lang') }}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
->where('path', '.*$');
- Route::group(['prefix' => 'pages'], function() {
- Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
- });
+ Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
// Shelves
Route::get('/create-shelf', 'BookshelfController@create');
Route::get('/{slug}/edit', 'BookController@edit');
Route::put('/{slug}', 'BookController@update');
Route::delete('/{id}', 'BookController@destroy');
- Route::get('/{slug}/sort-item', 'BookController@getSortItem');
+ Route::get('/{slug}/sort-item', 'BookSortController@showItem');
Route::get('/{slug}', 'BookController@show');
Route::get('/{bookSlug}/permissions', 'BookController@showPermissions');
Route::put('/{bookSlug}/permissions', 'BookController@permissions');
Route::get('/{slug}/delete', 'BookController@showDelete');
- Route::get('/{bookSlug}/sort', 'BookController@sort');
- Route::put('/{bookSlug}/sort', 'BookController@saveSort');
- Route::get('/{bookSlug}/export/html', 'BookController@exportHtml');
- Route::get('/{bookSlug}/export/pdf', 'BookController@exportPdf');
- Route::get('/{bookSlug}/export/plaintext', 'BookController@exportPlainText');
+ Route::get('/{bookSlug}/sort', 'BookSortController@show');
+ Route::put('/{bookSlug}/sort', 'BookSortController@update');
+ Route::get('/{bookSlug}/export/html', 'BookExportController@html');
+ Route::get('/{bookSlug}/export/pdf', 'BookExportController@pdf');
+ Route::get('/{bookSlug}/export/plaintext', 'BookExportController@plainText');
// Pages
Route::get('/{bookSlug}/create-page', 'PageController@create');
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
- Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
- Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
- Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
+ Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageExportController@pdf');
+ Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageExportController@html');
+ Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageExportController@plainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
// Revisions
- Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
- Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
- Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
- Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
- Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageController@destroyRevision');
+ Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageRevisionController@index');
+ Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageRevisionController@show');
+ Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageRevisionController@changes');
+ Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageRevisionController@restore');
+ Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageRevisionController@destroy');
// Chapters
Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showPermissions');
- Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterController@exportPdf');
- Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterController@exportHtml');
- Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterController@exportPlainText');
+ Route::get('/{bookSlug}/chapter/{chapterSlug}/export/pdf', 'ChapterExportController@pdf');
+ Route::get('/{bookSlug}/chapter/{chapterSlug}/export/html', 'ChapterExportController@html');
+ Route::get('/{bookSlug}/chapter/{chapterSlug}/export/plaintext', 'ChapterExportController@plainText');
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@permissions');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
Route::post('/login', 'Auth\LoginController@login');
Route::get('/logout', 'Auth\LoginController@logout');
Route::get('/register', 'Auth\RegisterController@getRegister');
-Route::get('/register/confirm', 'Auth\RegisterController@getRegisterConfirmation');
-Route::get('/register/confirm/awaiting', 'Auth\RegisterController@showAwaitingConfirmation');
-Route::post('/register/confirm/resend', 'Auth\RegisterController@resendConfirmation');
-Route::get('/register/confirm/{token}', 'Auth\RegisterController@confirmEmail');
+Route::get('/register/confirm', 'Auth\ConfirmEmailController@show');
+Route::get('/register/confirm/awaiting', 'Auth\ConfirmEmailController@showAwaiting');
+Route::post('/register/confirm/resend', 'Auth\ConfirmEmailController@resend');
+Route::get('/register/confirm/{token}', 'Auth\ConfirmEmailController@confirm');
Route::post('/register', 'Auth\RegisterController@postRegister');
+// User invitation routes
+Route::get('/register/invite/{token}', 'Auth\UserInviteController@showSetPassword');
+Route::post('/register/invite/{token}', 'Auth\UserInviteController@setPassword');
+
// Password reset link request routes...
Route::get('/password/email', 'Auth\ForgotPasswordController@showLinkRequestForm');
Route::post('/password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
*
+!data/
!.gitignore
\ No newline at end of file
<?php namespace Tests;
+use BookStack\Auth\User;
use BookStack\Entities\Page;
use BookStack\Notifications\ConfirmEmail;
-use BookStack\Auth\User;
use BookStack\Settings\SettingService;
use Illuminate\Support\Facades\Notification;
->press('Create Account')
->see('The name must be at least 2 characters.')
->see('The email must be a valid email address.')
- ->see('The password must be at least 6 characters.')
+ ->see('The password must be at least 8 characters.')
->seePageIs('/register');
}
public function test_user_cannot_be_deleted_if_last_admin()
{
$adminRole = \BookStack\Auth\Role::getRole('admin');
+
+ // Delete all but one admin user if there are more than one
+ $adminUsers = $adminRole->users;
+ if (count($adminUsers) > 1) {
+ foreach ($adminUsers->splice(1) as $user) {
+ $user->delete();
+ }
+ }
+
// Ensure we currently only have 1 admin user
$this->assertEquals(1, $adminRole->users()->count());
$user = $adminRole->users->first();
protected $mockUser;
protected $resourceId = 'resource-test';
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
config()->set('services.google.select_account', 'true');
$resp = $this->get('/login/service/google');
- $this->assertContains('prompt=select_account', $resp->headers->get('Location'));
+ $this->assertStringContainsString('prompt=select_account', $resp->headers->get('Location'));
}
}
--- /dev/null
+<?php namespace Tests;
+
+
+use BookStack\Auth\Access\UserInviteService;
+use BookStack\Auth\User;
+use BookStack\Notifications\UserInvite;
+use Carbon\Carbon;
+use DB;
+use Illuminate\Support\Str;
+use Notification;
+
+class UserInviteTest extends TestCase
+{
+
+ public function test_user_creation_creates_invite()
+ {
+ Notification::fake();
+ $admin = $this->getAdmin();
+
+ $this->actingAs($admin)->post('/settings/users/create', [
+ 'name' => 'Barry',
+ 'send_invite' => 'true',
+ ]);
+
+ $newUser = User::query()->where('email', '=', '
[email protected]')->orderBy('id', 'desc')->first();
+
+ Notification::assertSentTo($newUser, UserInvite::class);
+ $this->assertDatabaseHas('user_invites', [
+ 'user_id' => $newUser->id
+ ]);
+ }
+
+ public function test_invite_set_password()
+ {
+ Notification::fake();
+ $user = $this->getViewer();
+ $inviteService = app(UserInviteService::class);
+
+ $inviteService->sendInvitation($user);
+ $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;
+
+ $setPasswordPageResp = $this->get('/register/invite/' . $token);
+ $setPasswordPageResp->assertSuccessful();
+ $setPasswordPageResp->assertSee('Welcome to BookStack!');
+ $setPasswordPageResp->assertSee('Password');
+ $setPasswordPageResp->assertSee('Confirm Password');
+
+ $setPasswordResp = $this->followingRedirects()->post('/register/invite/' . $token, [
+ 'password' => 'my test password',
+ ]);
+ $setPasswordResp->assertSee('Password set, you now have access to BookStack!');
+ $newPasswordValid = auth()->validate([
+ 'email' => $user->email,
+ 'password' => 'my test password'
+ ]);
+ $this->assertTrue($newPasswordValid);
+ $this->assertDatabaseMissing('user_invites', [
+ 'user_id' => $user->id
+ ]);
+ }
+
+ public function test_invite_set_has_password_validation()
+ {
+ Notification::fake();
+ $user = $this->getViewer();
+ $inviteService = app(UserInviteService::class);
+
+ $inviteService->sendInvitation($user);
+ $token = DB::table('user_invites')->where('user_id', '=', $user->id)->first()->token;
+
+ $this->get('/register/invite/' . $token);
+ $shortPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
+ 'password' => 'mypassw',
+ ]);
+ $shortPassword->assertSee('The password must be at least 8 characters.');
+
+ $this->get('/register/invite/' . $token);
+ $noPassword = $this->followingRedirects()->post('/register/invite/' . $token, [
+ 'password' => '',
+ ]);
+ $noPassword->assertSee('The password field is required.');
+
+ $this->assertDatabaseHas('user_invites', [
+ 'user_id' => $user->id
+ ]);
+ }
+
+ public function test_non_existent_invite_token_redirects_to_home()
+ {
+ $setPasswordPageResp = $this->get('/register/invite/' . Str::random(12));
+ $setPasswordPageResp->assertRedirect('/');
+
+ $setPasswordResp = $this->post('/register/invite/' . Str::random(12), ['password' => 'Password Test']);
+ $setPasswordResp->assertRedirect('/');
+ }
+
+ public function test_token_expires_after_two_weeks()
+ {
+ Notification::fake();
+ $user = $this->getViewer();
+ $inviteService = app(UserInviteService::class);
+
+ $inviteService->sendInvitation($user);
+ $tokenEntry = DB::table('user_invites')->where('user_id', '=', $user->id)->first();
+ DB::table('user_invites')->update(['created_at' => Carbon::now()->subDays(14)->subHour(1)]);
+
+ $setPasswordPageResp = $this->get('/register/invite/' . $tokenEntry->token);
+ $setPasswordPageResp->assertRedirect('/password/email');
+ $setPasswordPageResp->assertSessionHas('error', 'This invitation link has expired. You can instead try to reset your account password.');
+ }
+
+
+}
\ No newline at end of file
*/
protected $baseUrl = 'http://localhost';
- public function tearDown()
+ public function tearDown() : void
{
\DB::disconnect();
parent::tearDown();
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
use BookStack\Auth\User;
use BookStack\Entities\Repos\PageRepo;
$this->asEditor();
$pageRepo = app(PageRepo::class);
$page = Page::first();
- $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+ $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
$pageRepo->updatePageDraft($page, ['name' => 'updated page', 'html' => '<p>new content in draft</p>', 'summary' => 'page revision testing']);
$this->assertDatabaseHas('page_revisions', [
use BookStack\Auth\User;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
+use Illuminate\Support\Str;
class BookShelfTest extends TestCase
{
{
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . str_random(4),
- 'description' => 'Test book description ' . str_random(10)
+ 'name' => 'My test book' . Str::random(4),
+ 'description' => 'Test book description ' . Str::random(10)
];
$resp = $this->asEditor()->post('/shelves', array_merge($shelfInfo, [
'books' => $booksToInclude->implode('id', ','),
$booksToInclude = Book::take(2)->get();
$shelfInfo = [
- 'name' => 'My test book' . str_random(4),
- 'description' => 'Test book description ' . str_random(10)
+ 'name' => 'My test book' . Str::random(4),
+ 'description' => 'Test book description ' . Str::random(10)
];
$resp = $this->asEditor()->put($shelf->getUrl(), array_merge($shelfInfo, [
class CommentSettingTest extends BrowserKitTest {
protected $page;
- public function setUp() {
+ public function setUp(): void {
parent::setUp();
$this->page = \BookStack\Entities\Page::first();
}
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Repos\PageRepo;
use Carbon\Carbon;
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($creator);
app(UserRepo::class)->destroy($creator);
- app(PageRepo::class)->savePageRevision($entities['page']);
+ app(PageRepo::class)->update($entities['page'], ['html' => '<p>hello!</p>>']);
$this->checkEntitiesViewable($entities);
}
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($updater);
app(UserRepo::class)->destroy($updater);
- app(PageRepo::class)->savePageRevision($entities['page']);
+ app(PageRepo::class)->update($entities['page'], ['html' => '<p>Hello there!</p>']);
$this->checkEntitiesViewable($entities);
}
public function test_slug_multi_byte_lower_casing()
{
- $entityRepo = app(EntityRepo::class);
- $book = $entityRepo->createFromInput('book', [
+ $book = $this->newBook([
'name' => 'КНИГА'
]);
public function test_slug_format()
{
- $entityRepo = app(EntityRepo::class);
- $book = $entityRepo->createFromInput('book', [
+ $book = $this->newBook([
'name' => 'PartA / PartB / PartC'
]);
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Str;
class ExportTest extends TestCase
{
public function test_book_html_export_shows_chapter_descriptions()
{
- $chapterDesc = 'My custom test chapter description ' . str_random(12);
+ $chapterDesc = 'My custom test chapter description ' . Str::random(12);
$chapter = Chapter::query()->first();
$chapter->description = $chapterDesc;
$chapter->save();
{
protected $page;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->page = \BookStack\Entities\Page::first();
<?php namespace Tests;
+use BookStack\Entities\Managers\PageContent;
use BookStack\Entities\Page;
-use BookStack\Entities\Repos\EntityRepo;
-use BookStack\Entities\Repos\PageRepo;
class PageContentTest extends TestCase
{
$resp->assertStatus(302);
$page = Page::find($page->id);
- $this->assertContains($includeTag, $page->html);
+ $this->assertStringContainsString($includeTag, $page->html);
$this->assertEquals('', $page->text);
}
'<iframe SRC=" javascript: alert(document.cookie)"></iframe>',
'<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<iframe src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
-
+ '<iframe srcdoc="<script>window.alert(document.cookie)</script>"></iframe>'
];
$this->asEditor();
$updatedPage = Page::where('id', '=', $page->id)->first();
$this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
}
+
+ public function test_get_page_nav_sets_correct_properties()
+ {
+ $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
+ $pageContent = new PageContent(new Page(['html' => $content]));
+ $navMap = $pageContent->getNavigation($content);
+
+ $this->assertCount(3, $navMap);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h1',
+ 'link' => '#testa',
+ 'text' => 'Hello',
+ 'level' => 1,
+ ], $navMap[0]);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h2',
+ 'link' => '#testb',
+ 'text' => 'There',
+ 'level' => 2,
+ ], $navMap[1]);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h3',
+ 'link' => '#testc',
+ 'text' => 'Donkey',
+ 'level' => 3,
+ ], $navMap[2]);
+ }
+
+ public function test_get_page_nav_does_not_show_empty_titles()
+ {
+ $content = '<h1 id="testa">Hello</h1><h2 id="testb"> </h2><h3 id="testc"></h3>';
+ $pageContent = new PageContent(new Page(['html' => $content]));
+ $navMap = $pageContent->getNavigation($content);
+
+ $this->assertCount(1, $navMap);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h1',
+ 'link' => '#testa',
+ 'text' => 'Hello'
+ ], $navMap[0]);
+ }
+
+ public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
+ {
+ $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
+ $pageContent = new PageContent(new Page(['html' => $content]));
+ $navMap = $pageContent->getNavigation($content);
+
+ $this->assertCount(3, $navMap);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h4',
+ 'level' => 1,
+ ], $navMap[0]);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h5',
+ 'level' => 2,
+ ], $navMap[1]);
+ $this->assertArrayMapIncludes([
+ 'nodeName' => 'h6',
+ 'level' => 3,
+ ], $navMap[2]);
+ }
}
<?php namespace Tests;
-
use BookStack\Entities\Repos\PageRepo;
class PageDraftTest extends BrowserKitTest
{
protected $page;
+
+ /**
+ * @var PageRepo
+ */
protected $pageRepo;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->page = \BookStack\Entities\Page::first();
public function test_draft_content_shows_if_available()
{
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->seeInField('html', $newContent);
}
public function test_draft_not_visible_by_others()
{
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$newUser = $this->getEditor();
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
- $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
+
+ $this->actingAs($newUser)->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $newContent);
}
{
$this->asAdmin();
$this->pageRepo->updatePageDraft($this->page, ['html' => 'test content']);
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->see('You are currently editing a draft');
}
{
$nonEditedPage = \BookStack\Entities\Page::take(10)->get()->last();
$addedContent = '<p>test message content</p>';
- $this->asAdmin()->visit($this->page->getUrl() . '/edit')
+ $this->asAdmin()->visit($this->page->getUrl('/edit'))
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
$this->pageRepo->updatePageDraft($this->page, ['html' => $newContent]);
$this->actingAs($newUser)
- ->visit($this->page->getUrl() . '/edit')
+ ->visit($this->page->getUrl('/edit'))
->see('Admin has started editing this page');
$this->flushSession();
$this->visit($nonEditedPage->getUrl() . '/edit')
$newUser = $this->getEditor();
$this->actingAs($newUser)->visit('/')
- ->visit($book->getUrl() . '/create-page')
- ->visit($chapter->getUrl() . '/create-page')
+ ->visit($book->getUrl('/create-page'))
+ ->visit($chapter->getUrl('/create-page'))
->visit($book->getUrl())
->seeInElement('.book-contents', 'New Page');
-
+
$this->asAdmin()
->visit($book->getUrl())
->dontSeeInElement('.book-contents', 'New Page')
$pageRepo = app(PageRepo::class);
$page = Page::first();
- $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+ $pageRepo->update($page, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
$pageRevision = $page->revisions->last();
$revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
$pageRepo = app(PageRepo::class);
$page = Page::first();
- $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
- $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+ $pageRepo->update($page, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
+ $pageRepo->update($page, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
$page = Page::find($page->id);
// Delete the first revision
$revision = $page->revisions->get(1);
$resp = $this->asEditor()->delete($revision->getUrl('/delete/'));
- $resp->assertStatus(200);
+ $resp->assertRedirect($page->getUrl('/revisions'));
$page = Page::find($page->id);
$afterRevisionCount = $page->revisions->count();
$beforeRevisionCount = $page->revisions->count();
$currentRevision = $page->getCurrentRevision();
$resp = $this->asEditor()->delete($currentRevision->getUrl('/delete/'));
- $resp->assertStatus(400);
+ $resp->assertRedirect($page->getUrl('/revisions'));
$page = Page::find($page->id);
$afterRevisionCount = $page->revisions->count();
<?php namespace Tests;
-use BookStack\Auth\Role;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
{
protected $book;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->book = Book::first();
{
$this->asAdmin();
$pageRepo = app(PageRepo::class);
- $draft = $pageRepo->getDraftPage($this->book);
+ $draft = $pageRepo->getNewDraftPage($this->book);
$resp = $this->get($this->book->getUrl());
$resp->assertSee($draft->name);
'entity_selection' => 'book:' . $newBook->id,
'name' => 'My copied test page'
]);
-
$pageCopy = Page::where('name', '=', 'My copied test page')->first();
$movePageResp->assertRedirect($pageCopy->getUrl());
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Actions\Tag;
+use BookStack\Entities\Entity;
use BookStack\Entities\Page;
use BookStack\Auth\Permissions\PermissionService;
/**
* Get an instance of a page that has many tags.
* @param \BookStack\Actions\Tag[]|bool $tags
- * @return mixed
+ * @return Entity
*/
- protected function getEntityWithTags($class, $tags = false)
+ protected function getEntityWithTags($class, $tags = false): Entity
{
$entity = $class::first();
// Set restricted permission the page
$page->restricted = true;
$page->save();
- $permissionService->buildJointPermissionsForEntity($page);
+ $page->rebuildPermissions();
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]);
/**
* LanguageTest constructor.
*/
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->langs = array_diff(scandir(resource_path('lang')), ['..', '.', 'check.php', 'format.php']);
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Auth\User;
-use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\Page;
class RestrictionsTest extends BrowserKitTest
{
/**
- * @var \BookStack\Auth\User
+ * @var User
*/
protected $user;
*/
protected $viewer;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->user = $this->getEditor();
public function test_page_view_restriction()
{
- $page = \BookStack\Entities\Page::first();
+ $page = Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
public function test_page_delete_restriction()
{
- $page = \BookStack\Entities\Page::first();
+ $page = Page::first();
$pageUrl = $page->getUrl();
$this->actingAs($this->user)
public function test_page_restriction_form()
{
- $page = \BookStack\Entities\Page::first();
+ $page = Page::first();
$this->asAdmin()->visit($page->getUrl() . '/permissions')
->see('Page Permissions')
->check('restricted')
$this->setEntityRestrictions($firstBook, ['view', 'update']);
$this->setEntityRestrictions($secondBook, ['view']);
- $firstBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
- ['name' => 'first book chapter'], $firstBook);
- $secondBookChapter = $this->app[EntityRepo::class]->createFromInput('chapter',
- ['name' => 'second book chapter'], $secondBook);
+ $firstBookChapter = $this->newChapter(['name' => 'first book chapter'], $firstBook);
+ $secondBookChapter = $this->newChapter(['name' => 'second book chapter'], $secondBook);
// Create request data
$reqData = [
{
protected $user;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->user = $this->getViewer();
<?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\Repos\EntityRepo;
+use BookStack\Entities\Repos\BookRepo;
+use BookStack\Entities\Repos\BookshelfRepo;
+use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Role;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
+use Illuminate\Support\Env;
+use Mockery;
+use Throwable;
trait SharedTestHelpers
{
*/
protected function getViewer($attributes = [])
{
- $user = \BookStack\Auth\Role::getRole('viewer')->users()->first();
+ $user = Role::getRole('viewer')->users()->first();
if (!empty($attributes)) $user->forceFill($attributes)->save();
return $user;
}
/**
* Regenerate the permission for an entity.
* @param Entity $entity
+ * @throws Throwable
*/
protected function regenEntityPermissions(Entity $entity)
{
- app(PermissionService::class)->buildJointPermissionsForEntity($entity);
+ $entity->rebuildPermissions();
$entity->load('jointPermissions');
}
/**
* Create and return a new bookshelf.
* @param array $input
- * @return \BookStack\Entities\Bookshelf
+ * @return Bookshelf
*/
public function newShelf($input = ['name' => 'test shelf', 'description' => 'My new test shelf']) {
- return app(EntityRepo::class)->createFromInput('bookshelf', $input, false);
+ return app(BookshelfRepo::class)->create($input, []);
}
/**
* @return Book
*/
public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
- return app(EntityRepo::class)->createFromInput('book', $input, false);
+ return app(BookRepo::class)->create($input);
}
/**
* Create and return a new test chapter
* @param array $input
* @param Book $book
- * @return \BookStack\Entities\Chapter
+ * @return Chapter
*/
public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
- return app(EntityRepo::class)->createFromInput('chapter', $input, $book);
+ return app(ChapterRepo::class)->create($input, $book);
}
/**
* Create and return a new test page
* @param array $input
* @return Page
+ * @throws Throwable
*/
public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
$book = Book::first();
$pageRepo = app(PageRepo::class);
- $draftPage = $pageRepo->getDraftPage($book);
- return $pageRepo->publishPageDraft($draftPage, $input);
+ $draftPage = $pageRepo->getNewDraftPage($book);
+ return $pageRepo->publishDraft($draftPage, $input);
}
/**
/**
* Give the given user some permissions.
- * @param \BookStack\Auth\User $user
+ * @param User $user
* @param array $permissions
*/
- protected function giveUserPermissions(\BookStack\Auth\User $user, $permissions = [])
+ protected function giveUserPermissions(User $user, $permissions = [])
{
$newRole = $this->createNewRole($permissions);
$user->attachRole($newRole);
*/
protected function mockHttpFetch($returnData, int $times = 1)
{
- $mockHttp = \Mockery::mock(HttpFetcher::class);
+ $mockHttp = Mockery::mock(HttpFetcher::class);
$this->app[HttpFetcher::class] = $mockHttp;
$mockHttp->shouldReceive('fetch')
->times($times)
->andReturn($returnData);
}
+ /**
+ * Run a set test with the given env variable.
+ * Remembers the original and resets the value after test.
+ * @param string $name
+ * @param $value
+ * @param callable $callback
+ */
+ protected function runWithEnv(string $name, $value, callable $callback)
+ {
+ Env::disablePutenv();
+ $originalVal = $_SERVER[$name] ?? null;
+
+ if (is_null($value)) {
+ unset($_SERVER[$name]);
+ } else {
+ $_SERVER[$name] = $value;
+ }
+
+ $this->refreshApplication();
+ $callback();
+
+ if (is_null($originalVal)) {
+ unset($_SERVER[$name]);
+ } else {
+ $_SERVER[$name] = $originalVal;
+ }
+ }
+
+ /**
+ * Check the keys and properties in the given map to include
+ * exist, albeit not exclusively, within the map to check.
+ * @param array $mapToInclude
+ * @param array $mapToCheck
+ * @param string $message
+ */
+ protected function assertArrayMapIncludes(array $mapToInclude, array $mapToCheck, string $message = '') : void
+ {
+ $passed = true;
+
+ foreach ($mapToInclude as $key => $value) {
+ if (!isset($mapToCheck[$key]) || $mapToCheck[$key] !== $mapToInclude[$key]) {
+ $passed = false;
+ }
+ }
+
+ $toIncludeStr = print_r($mapToInclude, true);
+ $toCheckStr = print_r($mapToCheck, true);
+ self::assertThat($passed, self::isTrue(), "Failed asserting that given map:\n\n{$toCheckStr}\n\nincludes:\n\n{$toIncludeStr}");
+ }
+
}
\ No newline at end of file
public function test_filesystem_images_falls_back_to_storage_type_var()
{
- putenv('STORAGE_TYPE=local_secure');
-
- $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
- $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
-
- putenv('STORAGE_TYPE=local');
+ $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+ $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', 's3', 'filesystems.images', 's3');
+ $this->checkEnvConfigResult('STORAGE_IMAGE_TYPE', null, 'filesystems.images', 'local_secure');
+ });
}
public function test_filesystem_attachments_falls_back_to_storage_type_var()
{
- putenv('STORAGE_TYPE=local_secure');
-
- $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
- $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
-
- putenv('STORAGE_TYPE=local');
+ $this->runWithEnv('STORAGE_TYPE', 'local_secure', function() {
+ $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', 's3', 'filesystems.attachments', 's3');
+ $this->checkEnvConfigResult('STORAGE_ATTACHMENT_TYPE', null, 'filesystems.attachments', 'local_secure');
+ });
}
public function test_app_url_blank_if_old_default_value()
*/
protected function checkEnvConfigResult(string $envName, $envVal, string $configKey, string $expectedResult)
{
- $originalVal = getenv($envName);
- $envString = $envName . (is_null($envVal) ? '' : '=') . ($envVal ?? '');
- putenv($envString);
- $this->refreshApplication();
- $this->assertEquals($expectedResult, config($configKey));
- putenv($envString = $envName . (empty($originalVal) ? '' : '=') . ($originalVal ?? ''));
+ $this->runWithEnv($envName, $envVal, function() use ($configKey, $expectedResult) {
+ $this->assertEquals($expectedResult, config($configKey));
+ });
}
}
\ No newline at end of file
+++ /dev/null
-<?php
-namespace Tests;
-
-use BookStack\Entities\Repos\PageRepo;
-
-class PageRepoTest extends TestCase
-{
- /**
- * @var PageRepo $pageRepo
- */
- protected $pageRepo;
-
- protected function setUp()
- {
- parent::setUp();
- $this->pageRepo = app()->make(PageRepo::class);
- }
-
- public function test_get_page_nav_sets_correct_properties()
- {
- $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
- $navMap = $this->pageRepo->getPageNav($content);
-
- $this->assertCount(3, $navMap);
- $this->assertArraySubset([
- 'nodeName' => 'h1',
- 'link' => '#testa',
- 'text' => 'Hello',
- 'level' => 1,
- ], $navMap[0]);
- $this->assertArraySubset([
- 'nodeName' => 'h2',
- 'link' => '#testb',
- 'text' => 'There',
- 'level' => 2,
- ], $navMap[1]);
- $this->assertArraySubset([
- 'nodeName' => 'h3',
- 'link' => '#testc',
- 'text' => 'Donkey',
- 'level' => 3,
- ], $navMap[2]);
- }
-
- public function test_get_page_nav_does_not_show_empty_titles()
- {
- $content = '<h1 id="testa">Hello</h1><h2 id="testb"> </h2><h3 id="testc"></h3>';
- $navMap = $this->pageRepo->getPageNav($content);
-
- $this->assertCount(1, $navMap);
- $this->assertArraySubset([
- 'nodeName' => 'h1',
- 'link' => '#testa',
- 'text' => 'Hello'
- ], $navMap[0]);
- }
-
- public function test_get_page_nav_shifts_headers_if_only_smaller_ones_are_used()
- {
- $content = '<h4 id="testa">Hello</h4><h5 id="testb">There</h5><h6 id="testc">Donkey</h6>';
- $navMap = $this->pageRepo->getPageNav($content);
-
- $this->assertCount(3, $navMap);
- $this->assertArraySubset([
- 'nodeName' => 'h4',
- 'level' => 1,
- ], $navMap[0]);
- $this->assertArraySubset([
- 'nodeName' => 'h5',
- 'level' => 2,
- ], $navMap[1]);
- $this->assertArraySubset([
- 'nodeName' => 'h6',
- 'level' => 3,
- ], $navMap[2]);
- }
-
-}
\ No newline at end of file
public function test_url_helper_takes_custom_url_into_account()
{
- putenv('APP_URL=http://example.com/bookstack');
- $this->refreshApplication();
- $this->assertEquals('http://example.com/bookstack/books', url('/books'));
- putenv('APP_URL=');
+ $this->runWithEnv('APP_URL', 'http://example.com/bookstack', function() {
+ $this->assertEquals('http://example.com/bookstack/books', url('/books'));
+ });
+ }
+
+ public function test_url_helper_sets_correct_scheme_even_when_request_scheme_is_different()
+ {
+ $this->runWithEnv('APP_URL', 'https://example.com/', function() {
+ $this->get('http://example.com/login')->assertSee('https://example.com/dist/styles.css');
+ });
}
}
\ No newline at end of file
$upload->assertStatus(200);
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
- $this->assertNotContains($fileName, $attachment->path);
+ $this->assertStringNotContainsString($fileName, $attachment->path);
$this->assertStringEndsWith('.txt', $attachment->path);
}
{
$admin = $this->getAdmin();
$viewer = $this->getViewer();
- $page = Page::first();
+ $page = Page::first(); /** @var Page $page */
$this->actingAs($admin);
$fileName = 'permission_test.txt';
$page->restricted = true;
$page->permissions()->delete();
$page->save();
- $this->app[PermissionService::class]->buildJointPermissionsForEntity($page);
+ $page->rebuildPermissions();
$page->load('jointPermissions');
$this->actingAs($viewer);
use BookStack\Uploads\Image;
use BookStack\Entities\Page;
use BookStack\Uploads\ImageService;
+use Illuminate\Support\Str;
use Tests\TestCase;
class ImageTest extends TestCase
$imgDetails = $this->uploadGalleryImage();
$image = Image::query()->first();
- $newName = str_random();
+ $newName = Str::random();
$update = $this->put('/images/' . $image->id, ['name' => $newName]);
$update->assertSuccessful();
$update->assertJson([
$searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
$searchHitRequest->assertSuccessful()->assertJson($resultJson);
- $namePartial = str_random(16);
+ $namePartial = Str::random(16);
$searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}");
$searchHitRequest->assertSuccessful()->assertExactJson($emptyJson);
}
$encodedImageContent = base64_encode(file_get_contents($expectedPath));
$export = $this->get($page->getUrl('/export/html'));
- $this->assertTrue(str_contains($export->getContent(), $encodedImageContent), 'Uploaded image in export content');
+ $this->assertTrue(strpos($export->getContent(), $encodedImageContent) !== false, 'Uploaded image in export content');
if (file_exists($expectedPath)) {
unlink($expectedPath);
$image = Image::where('type', '=', 'gallery')->first();
$pageRepo = app(PageRepo::class);
- $pageRepo->updatePage($page, $page->book_id, [
+ $pageRepo->update($page, [
'name' => $page->name,
'html' => $page->html . "<img src=\"{$image->url}\">",
'summary' => ''
$this->assertCount(0, $toDelete);
// Save a revision of our page without the image;
- $pageRepo->updatePage($page, $page->book_id, [
+ $pageRepo->update($page, [
'name' => $page->name,
'html' => "<p>Hello</p>",
'summary' => ''
{
protected $user;
- public function setUp()
+ public function setUp(): void
{
parent::setUp();
$this->user = \BookStack\Auth\User::all()->last();
target: 'web',
mode: dev? 'development' : 'production',
entry: {
- app: './resources/assets/js/index.js',
- styles: './resources/assets/sass/styles.scss',
- "export-styles": './resources/assets/sass/export-styles.scss',
- "print-styles": './resources/assets/sass/print-styles.scss',
+ app: './resources/js/index.js',
+ styles: './resources/sass/styles.scss',
+ "export-styles": './resources/sass/export-styles.scss',
+ "print-styles": './resources/sass/print-styles.scss',
},
output: {
filename: '[name].js',