mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Merge branch 'master' into docker-tests
This commit is contained in:
commit
371033a0f2
@ -195,6 +195,7 @@ LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
LDAP_START_TLS=false
|
||||
LDAP_TLS_INSECURE=false
|
||||
LDAP_ID_ATTRIBUTE=uid
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
@ -245,10 +246,15 @@ AVATAR_URL=
|
||||
DRAWIO=true
|
||||
|
||||
# Default item listing view
|
||||
# Used for public visitors and user's without a preference
|
||||
# Can be 'list' or 'grid'
|
||||
# Used for public visitors and user's without a preference.
|
||||
# Can be 'list' or 'grid'.
|
||||
APP_VIEWS_BOOKS=list
|
||||
APP_VIEWS_BOOKSHELVES=grid
|
||||
APP_VIEWS_BOOKSHELF=grid
|
||||
|
||||
# Use dark mode by default
|
||||
# Will be overriden by any user/session preference.
|
||||
APP_DEFAULT_DARK_MODE=false
|
||||
|
||||
# Page revision limit
|
||||
# Number of page revisions to keep in the system before deleting old revisions.
|
||||
|
13
.github/workflows/phpunit.yml
vendored
13
.github/workflows/phpunit.yml
vendored
@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
- gh_actions_update
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@ -13,13 +14,19 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@ -38,7 +45,7 @@ jobs:
|
||||
- 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 "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
13
.github/workflows/test-migrations.yml
vendored
13
.github/workflows/test-migrations.yml
vendored
@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
- gh_actions_update
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
@ -13,13 +14,19 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@ -38,7 +45,7 @@ jobs:
|
||||
- name: Create database & user
|
||||
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 "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
@ -6,6 +6,7 @@ use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@ -23,7 +24,7 @@ class Activity extends Model
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
if ($this->entity_type === '') {
|
||||
$this->entity_type = null;
|
||||
|
@ -78,7 +78,7 @@ class ActivityService
|
||||
public function latest(int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['user', 'entity'])
|
||||
->skip($count * $page)
|
||||
@ -131,7 +131,7 @@ class ActivityService
|
||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->where('user_id', '=', $user->id)
|
||||
->skip($count * $page)
|
||||
|
@ -26,7 +26,9 @@ class TagRepo
|
||||
*/
|
||||
public function getNameSuggestions(?string $searchTerm): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
@ -45,7 +47,9 @@ class TagRepo
|
||||
*/
|
||||
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
|
@ -65,7 +65,7 @@ class ViewService
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
@ -142,5 +142,4 @@ class ApiDocsGenerator
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -299,5 +299,4 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,14 +5,12 @@ namespace BookStack\Auth\Access\Guards;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
@ -23,13 +21,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
/**
|
||||
* LdapSessionGuard constructor.
|
||||
*/
|
||||
public function __construct($name,
|
||||
public function __construct(
|
||||
$name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
LdapService $ldapService,
|
||||
RegistrationService $registrationService
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->ldapService = $ldapService;
|
||||
parent::__construct($name, $provider, $session, $registrationService);
|
||||
}
|
||||
@ -119,5 +117,4 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
|
||||
return $this->registrationService->registerUser($details, null, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,5 +34,4 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,6 +31,14 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start TLS on the given LDAP connection.
|
||||
*/
|
||||
public function startTls($ldapConnection): bool
|
||||
{
|
||||
return ldap_start_tls($ldapConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
* @param $ldapConnection
|
||||
|
@ -187,8 +187,8 @@ class LdapService extends ExternalAuthService
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
||||
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
|
||||
// Disable certificate verification.
|
||||
// This option works globally and must be set before a connection is created.
|
||||
if ($this->config['tls_insecure']) {
|
||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||
}
|
||||
@ -205,6 +205,14 @@ class LdapService extends ExternalAuthService
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
// Start and verify TLS if it's enabled
|
||||
if ($this->config['start_tls']) {
|
||||
$started = $this->ldap->startTls($ldapConnection);
|
||||
if (!$started) {
|
||||
throw new LdapException('Could not start TLS connection');
|
||||
}
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
|
||||
class RegistrationService
|
||||
@ -71,6 +73,7 @@ class RegistrationService
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
|
||||
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser);
|
||||
|
||||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
@ -83,7 +86,6 @@ class RegistrationService
|
||||
$message = trans('auth.email_confirm_send_error');
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $newUser;
|
||||
@ -109,5 +111,4 @@ class RegistrationService
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,8 @@ use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\SamlException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use OneLogin\Saml2\Auth;
|
||||
@ -375,6 +377,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
auth()->login($user);
|
||||
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,23 @@
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use Laravel\Socialite\Contracts\Provider;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
|
||||
protected $userRepo;
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
@ -25,14 +27,11 @@ class SocialAuthService
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
|
||||
public function __construct(Socialite $socialite)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
$this->socialite = $socialite;
|
||||
$this->socialAccount = $socialAccount;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the social login path.
|
||||
* @throws SocialDriverNotConfigured
|
||||
@ -60,11 +59,11 @@ class SocialAuthService
|
||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
||||
{
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
|
||||
$email = $socialUser->getEmail();
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||
}
|
||||
@ -91,7 +90,7 @@ class SocialAuthService
|
||||
$socialId = $socialUser->getId();
|
||||
|
||||
// Get any attached social accounts or users
|
||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = user();
|
||||
$titleCaseDriver = Str::title($socialDriver);
|
||||
@ -101,14 +100,15 @@ class SocialAuthService
|
||||
if (!$isLoggedIn && $socialAccount !== null) {
|
||||
auth()->login($socialAccount->user);
|
||||
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// When a user is logged in but the social account does not exist,
|
||||
// Create the social account and attach it to the user & redirect to the profile page.
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($account);
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
@ -207,21 +207,19 @@ class SocialAuthService
|
||||
/**
|
||||
* Fill and return a SocialAccount from the given driver name and SocialUser.
|
||||
*/
|
||||
public function fillSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
{
|
||||
$this->socialAccount->fill([
|
||||
return new SocialAccount([
|
||||
'driver' => $socialDriver,
|
||||
'driver_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar()
|
||||
]);
|
||||
return $this->socialAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function detachSocialAccount(string $socialDriver)
|
||||
public function detachSocialAccount(string $socialDriver): void
|
||||
{
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
}
|
||||
@ -242,4 +240,20 @@ class SocialAuthService
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom socialite driver to be used.
|
||||
* Driver name should be lower_snake_case.
|
||||
* Config array should mirror the structure of a service
|
||||
* within the `Config/services.php` file.
|
||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||
*/
|
||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
|
||||
{
|
||||
$this->validSocialDrivers[] = $driverName;
|
||||
config()->set('services.' . $driverName, $config);
|
||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,33 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* @var ?array
|
||||
*/
|
||||
protected $userRoles = null;
|
||||
|
||||
protected $currentAction;
|
||||
protected $isAdminUser;
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
/**
|
||||
* @var ?User
|
||||
*/
|
||||
protected $currentUserModel = null;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
@ -27,47 +35,20 @@ class PermissionService
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var JointPermission
|
||||
* @var array
|
||||
*/
|
||||
protected $jointPermission;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var EntityPermission
|
||||
*/
|
||||
protected $entityPermission;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
JointPermission $jointPermission,
|
||||
Permissions\EntityPermission $entityPermission,
|
||||
Role $role,
|
||||
Connection $db,
|
||||
EntityProvider $entityProvider
|
||||
) {
|
||||
public function __construct(Connection $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
@ -76,81 +57,63 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache($entities = [])
|
||||
protected function readyEntityCache(array $entities = [])
|
||||
{
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$type = $entity->getType();
|
||||
if (!isset($this->entityCache[$type])) {
|
||||
$this->entityCache[$type] = collect();
|
||||
$class = get_class($entity);
|
||||
if (!isset($this->entityCache[$class])) {
|
||||
$this->entityCache[$class] = collect();
|
||||
}
|
||||
$this->entityCache[$type]->put($entity->id, $entity);
|
||||
$this->entityCache[$class]->put($entity->id, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
protected function getBook(int $bookId): ?Book
|
||||
{
|
||||
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
|
||||
return $this->entityCache['book']->get($bookId);
|
||||
if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
|
||||
return $this->entityCache[Book::class]->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->entityProvider->book->find($bookId);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
|
||||
return $book;
|
||||
return Book::query()->withTrashed()->find($bookId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return \BookStack\Entities\Models\Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
protected function getChapter(int $chapterId): ?Chapter
|
||||
{
|
||||
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
|
||||
return $this->entityCache['chapter']->get($chapterId);
|
||||
if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
|
||||
return $this->entityCache[Chapter::class]->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->entityProvider->chapter->find($chapterId);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
return Chapter::query()
|
||||
->withTrashed()
|
||||
->find($chapterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
* Get the roles for the current logged in user.
|
||||
*/
|
||||
protected function getRoles()
|
||||
protected function getCurrentUserRoles(): array
|
||||
{
|
||||
if ($this->userRoles !== false) {
|
||||
if (!is_null($this->userRoles)) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
$this->userRoles = [Role::getSystemRole('public')->id];
|
||||
} else {
|
||||
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser()->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,59 +121,57 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
JointPermission::query()->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
$roles = Role::query()->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
return $this->entityProvider->book->withTrashed()->newQuery()
|
||||
->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
|
||||
return Book::query()->withTrashed()
|
||||
->select(['id', 'restricted', 'owned_by'])->with([
|
||||
'chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
}, 'pages' => function ($query) {
|
||||
},
|
||||
'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
* Build joint permissions for the given shelf and role combinations.
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
@ -227,55 +188,53 @@ class PermissionService
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
$this->createManyJointPermissions($entities->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
if ($entity instanceof Chapter) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
$this->buildJointPermissionsForEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
public function buildJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$roles = Role::query()->get()->values()->all();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
@ -288,7 +247,7 @@ class PermissionService
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
|
||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
@ -296,7 +255,6 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
@ -312,13 +270,13 @@ class PermissionService
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
JointPermission::query()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@ -327,10 +285,10 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param \BookStack\Entities\Models\Entity[] $entities
|
||||
* @throws \Throwable
|
||||
* @param Entity[] $entities
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
@ -352,19 +310,19 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
* @throws \Throwable
|
||||
* Create & Save entity jointPermissions for many entities and roles.
|
||||
* @param Entity[] $entities
|
||||
* @param Role[] $roles
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
protected function createManyJointPermissions(array $entities, array $roles)
|
||||
{
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
$permissionFetch = EntityPermission::query();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
@ -408,16 +366,14 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
protected function getActions(Entity $entity): array
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
return $baseActions;
|
||||
@ -426,14 +382,8 @@ class PermissionService
|
||||
/**
|
||||
* Create entity permission data for an entity and role
|
||||
* for a particular action.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param string $action
|
||||
* @param array $permissionMap
|
||||
* @param array $rolePermissionMap
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
@ -450,7 +400,7 @@ class PermissionService
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
if ($entity instanceof Book || $entity instanceof Bookshelf) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
@ -460,7 +410,7 @@ class PermissionService
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
|
||||
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
@ -479,29 +429,18 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
* @param $entityMap
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
|
||||
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
return $entityMap[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
@ -510,7 +449,7 @@ class PermissionService
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'owned_by' => $entity->getRawAttribute('owned_by')
|
||||
'owned_by' => $entity->getRawAttribute('owned_by'),
|
||||
];
|
||||
}
|
||||
|
||||
@ -524,55 +463,47 @@ class PermissionService
|
||||
|
||||
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
$user = $this->currentUser();
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
|
||||
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
|
||||
$this->currentAction = 'view';
|
||||
$allPermission = $user && $user->can($permission . '-all');
|
||||
$ownPermission = $user && $user->can($permission . '-own');
|
||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField;
|
||||
$isOwner = $user && $user->id === $ownable->$ownerField;
|
||||
return ($allPermission || ($isOwner && $ownPermission));
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
$action = $permission;
|
||||
}
|
||||
|
||||
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
||||
$this->clean();
|
||||
return $q;
|
||||
return $hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
* @param string $permission
|
||||
* @param string $entityClass
|
||||
* @return bool
|
||||
*/
|
||||
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
|
||||
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
|
||||
{
|
||||
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
||||
$userId = $this->currentUser()->id;
|
||||
|
||||
$permissionQuery = $this->db->table('joint_permissions')
|
||||
$permissionQuery = JointPermission::query()
|
||||
->where('action', '=', $permission)
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where(function ($query) use ($userId) {
|
||||
$query->where('has_permission', '=', 1)
|
||||
->orWhere(function ($query2) use ($userId) {
|
||||
$query2->where('has_permission_own', '=', 1)
|
||||
->where('owned_by', '=', $userId);
|
||||
});
|
||||
->where(function (Builder $query) use ($userId) {
|
||||
$this->addJointHasPermissionCheck($query, $userId);
|
||||
});
|
||||
|
||||
if (!is_null($entityClass)) {
|
||||
$entityInstance = app()->make($entityClass);
|
||||
$entityInstance = app($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
@ -581,46 +512,22 @@ class PermissionService
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function checkIfRestrictionsSet(Entity $entity, $action)
|
||||
{
|
||||
$this->currentAction = $action;
|
||||
if ($entity->isA('page')) {
|
||||
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
return $entity->restricted || $entity->book->restricted;
|
||||
} elseif ($entity->isA('book')) {
|
||||
return $entity->restricted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The general query filter to remove all entities
|
||||
* that the current user does not have access to.
|
||||
* @param $query
|
||||
* @return mixed
|
||||
*/
|
||||
protected function entityRestrictionQuery($query)
|
||||
protected function entityRestrictionQuery(Builder $query, string $action): Builder
|
||||
{
|
||||
$q = $query->where(function ($parentQuery) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
->where('action', '=', $this->currentAction)
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$q = $query->where(function ($parentQuery) use ($action) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $action)
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
@ -634,14 +541,10 @@ class PermissionService
|
||||
$this->clean();
|
||||
return $query->where(function (Builder $parentQuery) use ($ability) {
|
||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $ability)
|
||||
->where(function (Builder $query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -651,7 +554,7 @@ class PermissionService
|
||||
* Extend the given page query to ensure draft items are not visible
|
||||
* unless created by the given user.
|
||||
*/
|
||||
public function enforceDraftVisiblityOnQuery(Builder $query): Builder
|
||||
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
$query->where('draft', '=', false)
|
||||
@ -663,109 +566,89 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|\BookStack\Entities\Models\Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
* Add restrictions for a generic entity.
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
|
||||
{
|
||||
if (strtolower($entityType) === 'page') {
|
||||
if ($entity instanceof Page) {
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
$this->enforceDraftVisibilityOnQuery($query);
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
return $this->entityRestrictionQuery($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @param string $action
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
|
||||
{
|
||||
|
||||
$this->currentAction = $action;
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
->where('action', '=', $action)
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where permissions are granted.
|
||||
* @param $entityType
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
* where view permissions are granted.
|
||||
*/
|
||||
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
||||
{
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
$morphClass = app($entityClass)->getMorphClass();
|
||||
|
||||
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
$q = $query->where(function ($query) use ($tableDetails, $morphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $morphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', $pageMorphClass)
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
->where('entity_type', '=', $morphClass)
|
||||
->where('action', '=', 'view')
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return \BookStack\Auth\User
|
||||
* Add the query for checking the given user id has permission
|
||||
* within the join_permissions table.
|
||||
* @param QueryBuilder|Builder $query
|
||||
*/
|
||||
private function currentUser()
|
||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $userIdToCheck);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
*/
|
||||
private function currentUser(): User
|
||||
{
|
||||
if (is_null($this->currentUserModel)) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
@ -775,10 +658,9 @@ class PermissionService
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean()
|
||||
private function clean(): void
|
||||
{
|
||||
$this->currentUserModel = false;
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
$this->currentUserModel = null;
|
||||
$this->userRoles = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use BookStack\Uploads\Image;
|
||||
@ -22,6 +24,7 @@ use Illuminate\Support\Collection;
|
||||
* Class User
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property Carbon $created_at
|
||||
@ -30,8 +33,9 @@ use Illuminate\Support\Collection;
|
||||
* @property int $image_id
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
* @property Collection $roles
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
|
||||
{
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
|
||||
@ -72,23 +76,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
public static function getDefault(): User
|
||||
{
|
||||
if (!is_null(static::$defaultUser)) {
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
static::$defaultUser = static::where('system_name', '=', 'public')->first();
|
||||
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is the default public user.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault()
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->system_name === 'public';
|
||||
}
|
||||
@ -115,12 +117,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
public function hasSystemRole(string $roleSystemName): bool
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains($role);
|
||||
return $this->roles->pluck('system_name')->contains($roleSystemName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,9 +184,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
* @return HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
public function socialAccounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
@ -207,11 +206,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* @param int $size
|
||||
* @return string
|
||||
* Returns a URL to the user's avatar
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
public function getAvatar(int $size = 50): string
|
||||
{
|
||||
$default = url('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
@ -229,9 +226,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Get the avatar for the user.
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function avatar()
|
||||
public function avatar(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
@ -271,15 +267,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getProfileUrl(): string
|
||||
{
|
||||
return url('/user/' . $this->id);
|
||||
return url('/user/' . $this->slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shortened version of the user's name.
|
||||
* @param int $chars
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
public function getShortName(int $chars = 8): string
|
||||
{
|
||||
if (mb_strlen($this->name) <= $chars) {
|
||||
return $this->name;
|
||||
@ -310,4 +304,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
{
|
||||
return "({$this->id}) {$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
return $this->slug;
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,14 @@ class UserRepo
|
||||
return User::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their slug.
|
||||
*/
|
||||
public function getBySlug(string $slug): User
|
||||
{
|
||||
return User::query()->where('slug', '=', $slug)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
*/
|
||||
@ -159,7 +167,13 @@ class UserRepo
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||
];
|
||||
return User::query()->forceCreate($details);
|
||||
|
||||
$user = new User();
|
||||
$user->forceFill($details);
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -19,13 +19,6 @@ return [
|
||||
// private configuration variables so should remain disabled in public.
|
||||
'debug' => env('APP_DEBUG', false),
|
||||
|
||||
// Set the default view type for various lists. Can be overridden by user preferences.
|
||||
// These will be used for public viewers and users that have not set a preference.
|
||||
'views' => [
|
||||
'books' => env('APP_VIEWS_BOOKS', 'list'),
|
||||
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
],
|
||||
|
||||
// The number of revisions to keep in the database.
|
||||
// Once this limit is reached older revisions will be deleted.
|
||||
// If set to false then a limit will not be enforced.
|
||||
@ -63,7 +56,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
@ -122,6 +115,7 @@ return [
|
||||
BookStack\Providers\TranslationServiceProvider::class,
|
||||
|
||||
// BookStack custom service providers
|
||||
BookStack\Providers\ThemeServiceProvider::class,
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\BroadcastServiceProvider::class,
|
||||
@ -190,10 +184,10 @@ return [
|
||||
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Setting' => BookStack\Facades\Setting::class,
|
||||
'Views' => BookStack\Facades\Views::class,
|
||||
'Images' => BookStack\Facades\Images::class,
|
||||
'Permissions' => BookStack\Facades\Permissions::class,
|
||||
'Theme' => BookStack\Facades\Theme::class,
|
||||
|
||||
],
|
||||
|
||||
|
@ -132,6 +132,7 @@ return [
|
||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
'start_tls' => env('LDAP_START_TLS', false),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -59,7 +59,7 @@ return [
|
||||
// The session cookie path determines the path for which the cookie will
|
||||
// be regarded as available. Typically, this will be the root path of
|
||||
// your application but you are free to change this when necessary.
|
||||
'path' => '/',
|
||||
'path' => '/' . (explode('/', env('APP_URL', ''), 4)[3] ?? ''),
|
||||
|
||||
// Session Cookie Domain
|
||||
// Here you may change the domain of the cookie used to identify a session
|
||||
|
@ -24,4 +24,12 @@ return [
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
// User-level default settings
|
||||
'user' => [
|
||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -4,6 +4,7 @@ namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateUrl extends Command
|
||||
{
|
||||
@ -60,22 +61,50 @@ class UpdateUrl extends Command
|
||||
"attachments" => ["path"],
|
||||
"pages" => ["html", "text", "markdown"],
|
||||
"images" => ["url"],
|
||||
"settings" => ["value"],
|
||||
"comments" => ["html", "text"],
|
||||
];
|
||||
|
||||
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$changeCount = $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
|
||||
]);
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldUrl, $newUrl);
|
||||
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$jsonColumnsToUpdateByTable = [
|
||||
"settings" => ["value"],
|
||||
];
|
||||
|
||||
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$oldJson = trim(json_encode($oldUrl), '"');
|
||||
$newJson = trim(json_encode($newUrl), '"');
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldJson, $newJson);
|
||||
$this->info("Updated {$changeCount} JSON encoded rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("URL update procedure complete.");
|
||||
$this->info('============================================================================');
|
||||
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
|
||||
$this->info('============================================================================');
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a find+replace operations in the provided table and column.
|
||||
* Returns the count of rows changed.
|
||||
*/
|
||||
protected function replaceValueInTable(string $table, string $column, string $oldUrl, string $newUrl): int
|
||||
{
|
||||
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
|
||||
$newQuoted = $this->db->getPdo()->quote($newUrl);
|
||||
return $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})")
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn the user of the dangers of this operation.
|
||||
* Returns a boolean indicating if they've accepted the warnings.
|
||||
|
@ -1,8 +1,5 @@
|
||||
<?php namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@ -49,7 +46,7 @@ abstract class BookChild extends Entity
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if ($this instanceof Chapter) {
|
||||
foreach ($this->pages as $page) {
|
||||
foreach ($this->pages()->withTrashed()->get() as $page) {
|
||||
$page->changeBook($newBookId);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
@ -37,7 +38,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static Builder withLastView()
|
||||
* @method static Builder withViewCount()
|
||||
*/
|
||||
abstract class Entity extends Model
|
||||
abstract class Entity extends Model implements Sluggable
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasCreatorAndUpdater;
|
||||
@ -289,11 +290,11 @@ abstract class Entity extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and set a new URL slug for this model.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = (new SlugGenerator)->generate($this);
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
return $this->slug;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class Page extends BookChild
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
$query = Permissions::enforceDraftVisiblityOnQuery($query);
|
||||
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
||||
return parent::scopeVisible($query);
|
||||
}
|
||||
|
||||
|
@ -177,25 +177,24 @@ class PageRepo
|
||||
// Hold the old details to compare later
|
||||
$oldHtml = $page->html;
|
||||
$oldName = $page->name;
|
||||
$oldMarkdown = $page->markdown;
|
||||
|
||||
$this->updateTemplateStatusAndContentFromInput($page, $input);
|
||||
$this->baseRepo->update($page, $input);
|
||||
|
||||
// Update with new details
|
||||
$page->revision_count++;
|
||||
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->getUserDraftQuery($page)->delete();
|
||||
|
||||
// Save a revision after updating
|
||||
$summary = $input['summary'] ?? null;
|
||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
|
||||
$summary = trim($input['summary'] ?? "");
|
||||
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
|
||||
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
|
||||
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
|
||||
if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
|
||||
$this->savePageRevision($page, $summary);
|
||||
}
|
||||
|
||||
@ -224,10 +223,6 @@ class PageRepo
|
||||
{
|
||||
$revision = new PageRevision($page->getAttributes());
|
||||
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$revision->markdown = '';
|
||||
}
|
||||
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
@ -290,7 +285,13 @@ class PageRepo
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$content = new PageContent($page);
|
||||
|
||||
if (!empty($revision->markdown)) {
|
||||
$content->setNewMarkdown($revision->markdown);
|
||||
} else {
|
||||
$content->setNewHTML($revision->html);
|
||||
}
|
||||
|
||||
$page->updated_by = user()->id;
|
||||
$page->refreshSlug();
|
||||
$page->save();
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use DOMDocument;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
@ -53,6 +55,7 @@ class PageContent
|
||||
$environment->addExtension(new TableExtension());
|
||||
$environment->addExtension(new TaskListExtension());
|
||||
$environment->addExtension(new CustomStrikeThroughExtension());
|
||||
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
|
||||
$converter = new CommonMarkConverter([], $environment);
|
||||
return $converter->convertToHtml($markdown);
|
||||
}
|
||||
|
@ -137,5 +137,4 @@ class SearchOptions
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use Illuminate\Database\Connection;
|
||||
@ -178,7 +179,7 @@ class SearchRunner
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
||||
return $this->permissionService->enforceEntityRestrictions($entity, $entitySelect, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,24 +271,29 @@ class SearchRunner
|
||||
|
||||
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('updated_by', '=', $user->id);
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('updated_by', '=', $input);
|
||||
|
||||
protected function filterOwnedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$userSlug = $input === 'me' ? user()->slug : trim($input);
|
||||
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
|
||||
if ($user) {
|
||||
$query->where('owned_by', '=', $user->id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function filterInName(EloquentBuilder $query, Entity $model, $input)
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SlugGenerator
|
||||
@ -10,11 +11,11 @@ class SlugGenerator
|
||||
* 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(Entity $entity): string
|
||||
public function generate(Sluggable $model): string
|
||||
{
|
||||
$slug = $this->formatNameAsSlug($entity->name);
|
||||
while ($this->slugInUse($slug, $entity)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
$slug = $this->formatNameAsSlug($model->name);
|
||||
while ($this->slugInUse($slug, $model)) {
|
||||
$slug .= '-' . Str::random(3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
@ -35,16 +36,16 @@ class SlugGenerator
|
||||
* Check if a slug is already in-use for this
|
||||
* type of model within the same parent.
|
||||
*/
|
||||
protected function slugInUse(string $slug, Entity $entity): bool
|
||||
protected function slugInUse(string $slug, Sluggable $model): bool
|
||||
{
|
||||
$query = $entity->newQuery()->where('slug', '=', $slug);
|
||||
$query = $model->newQuery()->where('slug', '=', $slug);
|
||||
|
||||
if ($entity instanceof BookChild) {
|
||||
$query->where('book_id', '=', $entity->book_id);
|
||||
if ($model instanceof BookChild) {
|
||||
$query->where('book_id', '=', $model->book_id);
|
||||
}
|
||||
|
||||
if ($entity->id) {
|
||||
$query->where('id', '!=', $entity->id);
|
||||
if ($model->id) {
|
||||
$query->where('id', '!=', $model->id);
|
||||
}
|
||||
|
||||
return $query->count() > 0;
|
||||
|
@ -273,11 +273,11 @@ class TrashCan
|
||||
$count++;
|
||||
};
|
||||
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
|
||||
}
|
||||
|
||||
@ -286,19 +286,20 @@ class TrashCan
|
||||
|
||||
/**
|
||||
* Destroy the given entity.
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function destroyEntity(Entity $entity): int
|
||||
{
|
||||
if ($entity->isA('page')) {
|
||||
if ($entity instanceof Page) {
|
||||
return $this->destroyPage($entity);
|
||||
}
|
||||
if ($entity->isA('chapter')) {
|
||||
if ($entity instanceof Chapter) {
|
||||
return $this->destroyChapter($entity);
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
return $this->destroyBook($entity);
|
||||
}
|
||||
if ($entity->isA('shelf')) {
|
||||
if ($entity instanceof Bookshelf) {
|
||||
return $this->destroyShelf($entity);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
class ApiAuthException extends UnauthorizedException {
|
||||
class ApiAuthException extends UnauthorizedException
|
||||
{
|
||||
|
||||
}
|
@ -3,9 +3,7 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -16,36 +14,42 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
/**
|
||||
* A list of the exception types that should not be reported.
|
||||
* A list of the exception types that are not reported.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [
|
||||
AuthorizationException::class,
|
||||
HttpException::class,
|
||||
ModelNotFoundException::class,
|
||||
ValidationException::class,
|
||||
NotFoundException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontFlash = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
public function report(Exception $e)
|
||||
public function report(Exception $exception)
|
||||
{
|
||||
return parent::report($e);
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @param Exception $e
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Exception $e)
|
||||
@ -117,11 +121,8 @@ class Handler extends ExceptionHandler
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type)
|
||||
protected function isExceptionType(Exception $e, string $type): bool
|
||||
{
|
||||
do {
|
||||
if (is_a($e, $type)) {
|
||||
@ -133,10 +134,8 @@ class Handler extends ExceptionHandler
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e)
|
||||
protected function getOriginalMessage(Exception $e): string
|
||||
{
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
|
@ -5,7 +5,6 @@ class NotFoundException extends PrettyException
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
* @param string $message
|
||||
*/
|
||||
public function __construct($message = 'Item not found')
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Setting extends Facade
|
||||
class Theme extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
@ -11,6 +11,6 @@ class Setting extends Facade
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
return 'theme';
|
||||
}
|
||||
}
|
@ -25,5 +25,4 @@ class ApiDocsController extends ApiController
|
||||
$docs = ApiDocsGenerator::generateConsideringCache();
|
||||
return response()->json($docs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ class AuditLogController extends Controller
|
||||
'sort' => $request->get('sort', 'created_at'),
|
||||
'date_from' => $request->get('date_from', ''),
|
||||
'date_to' => $request->get('date_to', ''),
|
||||
'user' => $request->get('user', ''),
|
||||
];
|
||||
|
||||
$query = Activity::query()
|
||||
@ -34,6 +35,9 @@ class AuditLogController extends Controller
|
||||
if ($listDetails['event']) {
|
||||
$query->where('type', '=', $listDetails['event']);
|
||||
}
|
||||
if ($listDetails['user']) {
|
||||
$query->where('user_id', '=', $listDetails['user']);
|
||||
}
|
||||
|
||||
if ($listDetails['date_from']) {
|
||||
$query->where('created_at', '>=', $listDetails['date_from']);
|
||||
|
@ -2,12 +2,15 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\EmailConfirmationService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -80,6 +83,8 @@ class ConfirmEmailController extends Controller
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
$this->showSuccessNotification(trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteByUser($user);
|
||||
|
||||
|
@ -7,7 +7,9 @@ use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -150,6 +152,7 @@ class LoginController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
return redirect()->intended($this->redirectPath());
|
||||
}
|
||||
@ -195,5 +198,4 @@ class LoginController extends Controller
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@ -93,6 +96,8 @@ class RegisterController extends Controller
|
||||
try {
|
||||
$user = $this->registrationService->registerUser($userData);
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
} catch (UserRegistrationException $exception) {
|
||||
if ($exception->getMessage()) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
@ -117,5 +122,4 @@ class RegisterController extends Controller
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -82,5 +82,4 @@ class Saml2Controller extends Controller
|
||||
|
||||
return redirect()->intended();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,16 +2,17 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
|
||||
@ -31,12 +32,11 @@ class SocialController extends Controller
|
||||
$this->registrationService = $registrationService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function getSocialLogin(string $socialDriver)
|
||||
public function login(string $socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
@ -47,7 +47,7 @@ class SocialController extends Controller
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function socialRegister(string $socialDriver)
|
||||
public function register(string $socialDriver)
|
||||
{
|
||||
$this->registrationService->ensureRegistrationAllowed();
|
||||
session()->put('social-callback', 'register');
|
||||
@ -60,7 +60,7 @@ class SocialController extends Controller
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function socialCallback(Request $request, string $socialDriver)
|
||||
public function callback(Request $request, string $socialDriver)
|
||||
{
|
||||
if (!session()->has('social-callback')) {
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
@ -99,7 +99,7 @@ class SocialController extends Controller
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
*/
|
||||
public function detachSocialAccount(string $socialDriver)
|
||||
public function detach(string $socialDriver)
|
||||
{
|
||||
$this->socialAuthService->detachSocialAccount($socialDriver);
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
|
||||
@ -113,7 +113,7 @@ class SocialController extends Controller
|
||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
@ -130,6 +130,8 @@ class SocialController extends Controller
|
||||
|
||||
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
|
||||
$this->showSuccessNotification(trans('auth.register_success'));
|
||||
return redirect('/');
|
||||
|
@ -2,11 +2,14 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -68,6 +71,8 @@ class UserInviteController extends Controller
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
|
||||
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
|
||||
$this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
|
||||
$this->inviteService->deleteByUser($user);
|
||||
|
||||
|
@ -30,7 +30,7 @@ class BookController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
|
||||
$view = setting()->getForCurrentUser('books_view_type');
|
||||
$sort = setting()->getForCurrentUser('books_sort', 'name');
|
||||
$order = setting()->getForCurrentUser('books_sort_order', 'asc');
|
||||
|
||||
|
@ -32,7 +32,7 @@ class BookshelfController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
||||
$view = setting()->getForCurrentUser('bookshelves_view_type');
|
||||
$sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
|
||||
$order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
|
||||
$sortOptions = [
|
||||
@ -103,7 +103,7 @@ class BookshelfController extends Controller
|
||||
|
||||
Views::add($shelf);
|
||||
$this->entityContextManager->setShelfContext($shelf->id);
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type', config('app.views.books'));
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
||||
|
||||
$this->setPageTitle($shelf->getShortName());
|
||||
return view('shelves.show', [
|
||||
|
@ -159,6 +159,6 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function getImageValidationRules(): string
|
||||
{
|
||||
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,webp';
|
||||
return 'image_extension|mimes:jpeg,png,gif,webp';
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ class HomeController extends Controller
|
||||
// Add required list ordering & sorting for books & shelves views.
|
||||
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
|
||||
$key = $homepageOption;
|
||||
$view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
|
||||
$view = setting()->getForCurrentUser($key . '_view_type');
|
||||
$sort = setting()->getForCurrentUser($key . '_sort', 'name');
|
||||
$order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
|
@ -140,5 +140,4 @@ class UserApiTokenController extends Controller
|
||||
$token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();
|
||||
return [$user, $token];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,10 +5,13 @@ use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@ -61,7 +64,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Store a newly created user in storage.
|
||||
* @throws UserUpdateException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
@ -90,6 +93,7 @@ class UserController extends Controller
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
if ($sendInvite) {
|
||||
@ -132,8 +136,8 @@ class UserController extends Controller
|
||||
/**
|
||||
* Update the specified user in storage.
|
||||
* @throws UserUpdateException
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ImageUploadException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
@ -157,6 +161,11 @@ class UserController extends Controller
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
// Refresh the slug if the user's name has changed
|
||||
if ($user->isDirty('name')) {
|
||||
$user->refreshSlug();
|
||||
}
|
||||
|
||||
// Role updates
|
||||
if (userCan('users-manage') && $request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
@ -216,7 +225,7 @@ class UserController extends Controller
|
||||
|
||||
/**
|
||||
* Remove the specified user from storage.
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(Request $request, int $id)
|
||||
{
|
||||
@ -243,25 +252,6 @@ class UserController extends Controller
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user profile page
|
||||
*/
|
||||
public function showProfilePage($id)
|
||||
{
|
||||
$user = $this->userRepo->getById($id);
|
||||
|
||||
$userActivity = $this->userRepo->getActivity($user);
|
||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred book-list display setting.
|
||||
*/
|
||||
|
25
app/Http/Controllers/UserProfileController.php
Normal file
25
app/Http/Controllers/UserProfileController.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\UserRepo;
|
||||
|
||||
class UserProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show the user profile page
|
||||
*/
|
||||
public function show(UserRepo $repo, string $slug)
|
||||
{
|
||||
$user = $repo->getBySlug($slug);
|
||||
|
||||
$userActivity = $repo->getActivity($user);
|
||||
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $repo->getAssetCounts($user);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
'recentlyCreated' => $recentlyCreated,
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\BookStack\Http\Middleware\RunThemeActions::class,
|
||||
\BookStack\Http\Middleware\Localization::class,
|
||||
],
|
||||
'api' => [
|
||||
|
@ -18,6 +18,8 @@ class Localization
|
||||
protected $localeMap = [
|
||||
'ar' => 'ar',
|
||||
'bg' => 'bg_BG',
|
||||
'bs' => 'bs_BA',
|
||||
'ca' => 'ca',
|
||||
'da' => 'da_DK',
|
||||
'de' => 'de_DE',
|
||||
'de_informal' => 'de_DE',
|
||||
@ -26,13 +28,15 @@ class Localization
|
||||
'es_AR' => 'es_AR',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'id' => 'id_ID',
|
||||
'it' => 'it_IT',
|
||||
'ja' => 'ja',
|
||||
'ko' => 'ko_KR',
|
||||
'lv' => 'lv_LV',
|
||||
'nl' => 'nl_NL',
|
||||
'nb' => 'nb_NO',
|
||||
'pl' => 'pl_PL',
|
||||
'pt' => 'pl_PT',
|
||||
'pt' => 'pt_PT',
|
||||
'pt_BR' => 'pt_BR',
|
||||
'ru' => 'ru',
|
||||
'sk' => 'sk_SK',
|
||||
|
29
app/Http/Middleware/RunThemeActions.php
Normal file
29
app/Http/Middleware/RunThemeActions.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Closure;
|
||||
|
||||
class RunThemeActions
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$earlyResponse = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $request);
|
||||
if (!is_null($earlyResponse)) {
|
||||
return $earlyResponse;
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
$response = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_AFTER, $request, $response) ?? $response;
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -14,5 +14,4 @@ class ThrottleApiRequests extends Middleware
|
||||
{
|
||||
return (int) config('api.requests_per_minute');
|
||||
}
|
||||
|
||||
}
|
23
app/Interfaces/Sluggable.php
Normal file
23
app/Interfaces/Sluggable.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Interface Sluggable
|
||||
*
|
||||
* Assigned to models that can have slugs.
|
||||
* Must have the below properties.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @method Builder newQuery
|
||||
*/
|
||||
interface Sluggable
|
||||
{
|
||||
|
||||
/**
|
||||
* Regenerate the slug for this model.
|
||||
*/
|
||||
public function refreshSlug(): string;
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
use Blade;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||
@ -12,6 +13,7 @@ use Illuminate\Contracts\Cache\Repository;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
|
||||
use Schema;
|
||||
use URL;
|
||||
|
||||
@ -62,5 +64,9 @@ class AppServiceProvider extends ServiceProvider
|
||||
$this->app->singleton(SettingService::class, function ($app) {
|
||||
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
|
||||
});
|
||||
|
||||
$this->app->singleton(SocialAuthService::class, function($app) {
|
||||
return new SocialAuthService($app->make(SocialiteFactory::class));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ namespace BookStack\Providers;
|
||||
use BookStack\Actions\ActivityService;
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Settings\SettingService;
|
||||
use BookStack\Theming\ThemeService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -36,10 +36,6 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
return $this->app->make(ViewService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('setting', function () {
|
||||
return $this->app->make(SettingService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('images', function () {
|
||||
return $this->app->make(ImageService::class);
|
||||
});
|
||||
@ -47,5 +43,9 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
$this->app->singleton('permissions', function () {
|
||||
return $this->app->make(PermissionService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('theme', function () {
|
||||
return $this->app->make(ThemeService::class);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -18,11 +18,6 @@ class CustomValidationServiceProvider extends ServiceProvider
|
||||
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
|
||||
});
|
||||
|
||||
Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
|
||||
$uploadName = $value->getClientOriginalName();
|
||||
return substr_count($uploadName, '.') < 2;
|
||||
});
|
||||
|
||||
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
|
||||
$cleanLinkName = strtolower(trim($value));
|
||||
$isJs = strpos($cleanLinkName, 'javascript:') === 0;
|
||||
|
34
app/Providers/ThemeServiceProvider.php
Normal file
34
app/Providers/ThemeServiceProvider.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Theming\ThemeService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ThemeServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(ThemeService::class, function ($app) {
|
||||
return new ThemeService;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$themeService = $this->app->make(ThemeService::class);
|
||||
$themeService->readThemeActions();
|
||||
$themeService->dispatch(ThemeEvents::APP_BOOT, $this->app);
|
||||
}
|
||||
}
|
@ -17,5 +17,4 @@ class TranslationServiceProvider extends BaseProvider
|
||||
return new FileLoader($app['files'], $app['path.lang']);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -29,9 +29,9 @@ class SettingService
|
||||
* Gets a setting from the database,
|
||||
* If not found, Returns default, Which is false by default.
|
||||
*/
|
||||
public function get(string $key, $default = false)
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
if ($default === false) {
|
||||
if (is_null($default)) {
|
||||
$default = config('setting-defaults.' . $key, false);
|
||||
}
|
||||
|
||||
@ -57,8 +57,12 @@ class SettingService
|
||||
/**
|
||||
* Get a user-specific setting from the database or cache.
|
||||
*/
|
||||
public function getUser(User $user, string $key, $default = false)
|
||||
public function getUser(User $user, string $key, $default = null)
|
||||
{
|
||||
if (is_null($default)) {
|
||||
$default = config('setting-defaults.user.' . $key, false);
|
||||
}
|
||||
|
||||
if ($user->isDefault()) {
|
||||
return $this->getFromSession($key, $default);
|
||||
}
|
||||
@ -68,7 +72,7 @@ class SettingService
|
||||
/**
|
||||
* Get a value for the current logged-in user.
|
||||
*/
|
||||
public function getForCurrentUser(string $key, $default = false)
|
||||
public function getForCurrentUser(string $key, $default = null)
|
||||
{
|
||||
return $this->getUser(user(), $key, $default);
|
||||
}
|
||||
@ -172,7 +176,7 @@ class SettingService
|
||||
*/
|
||||
protected function formatArrayValue(array $value): string
|
||||
{
|
||||
$values = collect($value)->values()->filter(function(array $item) {
|
||||
$values = collect($value)->values()->filter(function (array $item) {
|
||||
return count(array_filter($item)) > 0;
|
||||
});
|
||||
return json_encode($values);
|
||||
|
73
app/Theming/ThemeEvents.php
Normal file
73
app/Theming/ThemeEvents.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php namespace BookStack\Theming;
|
||||
|
||||
/**
|
||||
* The ThemeEvents used within BookStack.
|
||||
*
|
||||
* This file details the events that BookStack may fire via the custom
|
||||
* theme system, including event names, parameters and expected return types.
|
||||
*
|
||||
* This system is regarded as semi-stable.
|
||||
* We'll look to fix issues with it or migrate old event types but
|
||||
* events and their signatures may change in new versions of BookStack.
|
||||
* We'd advise testing any usage of these events upon upgrade.
|
||||
*/
|
||||
class ThemeEvents
|
||||
{
|
||||
/**
|
||||
* Application boot-up.
|
||||
* After main services are registered.
|
||||
* @param \BookStack\Application $app
|
||||
*/
|
||||
const APP_BOOT = 'app_boot';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
* that depend on the current session user (Localization for example).
|
||||
* Provides the original request to use.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_BEFORE = 'web_middleware_before';
|
||||
|
||||
/**
|
||||
* Web after middleware action.
|
||||
* Runs after the request is handled but before the response is sent.
|
||||
* Provides both the original request and the currently resolved response.
|
||||
* Return values, if provided, will be used as a new response to use.
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @returns \Illuminate\Http\Response|null
|
||||
*/
|
||||
const WEB_MIDDLEWARE_AFTER = 'web_middleware_after';
|
||||
|
||||
/**
|
||||
* Auth login event.
|
||||
* Runs right after a user is logged-in to the application by any authentication
|
||||
* system as a standard app user. This includes a user becoming logged in
|
||||
* after registration. This is not emitted upon API usage.
|
||||
* @param string $authSystem
|
||||
* @param \BookStack\Auth\User $user
|
||||
*/
|
||||
const AUTH_LOGIN = 'auth_login';
|
||||
|
||||
/**
|
||||
* Auth register event.
|
||||
* Runs right after a user is newly registered to the application by any authentication
|
||||
* system as a standard app user. This includes auto-registration systems used
|
||||
* by LDAP, SAML and social systems. It only includes self-registrations.
|
||||
* @param string $authSystem
|
||||
* @param \BookStack\Auth\User $user
|
||||
*/
|
||||
const AUTH_REGISTER = 'auth_register';
|
||||
|
||||
/**
|
||||
* Commonmark environment configure.
|
||||
* Provides the commonmark library environment for customization
|
||||
* before its used to render markdown content.
|
||||
* If the listener returns a non-null value, that will be used as an environment instead.
|
||||
* @param \League\CommonMark\ConfigurableEnvironmentInterface $environment
|
||||
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
|
||||
*/
|
||||
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
|
||||
}
|
61
app/Theming/ThemeService.php
Normal file
61
app/Theming/ThemeService.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php namespace BookStack\Theming;
|
||||
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
|
||||
class ThemeService
|
||||
{
|
||||
protected $listeners = [];
|
||||
|
||||
/**
|
||||
* Listen to a given custom theme event,
|
||||
* setting up the action to be ran when the event occurs.
|
||||
*/
|
||||
public function listen(string $event, callable $action)
|
||||
{
|
||||
if (!isset($this->listeners[$event])) {
|
||||
$this->listeners[$event] = [];
|
||||
}
|
||||
|
||||
$this->listeners[$event][] = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the given event name.
|
||||
* Runs any registered listeners for that event name,
|
||||
* passing all additional variables to the listener action.
|
||||
*
|
||||
* If a callback returns a non-null value, this method will
|
||||
* stop and return that value itself.
|
||||
* @return mixed
|
||||
*/
|
||||
public function dispatch(string $event, ...$args)
|
||||
{
|
||||
foreach ($this->listeners[$event] ?? [] as $action) {
|
||||
$result = call_user_func_array($action, $args);
|
||||
if (!is_null($result)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read any actions from the set theme path if the 'functions.php' file exists.
|
||||
*/
|
||||
public function readThemeActions()
|
||||
{
|
||||
$themeActionsFile = theme_path('functions.php');
|
||||
if (file_exists($themeActionsFile)) {
|
||||
require $themeActionsFile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SocialAuthService::addSocialDriver
|
||||
*/
|
||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
|
||||
{
|
||||
$socialAuthService = app()->make(SocialAuthService::class);
|
||||
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler);
|
||||
}
|
||||
}
|
@ -24,5 +24,4 @@ trait HasCreatorAndUpdater
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,5 +15,4 @@ trait HasOwner
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owned_by');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -70,8 +70,7 @@ class ImageRepo
|
||||
int $uploadedTo = null,
|
||||
string $search = null,
|
||||
callable $whereClause = null
|
||||
): array
|
||||
{
|
||||
): array {
|
||||
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
|
||||
|
||||
if ($uploadedTo !== null) {
|
||||
@ -83,7 +82,7 @@ class ImageRepo
|
||||
}
|
||||
|
||||
// Filter by page access
|
||||
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
|
||||
$imageQuery = $this->restrictionService->filterRelatedEntity(Page::class, $imageQuery, 'images', 'uploaded_to');
|
||||
|
||||
if ($whereClause !== null) {
|
||||
$imageQuery = $imageQuery->where($whereClause);
|
||||
@ -102,8 +101,7 @@ class ImageRepo
|
||||
int $pageSize = 24,
|
||||
int $uploadedTo = null,
|
||||
string $search = null
|
||||
): array
|
||||
{
|
||||
): array {
|
||||
$contextPage = $this->page->findOrFail($uploadedTo);
|
||||
$parentFilter = null;
|
||||
|
||||
|
@ -139,7 +139,7 @@ class ImageService
|
||||
$name = str_replace(' ', '-', $name);
|
||||
$nameParts = explode('.', $name);
|
||||
$extension = array_pop($nameParts);
|
||||
$name = implode('.', $nameParts);
|
||||
$name = implode('-', $nameParts);
|
||||
$name = Str::slug($name);
|
||||
|
||||
if (strlen($name) === 0) {
|
||||
|
@ -97,5 +97,4 @@ class UserAvatars
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
}
|
@ -79,9 +79,9 @@ function userCanOnAny(string $permission, string $entityClass = null): bool
|
||||
|
||||
/**
|
||||
* Helper to access system settings.
|
||||
* @return bool|string|SettingService
|
||||
* @return mixed|SettingService
|
||||
*/
|
||||
function setting(string $key = null, $default = false)
|
||||
function setting(string $key = null, $default = null)
|
||||
{
|
||||
$settingService = resolve(SettingService::class);
|
||||
|
||||
|
@ -5,25 +5,25 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"php": "^7.3|^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-xml": "*",
|
||||
"barryvdh/laravel-dompdf": "^0.8.7",
|
||||
"barryvdh/laravel-dompdf": "^0.9.0",
|
||||
"barryvdh/laravel-snappy": "^0.4.8",
|
||||
"doctrine/dbal": "^2.9",
|
||||
"doctrine/dbal": "^2.12.1",
|
||||
"facade/ignition": "^1.16.4",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"intervention/image": "^2.5.1",
|
||||
"laravel/framework": "^6.20.12",
|
||||
"laravel/framework": "^6.20.16",
|
||||
"laravel/socialite": "^5.1",
|
||||
"league/commonmark": "^1.5",
|
||||
"league/flysystem-aws-s3-v3": "^1.0.29",
|
||||
"nunomaduro/collision": "^3.1",
|
||||
"onelogin/php-saml": "^3.3",
|
||||
"onelogin/php-saml": "^4.0",
|
||||
"predis/predis": "^1.1.6",
|
||||
"socialiteproviders/discord": "^4.1",
|
||||
"socialiteproviders/gitlab": "^4.1",
|
||||
@ -36,10 +36,10 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.5.1",
|
||||
"barryvdh/laravel-ide-helper": "^2.8.2",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"fakerphp/faker": "^1.13.0",
|
||||
"laravel/browser-kit-testing": "^5.2",
|
||||
"mockery/mockery": "^1.3.3",
|
||||
"phpunit/phpunit": "^8.0",
|
||||
"phpunit/phpunit": "^9.5.3",
|
||||
"squizlabs/php_codesniffer": "^3.5.8"
|
||||
},
|
||||
"autoload": {
|
||||
@ -87,7 +87,7 @@
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "7.2.5"
|
||||
"php": "7.3.0"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
|
1583
composer.lock
generated
1583
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -12,9 +12,11 @@
|
||||
*/
|
||||
|
||||
$factory->define(\BookStack\Auth\User::class, function ($faker) {
|
||||
$name = $faker->name;
|
||||
return [
|
||||
'name' => $faker->name,
|
||||
'name' => $name,
|
||||
'email' => $faker->email,
|
||||
'slug' => \Illuminate\Support\Str::slug($name . '-' . \Illuminate\Support\Str::random(5)),
|
||||
'password' => Str::random(10),
|
||||
'remember_token' => Str::random(10),
|
||||
'email_confirmed' => 1
|
||||
|
50
database/migrations/2021_03_08_215138_add_user_slug.php
Normal file
50
database/migrations/2021_03_08_215138_add_user_slug.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AddUserSlug extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('slug', 250);
|
||||
});
|
||||
|
||||
$slugMap = [];
|
||||
DB::table('users')->cursor()->each(function ($user) use (&$slugMap) {
|
||||
$userSlug = Str::slug($user->name);
|
||||
while (isset($slugMap[$userSlug])) {
|
||||
$userSlug = Str::slug($user->name . Str::random(4));
|
||||
}
|
||||
$slugMap[$userSlug] = true;
|
||||
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->update(['slug' => $userSlug]);
|
||||
});
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->unique('slug');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('slug');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
FROM php:7.3-apache
|
||||
FROM php:7.4-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 \
|
||||
&& apt-get install -y git zip unzip libpng-dev libldap2-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 \
|
||||
&& docker-php-ext-install pdo_mysql 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 \
|
||||
|
98
dev/docs/logical-theme-system.md
Normal file
98
dev/docs/logical-theme-system.md
Normal file
@ -0,0 +1,98 @@
|
||||
# Logical Theme System
|
||||
|
||||
BookStack allows logical customization via the theme system which enables you to add, or extend, functionality within the PHP side of the system without needing to alter the core application files.
|
||||
|
||||
WARNING: This system is currently in alpha so may incur changes. Once we've gathered some feedback on usage we'll look to removing this warning. This system will be considered semi-stable in the future. The `Theme::` system will be kept maintained but specific customizations or deeper app/framework usage using this system will not be supported nor considered in any way stable. Customizations using this system should be checked after updates.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
||||
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
||||
|
||||
Within your theme folder create a `functions.php` file. BookStack will look for this and run it during app boot-up. Within this file you can use the `Theme` facade API, described below, to hook into certain app events.
|
||||
|
||||
## `Theme` Facade API
|
||||
|
||||
Below details the public methods of the `Theme` facade that are considered stable:
|
||||
|
||||
### `Theme::listen`
|
||||
|
||||
This method listens to a system event and runs the given action when that event occurs. The arguments passed to the action depend on the event. Event names are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class.
|
||||
|
||||
It is possible to listen to a single event using multiple actions. When dispatched, BookStack will loop over and run each action for that event.
|
||||
If an action returns a non-null value then BookStack will stop cycling through actions at that point and make use of the non-null return value if possible (Depending on the event).
|
||||
|
||||
**Arguments**
|
||||
- string $event
|
||||
- callable $action
|
||||
|
||||
**Example**
|
||||
|
||||
```php
|
||||
Theme::listen(
|
||||
\BookStack\Theming\ThemeEvents::AUTH_LOGIN,
|
||||
function($service, $user) {
|
||||
\Log::info("Login by {$user->name} via {$service}");
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### `Theme::addSocialDriver`
|
||||
|
||||
This method allows you to register a custom social authentication driver within the system. This is primarily intended to use with [Socialite Providers](https://socialiteproviders.com/).
|
||||
|
||||
**Arguments**
|
||||
- string $driverName
|
||||
- array $config
|
||||
- string $socialiteHandler
|
||||
|
||||
**Example**
|
||||
|
||||
*See "Custom Socialite Service Example" below.*
|
||||
|
||||
## Available Events
|
||||
|
||||
All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php).
|
||||
|
||||
The comments above each constant with the `ThemeEvents.php` file describe the dispatch conditions of the event, in addition to the arguments the action will receive. The comments may also describe any ways the return value of the action may be used.
|
||||
|
||||
## Example `functions.php` file
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
|
||||
// Logs custom message on user login
|
||||
Theme::listen(ThemeEvents::AUTH_LOGIN, function($method, $user) {
|
||||
Log::info("Login via {$method} for {$user->name}");
|
||||
});
|
||||
|
||||
// Adds a `/info` public URL endpoint that emits php debug details
|
||||
Theme::listen(ThemeEvents::APP_BOOT, function($app) {
|
||||
\Route::get('info', function() {
|
||||
phpinfo(); // Don't do this on a production instance!
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Socialite Service Example
|
||||
|
||||
The below shows an example of adding a custom reddit socialite service to BookStack.
|
||||
BookStack exposes a helper function for this via `Theme::addSocialDriver` which sets the required config and event listeners in the platform.
|
||||
|
||||
The require statements reference composer installed dependencies within the theme folder. They are required manually since they are not auto-loaded like other app files due to being outside the main BookStack dependency list.
|
||||
|
||||
```php
|
||||
require "vendor/socialiteproviders/reddit/Provider.php";
|
||||
require "vendor/socialiteproviders/reddit/RedditExtendSocialite.php";
|
||||
|
||||
Theme::listen(ThemeEvents::APP_BOOT, function($app) {
|
||||
Theme::addSocialDriver('reddit', [
|
||||
'client_id' => 'abc123',
|
||||
'client_secret' => 'def456789',
|
||||
'name' => 'Reddit',
|
||||
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle');
|
||||
});
|
||||
```
|
31
dev/docs/visual-theme-system.md
Normal file
31
dev/docs/visual-theme-system.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Visual Theme System
|
||||
|
||||
BookStack allows visual customization via the theme system which enables you to extensively customize views, translation text & icons.
|
||||
|
||||
This theme system itself is maintained and supported but usages of this system, including the files you are able to override, are not considered stable and may change upon any update. You should test any customizations made after updates.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This makes use of the theme system. Create a folder for your theme within your BookStack `themes` directory. As an example we'll use `my_theme`, so we'd create a `themes/my_theme` folder.
|
||||
You'll need to tell BookStack to use your theme via the `APP_THEME` option in your `.env` file. For example: `APP_THEME=my_theme`.
|
||||
|
||||
## Customizing View Files
|
||||
|
||||
Content placed in your `themes/<theme_name>/` folder will override the original view files found in the `resources/views` folder. These files are typically [Laravel Blade](https://laravel.com/docs/6.x/blade) files.
|
||||
|
||||
## Customizing Icons
|
||||
|
||||
SVG files placed in a `themes/<theme_name>/icons` folder will override any icons of the same name within `resources/icons`. You'd typically want to follow the format convention of the existing icons, where no XML deceleration is included and no width & height attributes are set, to ensure optimal compatibility.
|
||||
|
||||
## Customizing Text Content
|
||||
|
||||
Folders with PHP translation files placed in a `themes/<theme_name>/lang` folder will override translations defined within the `resources/lang` folder. Custom translations are merged with the original files so you only need to override the select translations you want to override, you don't need to copy the whole original file. Note that you'll need to include the language folder.
|
||||
|
||||
As an example, Say I wanted to change 'Search' to 'Find'; Within a `themes/<theme_name>/lang/en/common.php` file I'd set the following:
|
||||
|
||||
```php
|
||||
<?php
|
||||
return [
|
||||
'search' => 'find',
|
||||
];
|
||||
```
|
@ -1,9 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="PHP_CodeSniffer">
|
||||
<ruleset name="BookStack Standard">
|
||||
<!-- Format described at: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-Ruleset -->
|
||||
<description>The coding standard for BookStack.</description>
|
||||
<file>app</file>
|
||||
<config name="php_version" value="70205"/>
|
||||
<file>./app</file>
|
||||
<exclude-pattern>*/migrations/*</exclude-pattern>
|
||||
<exclude-pattern>*/tests/*</exclude-pattern>
|
||||
<arg value="np"/>
|
||||
<arg name="colors"/>
|
||||
<rule ref="PSR2"/>
|
||||
</ruleset>
|
21
phpunit.xml
21
phpunit.xml
@ -1,5 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
@ -8,18 +10,18 @@
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false">
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">app/</directory>
|
||||
</include>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Application Test Suite">
|
||||
<directory>./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">app/</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing" force="true"/>
|
||||
<server name="APP_ENV" value="testing"/>
|
||||
<server name="APP_DEBUG" value="false"/>
|
||||
<server name="APP_LANG" value="en"/>
|
||||
<server name="APP_THEME" value="none"/>
|
||||
@ -29,13 +31,14 @@
|
||||
<server name="CACHE_DRIVER" value="array"/>
|
||||
<server name="SESSION_DRIVER" value="array"/>
|
||||
<server name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="DB_CONNECTION" value="mysql_testing" force="true"/>
|
||||
<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_START_TLS" value="false"/>
|
||||
<server name="LDAP_VERSION" value="3"/>
|
||||
<server name="SESSION_SECURE_COOKIE" value="null"/>
|
||||
<server name="STORAGE_TYPE" value="local"/>
|
||||
@ -56,6 +59,6 @@
|
||||
<server name="LOG_FAILED_LOGIN_MESSAGE" value=""/>
|
||||
<server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
|
||||
<server name="WKHTMLTOPDF" value="false"/>
|
||||
<ini name="memory_limit" value="1024M"/>
|
||||
<server name="APP_DEFAULT_DARK_MODE" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
@ -4,7 +4,8 @@
|
||||
[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
|
||||
[![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack)
|
||||
[![Build Status](https://github.com/BookStackApp/BookStack/workflows/phpunit/badge.svg)](https://github.com/BookStackApp/BookStack/actions)
|
||||
[![Discord](https://img.shields.io/static/v1?label=Chat&message=Discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
|
||||
[![Discord](https://img.shields.io/static/v1?label=chat&message=discord&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
|
||||
[![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)
|
||||
|
||||
A platform for storing and organising information and documentation. Details for BookStack can be found on the official website at https://www.bookstackapp.com/.
|
||||
|
||||
@ -133,7 +134,7 @@ Feel free to create issues to request new features or to report bugs & problems.
|
||||
|
||||
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. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
|
||||
|
||||
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.
|
||||
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/`. 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).
|
||||
|
||||
@ -181,3 +182,5 @@ These are the great open-source projects used to help build BookStack:
|
||||
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
|
||||
* [diagrams.net](https://github.com/jgraph/drawio)
|
||||
* [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
|
||||
* [League/CommonMark](https://commonmark.thephpleague.com/)
|
||||
* [League/Flysystem](https://flysystem.thephpleague.com)
|
||||
|
@ -1,22 +1,32 @@
|
||||
import {onChildEvent} from "../services/dom";
|
||||
|
||||
/**
|
||||
* Entity Selector
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EntitySelector {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.entityTypes = this.$opts.entityTypes || 'page,book,chapter';
|
||||
this.entityPermission = this.$opts.entityPermission || 'view';
|
||||
|
||||
this.input = this.$refs.input;
|
||||
this.searchInput = this.$refs.search;
|
||||
this.loading = this.$refs.loading;
|
||||
this.resultsContainer = this.$refs.results;
|
||||
this.addButton = this.$refs.add;
|
||||
|
||||
this.search = '';
|
||||
this.lastClick = 0;
|
||||
this.selectedItemData = null;
|
||||
|
||||
const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
||||
const entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
|
||||
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
|
||||
|
||||
this.input = elem.querySelector('[entity-selector-input]');
|
||||
this.searchInput = elem.querySelector('[entity-selector-search]');
|
||||
this.loading = elem.querySelector('[entity-selector-loading]');
|
||||
this.resultsContainer = elem.querySelector('[entity-selector-results]');
|
||||
this.addButton = elem.querySelector('[entity-selector-add-button]');
|
||||
this.setupListeners();
|
||||
this.showLoading();
|
||||
this.initialLoad();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||
|
||||
let lastSearch = 0;
|
||||
@ -42,8 +52,39 @@ class EntitySelector {
|
||||
});
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
this.initialLoad();
|
||||
// Keyboard navigation
|
||||
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
|
||||
if (e.ctrlKey && e.code === 'Enter') {
|
||||
const form = this.$el.closest('form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowDown') {
|
||||
this.focusAdjacent(true);
|
||||
}
|
||||
if (e.code === 'ArrowUp') {
|
||||
this.focusAdjacent(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.searchInput.addEventListener('keydown', e => {
|
||||
if (e.code === 'ArrowDown') {
|
||||
this.focusAdjacent(true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
focusAdjacent(forward = true) {
|
||||
const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
|
||||
const selectedIndex = items.indexOf(document.activeElement);
|
||||
const newItem = items[selectedIndex+ (forward ? 1 : -1)] || items[0];
|
||||
if (newItem) {
|
||||
newItem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
@ -57,15 +98,19 @@ class EntitySelector {
|
||||
}
|
||||
|
||||
initialLoad() {
|
||||
window.$http.get(this.searchUrl).then(resp => {
|
||||
window.$http.get(this.searchUrl()).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
})
|
||||
}
|
||||
|
||||
searchUrl() {
|
||||
return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
|
||||
}
|
||||
|
||||
searchEntities(searchTerm) {
|
||||
this.input.value = '';
|
||||
let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
|
||||
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
|
||||
window.$http.get(url).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
@ -73,8 +118,8 @@ class EntitySelector {
|
||||
}
|
||||
|
||||
isDoubleClick() {
|
||||
let now = Date.now();
|
||||
let answer = now - this.lastClick < 300;
|
||||
const now = Date.now();
|
||||
const answer = now - this.lastClick < 300;
|
||||
this.lastClick = now;
|
||||
return answer;
|
||||
}
|
||||
@ -123,8 +168,8 @@ class EntitySelector {
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
let selected = this.elem.querySelectorAll('.selected');
|
||||
for (let selectedElem of selected) {
|
||||
const selected = this.elem.querySelectorAll('.selected');
|
||||
for (const selectedElem of selected) {
|
||||
selectedElem.classList.remove('selected', 'primary-background');
|
||||
}
|
||||
this.selectedItemData = null;
|
||||
|
@ -22,7 +22,6 @@ class MarkdownEditor {
|
||||
|
||||
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);
|
||||
@ -125,7 +124,6 @@ class MarkdownEditor {
|
||||
// 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();
|
||||
|
@ -13,9 +13,11 @@ class UserSelect {
|
||||
}
|
||||
|
||||
selectUser(event, userEl) {
|
||||
event.preventDefault();
|
||||
const id = userEl.getAttribute('data-id');
|
||||
this.input.value = id;
|
||||
this.userInfoContainer.innerHTML = userEl.innerHTML;
|
||||
this.input.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
this.hide();
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,7 @@ function codePlugin() {
|
||||
showPopup(editor);
|
||||
});
|
||||
|
||||
editor.on('SetContent', function () {
|
||||
function parseCodeMirrorInstances() {
|
||||
|
||||
// Recover broken codemirror instances
|
||||
$('.CodeMirrorContainer').filter((index ,elem) => {
|
||||
@ -231,6 +231,17 @@ function codePlugin() {
|
||||
Code.wysiwygView(elem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editor.on('init', function() {
|
||||
// Parse code mirror instances on init, but delay a little so this runs after
|
||||
// initial styles are fetched into the editor.
|
||||
parseCodeMirrorInstances();
|
||||
// Parsed code mirror blocks when content is set but wait before setting this handler
|
||||
// to avoid any init 'SetContent' events.
|
||||
setTimeout(() => {
|
||||
editor.on('SetContent', parseCodeMirrorInstances);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -238,9 +238,7 @@ function wysiwygView(elem) {
|
||||
theme: getTheme(),
|
||||
readOnly: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
cm.refresh();
|
||||
}, 300);
|
||||
|
||||
return {wrap: newWrap, editor: cm};
|
||||
}
|
||||
|
||||
|
@ -77,4 +77,9 @@ return [
|
||||
// Email Content
|
||||
'email_action_help' => 'إذا واجهتكم مشكلة بضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
|
||||
'email_rights' => 'جميع الحقوق محفوظة',
|
||||
|
||||
// Footer Link Options
|
||||
// Not directly used but available for convenience to users.
|
||||
'privacy_policy' => 'Privacy Policy',
|
||||
'terms_of_service' => 'Terms of Service',
|
||||
];
|
||||
|
@ -37,6 +37,11 @@ return [
|
||||
'app_homepage' => 'الصفحة الرئيسية للتطبيق',
|
||||
'app_homepage_desc' => 'الرجاء اختيار صفحة لتصبح الصفحة الرئيسية بدل من الافتراضية. سيتم تجاهل جميع الأذونات الخاصة بالصفحة المختارة.',
|
||||
'app_homepage_select' => 'اختر صفحة',
|
||||
'app_footer_links' => 'Footer Links',
|
||||
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
|
||||
'app_footer_links_label' => 'Link Label',
|
||||
'app_footer_links_url' => 'Link URL',
|
||||
'app_footer_links_add' => 'Add Footer Link',
|
||||
'app_disable_comments' => 'تعطيل التعليقات',
|
||||
'app_disable_comments_toggle' => 'تعطيل التعليقات',
|
||||
'app_disable_comments_desc' => 'تعطيل التعليقات على جميع الصفحات داخل التطبيق. التعليقات الموجودة من الأصل لن تكون ظاهرة.',
|
||||
@ -226,6 +231,8 @@ return [
|
||||
'en' => 'English',
|
||||
'ar' => 'العربية',
|
||||
'bg' => 'Bǎlgarski',
|
||||
'bs' => 'Bosanski',
|
||||
'ca' => 'Català',
|
||||
'cs' => 'Česky',
|
||||
'da' => 'Dansk',
|
||||
'de' => 'Deutsch (Sie)',
|
||||
@ -235,12 +242,15 @@ return [
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
'ja' => '日本語',
|
||||
'ko' => '한국어',
|
||||
'lv' => 'Latviešu Valoda',
|
||||
'nl' => 'Nederlands',
|
||||
'nb' => 'Norsk (Bokmål)',
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
|
@ -78,7 +78,6 @@ return [
|
||||
'string' => 'يجب أن يكون :attribute على الأقل :min حرف / حروف.',
|
||||
'array' => 'يجب أن يحتوي :attribute على :min عنصر / عناصر كحد أدنى.',
|
||||
],
|
||||
'no_double_extension' => 'يجب أن يكون للسمة: امتداد ملف واحد فقط.',
|
||||
'not_in' => ':attribute المحدد غير صالح.',
|
||||
'not_regex' => 'صيغة السمة: غير صالحة.',
|
||||
'numeric' => 'يجب أن يكون :attribute رقم.',
|
||||
|
@ -77,4 +77,9 @@ return [
|
||||
// Email Content
|
||||
'email_action_help' => 'Ако имате проблеми с бутона ":actionText" по-горе, копирайте и поставете URL адреса по-долу в уеб браузъра си:',
|
||||
'email_rights' => 'Всички права запазени',
|
||||
|
||||
// Footer Link Options
|
||||
// Not directly used but available for convenience to users.
|
||||
'privacy_policy' => 'Privacy Policy',
|
||||
'terms_of_service' => 'Terms of Service',
|
||||
];
|
||||
|
@ -37,6 +37,11 @@ return [
|
||||
'app_homepage' => 'Application Homepage',
|
||||
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
|
||||
'app_homepage_select' => 'Select a page',
|
||||
'app_footer_links' => 'Footer Links',
|
||||
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
|
||||
'app_footer_links_label' => 'Link Label',
|
||||
'app_footer_links_url' => 'Link URL',
|
||||
'app_footer_links_add' => 'Add Footer Link',
|
||||
'app_disable_comments' => 'Disable Comments',
|
||||
'app_disable_comments_toggle' => 'Disable comments',
|
||||
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
|
||||
@ -226,6 +231,8 @@ return [
|
||||
'en' => 'English',
|
||||
'ar' => 'العربية',
|
||||
'bg' => 'Bǎlgarski',
|
||||
'bs' => 'Bosanski',
|
||||
'ca' => 'Català',
|
||||
'cs' => 'Česky',
|
||||
'da' => 'Dansk',
|
||||
'de' => 'Deutsch (Sie)',
|
||||
@ -235,12 +242,15 @@ return [
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
'ja' => '日本語',
|
||||
'ko' => '한국어',
|
||||
'lv' => 'Latviešu Valoda',
|
||||
'nl' => 'Nederlands',
|
||||
'nb' => 'Norsk (Bokmål)',
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
|
@ -78,7 +78,6 @@ return [
|
||||
'string' => 'The :attribute must be at least :min characters.',
|
||||
'array' => 'The :attribute must have at least :min items.',
|
||||
],
|
||||
'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.',
|
||||
|
49
resources/lang/bs/activities.php
Normal file
49
resources/lang/bs/activities.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Activity text strings.
|
||||
* Is used for all the text within activity logs & notifications.
|
||||
*/
|
||||
return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'je kreirao/la stranicu',
|
||||
'page_create_notification' => 'Stranica Uspješno Kreirana',
|
||||
'page_update' => 'je ažurirao/la stranicu',
|
||||
'page_update_notification' => 'Stranica Uspješno Ažurirana',
|
||||
'page_delete' => 'je izbrisao/la stranicu',
|
||||
'page_delete_notification' => 'Stranica Uspješno Izbrisana',
|
||||
'page_restore' => 'je vratio/la stranicu',
|
||||
'page_restore_notification' => 'Stranica Uspješno Vraćena',
|
||||
'page_move' => 'je premjestio/la stranicu',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'je kreirao/la poglavlje',
|
||||
'chapter_create_notification' => 'Poglavlje Uspješno Kreirano',
|
||||
'chapter_update' => 'je ažurirao/la poglavlje',
|
||||
'chapter_update_notification' => 'Poglavlje Uspješno Ažurirano',
|
||||
'chapter_delete' => 'je izbrisao/la poglavlje',
|
||||
'chapter_delete_notification' => 'Poglavlje Uspješno Izbrisano',
|
||||
'chapter_move' => 'je premjestio/la poglavlje',
|
||||
|
||||
// Books
|
||||
'book_create' => 'je kreirao/la knjigu',
|
||||
'book_create_notification' => 'Knjiga Uspješno Kreirana',
|
||||
'book_update' => 'je ažurirao/la knjigu',
|
||||
'book_update_notification' => 'Knjiga Uspješno Ažurirana',
|
||||
'book_delete' => 'je izbrisao/la knjigu',
|
||||
'book_delete_notification' => 'Knjiga Uspješno Izbrisana',
|
||||
'book_sort' => 'je sortirao/la knjigu',
|
||||
'book_sort_notification' => 'Knjiga Uspješno Ponovno Sortirana',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'je kreirao/la Policu za knjige',
|
||||
'bookshelf_create_notification' => 'Polica za knjige Uspješno Kreirana',
|
||||
'bookshelf_update' => 'je ažurirao/la policu za knjige',
|
||||
'bookshelf_update_notification' => 'Polica za knjige Uspješno Ažurirana',
|
||||
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
|
||||
'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'je komentarisao/la na',
|
||||
'permissions_update' => 'je ažurirao/la dozvole',
|
||||
];
|
77
resources/lang/bs/auth.php
Normal file
77
resources/lang/bs/auth.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?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' => 'Ovi pristupni podaci se ne slažu sa našom evidencijom.',
|
||||
'throttle' => 'Preveliki broj pokušaja prijave. Molimo vas da pokušate ponovo za :seconds sekundi.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registruj se',
|
||||
'log_in' => 'Prijavi se',
|
||||
'log_in_with' => 'Prijavi se sa :socialDriver',
|
||||
'sign_up_with' => 'Registruj se sa :socialDriver',
|
||||
'logout' => 'Odjavi se',
|
||||
|
||||
'name' => 'Ime',
|
||||
'username' => 'Korisničko ime',
|
||||
'email' => 'E-mail',
|
||||
'password' => 'Lozinka',
|
||||
'password_confirm' => 'Potvrdi lozinku',
|
||||
'password_hint' => 'Mora imati više od 7 karaktera',
|
||||
'forgot_password' => 'Zaboravljena lozinka?',
|
||||
'remember_me' => 'Zapamti me',
|
||||
'ldap_email_hint' => 'Unesite e-mail koji će se koristiti za ovaj račun.',
|
||||
'create_account' => 'Napravi račun',
|
||||
'already_have_account' => 'Već imate račun?',
|
||||
'dont_have_account' => 'Nemate korisnički račun?',
|
||||
'social_login' => 'Prijava preko društvene mreže',
|
||||
'social_registration' => 'Registracija pomoću društvene mreže',
|
||||
'social_registration_text' => 'Registruj i prijavi se koristeći drugi servis.',
|
||||
|
||||
'register_thanks' => 'Hvala na registraciji!',
|
||||
'register_confirm' => 'Provjerite vašu e-mail adresu i pritisnite dugme za potvrdu da bi dobili pristup :appName.',
|
||||
'registrations_disabled' => 'Registracije su trenutno onemogućene',
|
||||
'registration_email_domain_invalid' => 'Ta e-mail domena nema pristup ovoj aplikaciji',
|
||||
'register_success' => 'Hvala na registraciji! Sada ste registrovani i prijavljeni.',
|
||||
|
||||
|
||||
// Password Reset
|
||||
'reset_password' => 'Resetuj Lozinku',
|
||||
'reset_password_send_instructions' => 'Unesite vašu e-mail adresu ispod i na nju ćemo vam poslati e-mail sa linkom za promjenu lozinke.',
|
||||
'reset_password_send_button' => 'Pošalji link za promjenu',
|
||||
'reset_password_sent' => 'Link za promjenu lozinke će biti poslan na :email ako ta adresa postoji u sistemu.',
|
||||
'reset_password_success' => 'Vaša lozinka je uspješno promijenjena.',
|
||||
'email_reset_subject' => 'Resetujte vašu lozinku od :appName',
|
||||
'email_reset_text' => 'Primate ovaj e-mail jer smo dobili zahtjev za promjenu lozinke za vaš račun.',
|
||||
'email_reset_not_requested' => 'Ako niste zahtijevali promjenu lozinke ne trebate ništa više uraditi.',
|
||||
|
||||
|
||||
// Email Confirmation
|
||||
'email_confirm_subject' => 'Potvrdite vaš e-mail na :appName',
|
||||
'email_confirm_greeting' => 'Hvala na pristupanju :appName!',
|
||||
'email_confirm_text' => 'Potvrdite vašu e-mail adresu pritiskom na dugme ispod:',
|
||||
'email_confirm_action' => 'Potvrdi e-mail',
|
||||
'email_confirm_send_error' => 'Potvrda e-maila je obavezna ali sistem nije mogao poslati e-mail. Kontaktirajte administratora da biste bili sigurni da je e-mail postavljen ispravno.',
|
||||
'email_confirm_success' => 'Vaš e-mail je potvrđen!',
|
||||
'email_confirm_resent' => 'E-mail za potvrdu je ponovno poslan. Provjerite vaš e-mail.',
|
||||
|
||||
'email_not_confirmed' => 'E-mail adresa nije potvrđena',
|
||||
'email_not_confirmed_text' => 'Vaša e-mail adresa nije još potvrđena.',
|
||||
'email_not_confirmed_click_link' => 'Kliknite na link u e-mailu koji vam je poslan nakon što ste se registrovali.',
|
||||
'email_not_confirmed_resend' => 'Ako ne možete naći e-mail možete ponovno poslati e-mail za potvrdu tako što ćete ispuniti formu ispod.',
|
||||
'email_not_confirmed_resend_button' => 'Ponovno pošalji e-mail za potvrdu',
|
||||
|
||||
// User Invite
|
||||
'user_invite_email_subject' => 'Pozvani ste da se pridružite :appName!',
|
||||
'user_invite_email_greeting' => 'Račun je napravljen za vas na :appName.',
|
||||
'user_invite_email_text' => 'Pritisnite dugme ispod da niste postavili lozinku vašeg računa i tako dobili pristup:',
|
||||
'user_invite_email_action' => 'Postavi lozinku računa',
|
||||
'user_invite_page_welcome' => 'Dobrodošli na :appName!',
|
||||
'user_invite_page_text' => 'Da biste završili vaš račun i dobili pristup morate postaviti lozinku koju ćete koristiti da se prijavite na :appName tokom budućih posjeta.',
|
||||
'user_invite_page_confirm_button' => 'Potvrdi lozinku',
|
||||
'user_invite_success' => 'Lozinka postavljena, sada imate pristup :sppName!'
|
||||
];
|
85
resources/lang/bs/common.php
Normal file
85
resources/lang/bs/common.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Common elements found throughout many areas of BookStack.
|
||||
*/
|
||||
return [
|
||||
|
||||
// Buttons
|
||||
'cancel' => 'Otkaži',
|
||||
'confirm' => 'Potvrdi',
|
||||
'back' => 'Nazad',
|
||||
'save' => 'Spremi',
|
||||
'continue' => 'Nastavi',
|
||||
'select' => 'Odaberi',
|
||||
'toggle_all' => 'Prebaci sve',
|
||||
'more' => 'Više',
|
||||
|
||||
// Form Labels
|
||||
'name' => 'Ime',
|
||||
'description' => 'Opis',
|
||||
'role' => 'Uloga',
|
||||
'cover_image' => 'Naslovna slika',
|
||||
'cover_image_description' => 'Ova slika treba biti približno 440x250px.',
|
||||
|
||||
// Actions
|
||||
'actions' => 'Akcije',
|
||||
'view' => 'Prikaz',
|
||||
'view_all' => 'Prikaži sve',
|
||||
'create' => 'Kreiraj',
|
||||
'update' => 'Ažuriraj',
|
||||
'edit' => 'Uredi',
|
||||
'sort' => 'Sortiraj',
|
||||
'move' => 'Pomjeri',
|
||||
'copy' => 'Kopiraj',
|
||||
'reply' => 'Odgovori',
|
||||
'delete' => 'Izbriši',
|
||||
'delete_confirm' => 'Potvrdi brisanje',
|
||||
'search' => 'Traži',
|
||||
'search_clear' => 'Očisti pretragu',
|
||||
'reset' => 'Resetuj',
|
||||
'remove' => 'Ukloni',
|
||||
'add' => 'Dodaj',
|
||||
'fullscreen' => 'Prikaz preko čitavog ekrana',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Opcije sortiranja',
|
||||
'sort_direction_toggle' => 'Prebacivanje smjera sortiranja',
|
||||
'sort_ascending' => 'Sortiraj uzlazno',
|
||||
'sort_descending' => 'Sortiraj silazno',
|
||||
'sort_name' => 'Ime',
|
||||
'sort_created_at' => 'Datum kreiranja',
|
||||
'sort_updated_at' => 'Datum ažuriranja',
|
||||
|
||||
// Misc
|
||||
'deleted_user' => 'Obrisani korisnik',
|
||||
'no_activity' => 'Nema aktivnosti za prikazivanje',
|
||||
'no_items' => 'Nema dostupnih stavki',
|
||||
'back_to_top' => 'Povratak na vrh',
|
||||
'toggle_details' => 'Vidi detalje',
|
||||
'toggle_thumbnails' => 'Vidi prikaze slika',
|
||||
'details' => 'Detalji',
|
||||
'grid_view' => 'Prikaz rešetke',
|
||||
'list_view' => 'Prikaz liste',
|
||||
'default' => 'Početne postavke',
|
||||
'breadcrumb' => 'Navigacijske stavke',
|
||||
|
||||
// Header
|
||||
'profile_menu' => 'Meni profila',
|
||||
'view_profile' => 'Pogledaj profil',
|
||||
'edit_profile' => 'Izmjeni profil',
|
||||
'dark_mode' => 'Tamni način rada',
|
||||
'light_mode' => 'Svijetli način rada',
|
||||
|
||||
// Layout tabs
|
||||
'tab_info' => 'Informacije',
|
||||
'tab_content' => 'Sadržaj',
|
||||
|
||||
// Email Content
|
||||
'email_action_help' => 'Ukoliko imate poteškoća sa pritiskom na ":actionText" dugme, kopirajte i zaljepite URL koji se nalazi ispod u vaš web pretraživač:',
|
||||
'email_rights' => 'Sva prava pridržana',
|
||||
|
||||
// Footer Link Options
|
||||
// Not directly used but available for convenience to users.
|
||||
'privacy_policy' => 'Pravila o privatnosti',
|
||||
'terms_of_service' => 'Uslovi korištenja',
|
||||
];
|
34
resources/lang/bs/components.php
Normal file
34
resources/lang/bs/components.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Text used in custom JavaScript driven components.
|
||||
*/
|
||||
return [
|
||||
|
||||
// Image Manager
|
||||
'image_select' => 'Biraj sliku',
|
||||
'image_all' => 'Sve',
|
||||
'image_all_title' => 'Pogledaj sve slike',
|
||||
'image_book_title' => 'Pogledaj slike prenesene u ovu knjigu',
|
||||
'image_page_title' => 'Pogledaj slike prenesene na ovu stranicu',
|
||||
'image_search_hint' => 'Traži po nazivu slike',
|
||||
'image_uploaded' => 'Preneseno :uploadedDate',
|
||||
'image_load_more' => 'Učitaj još',
|
||||
'image_image_name' => 'Naziv slike',
|
||||
'image_delete_used' => 'Ova slika se koristi na stranicama prikazanim ispod.',
|
||||
'image_delete_confirm_text' => 'Jeste li sigurni da želite obrisati ovu sliku?',
|
||||
'image_select_image' => 'Odaberi sliku',
|
||||
'image_dropzone' => 'Ostavi slike ili pritisnite ovdje da ih prenesete',
|
||||
'images_deleted' => 'Slike su izbrisane',
|
||||
'image_preview' => 'Pregled Slike',
|
||||
'image_upload_success' => 'Slika uspješno učitana',
|
||||
'image_update_success' => 'Detalji slike uspješno ažurirani',
|
||||
'image_delete_success' => 'Slika uspješno izbrisana',
|
||||
'image_upload_remove' => 'Ukloni',
|
||||
|
||||
// Code Editor
|
||||
'code_editor' => 'Uredi Kod',
|
||||
'code_language' => 'Jezik koda',
|
||||
'code_content' => 'Sadržaj Koda',
|
||||
'code_session_history' => 'Historija Sesije',
|
||||
'code_save' => 'Snimi Kod',
|
||||
];
|
319
resources/lang/bs/entities.php
Normal file
319
resources/lang/bs/entities.php
Normal file
@ -0,0 +1,319 @@
|
||||
<?php
|
||||
/**
|
||||
* Text used for 'Entities' (Document Structure Elements) such as
|
||||
* Books, Shelves, Chapters & Pages
|
||||
*/
|
||||
return [
|
||||
|
||||
// Shared
|
||||
'recently_created' => 'Nedavno napravljen',
|
||||
'recently_created_pages' => 'Nedavno napravljene stranice',
|
||||
'recently_updated_pages' => 'Nedavno ažurirane stranice',
|
||||
'recently_created_chapters' => 'Nedavno napravljena poglavlja',
|
||||
'recently_created_books' => 'Nedavno napravljene knjige',
|
||||
'recently_created_shelves' => 'Nedavno napravljene police',
|
||||
'recently_update' => 'Nedavno ažurirana',
|
||||
'recently_viewed' => 'Nedavno pogledana',
|
||||
'recent_activity' => 'Nedavna aktivnost',
|
||||
'create_now' => 'Napravi jednu sada',
|
||||
'revisions' => 'Promjene',
|
||||
'meta_revision' => 'Promjena #:revisionCount',
|
||||
'meta_created' => 'Napravljena :timeLength',
|
||||
'meta_created_name' => 'Napravljena :timeLength od :user',
|
||||
'meta_updated' => 'Ažurirana :timeLength',
|
||||
'meta_updated_name' => 'Ažurirana :timeLength od :user',
|
||||
'meta_owned_name' => 'Vlasnik je :user',
|
||||
'entity_select' => 'Odaberi entitet',
|
||||
'images' => 'Slike',
|
||||
'my_recent_drafts' => 'Moje nedavne skice',
|
||||
'my_recently_viewed' => 'Moji nedavni pregledi',
|
||||
'no_pages_viewed' => 'Niste pogledali nijednu stranicu',
|
||||
'no_pages_recently_created' => 'Nijedna stranica nije napravljena nedavno',
|
||||
'no_pages_recently_updated' => 'Niijedna stranica nije ažurirana nedavno',
|
||||
'export' => 'Izvezi',
|
||||
'export_html' => 'Sadržani web fajl',
|
||||
'export_pdf' => 'PDF fajl',
|
||||
'export_text' => 'Plain Text fajl',
|
||||
|
||||
// Permissions and restrictions
|
||||
'permissions' => 'Dozvole',
|
||||
'permissions_intro' => 'Jednom omogućene, ove dozvole imaju prednost nad dozvolama uloge.',
|
||||
'permissions_enable' => 'Omogući prilagođena dopuštenja',
|
||||
'permissions_save' => 'Snimi dozvole',
|
||||
'permissions_owner' => 'Vlasnik',
|
||||
|
||||
// Search
|
||||
'search_results' => 'Rezultati pretrage',
|
||||
'search_total_results_found' => ':count rezultata je nađeno|:count ukupno rezultata je nađeno',
|
||||
'search_clear' => 'Očisti pretragu',
|
||||
'search_no_pages' => 'Nijedna stranica nije nađena',
|
||||
'search_for_term' => 'Traži :term',
|
||||
'search_more' => 'Više rezultata',
|
||||
'search_advanced' => 'Napredna pretraga',
|
||||
'search_terms' => 'Pojmovi za pretragu',
|
||||
'search_content_type' => 'Vrsta sadržaja',
|
||||
'search_exact_matches' => 'Tačna podudaranja',
|
||||
'search_tags' => 'Pretraga oznaka',
|
||||
'search_options' => 'Opcije',
|
||||
'search_viewed_by_me' => 'Ja sam pogledao/la',
|
||||
'search_not_viewed_by_me' => 'Nisam pogledao/la',
|
||||
'search_permissions_set' => 'Dozvole',
|
||||
'search_created_by_me' => 'Ja sam napravio/la',
|
||||
'search_updated_by_me' => 'Ja sam ažurirao/la',
|
||||
'search_date_options' => 'Opcije datuma',
|
||||
'search_updated_before' => 'Ažurirano prije',
|
||||
'search_updated_after' => 'Ažurirano nakon',
|
||||
'search_created_before' => 'Kreirano prije',
|
||||
'search_created_after' => 'Kreirano nakon',
|
||||
'search_set_date' => 'Postavi datum',
|
||||
'search_update' => 'Ažuriraj pretragu',
|
||||
|
||||
// Shelves
|
||||
'shelf' => 'Polica',
|
||||
'shelves' => 'Police',
|
||||
'x_shelves' => ':count Polica|:count Police',
|
||||
'shelves_long' => 'Police za knjige',
|
||||
'shelves_empty' => 'Niti jedna polica nije kreirana',
|
||||
'shelves_create' => 'Kreiraj novu policu',
|
||||
'shelves_popular' => 'Popularne police',
|
||||
'shelves_new' => 'Nove police',
|
||||
'shelves_new_action' => 'Nova polica',
|
||||
'shelves_popular_empty' => 'Najpopularnije police će se pojaviti ovdje.',
|
||||
'shelves_new_empty' => 'Najnovije police će se pojaviti ovdje.',
|
||||
'shelves_save' => 'Spremi policu',
|
||||
'shelves_books' => 'Knjige na ovoj polici',
|
||||
'shelves_add_books' => 'Dodaj knjige na ovu policu',
|
||||
'shelves_drag_books' => 'Prenesi knjige ovdje da bi ih dodao/la na ovu policu',
|
||||
'shelves_empty_contents' => 'Ova polica nema knjiga koje su postavljene na nju',
|
||||
'shelves_edit_and_assign' => 'Uredi policu da bi dodao/la knjige',
|
||||
'shelves_edit_named' => 'Uredi :name police za knjige',
|
||||
'shelves_edit' => 'Uredi policu za knjige',
|
||||
'shelves_delete' => 'Izbriši policu za knjige',
|
||||
'shelves_delete_named' => 'Izbriši policu za knjige :name',
|
||||
'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
|
||||
'shelves_permissions' => 'Bookshelf Permissions',
|
||||
'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Bookshelf Permissions Active',
|
||||
'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
|
||||
'shelves_copy_permissions' => 'Copy Permissions',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Book',
|
||||
'books' => 'Books',
|
||||
'x_books' => ':count Book|:count Books',
|
||||
'books_empty' => 'No books have been created',
|
||||
'books_popular' => 'Popular Books',
|
||||
'books_recent' => 'Recent Books',
|
||||
'books_new' => 'New Books',
|
||||
'books_new_action' => 'New Book',
|
||||
'books_popular_empty' => 'The most popular books will appear here.',
|
||||
'books_new_empty' => 'The most recently created books will appear here.',
|
||||
'books_create' => 'Create New Book',
|
||||
'books_delete' => 'Delete Book',
|
||||
'books_delete_named' => 'Delete Book :bookName',
|
||||
'books_delete_explain' => 'Ovo će izbrisati knjigu naziva \':bookName\'. Sve stranice i poglavlja će biti uklonjene.',
|
||||
'books_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovu knjigu?',
|
||||
'books_edit' => 'Uredi knjigu',
|
||||
'books_edit_named' => 'Uredi knjigu :bookName',
|
||||
'books_form_book_name' => 'Naziv knjige',
|
||||
'books_save' => 'Spremi knjigu',
|
||||
'books_permissions' => 'Dozvole knjige',
|
||||
'books_permissions_updated' => 'Dozvole knjige su ažurirane',
|
||||
'books_empty_contents' => 'Za ovu knjigu nisu napravljene ni stranice ni poglavlja.',
|
||||
'books_empty_create_page' => 'Napravi novu stranicu',
|
||||
'books_empty_sort_current_book' => 'Sortiraj trenutnu knjigu',
|
||||
'books_empty_add_chapter' => 'Dodaj poglavlje',
|
||||
'books_permissions_active' => 'Dozvole za knjigu su aktivne',
|
||||
'books_search_this' => 'Pretraži ovu knjigu',
|
||||
'books_navigation' => 'Navigacija knjige',
|
||||
'books_sort' => 'Sortiraj sadržaj knjige',
|
||||
'books_sort_named' => 'Sortiraj knjigu :bookName',
|
||||
'books_sort_name' => 'Sortiraj po imenu',
|
||||
'books_sort_created' => 'Sortiraj po datumu kreiranja',
|
||||
'books_sort_updated' => 'Sortiraj po datumu ažuriranja',
|
||||
'books_sort_chapters_first' => 'Poglavlja prva',
|
||||
'books_sort_chapters_last' => 'Poglavlja zadnja',
|
||||
'books_sort_show_other' => 'Prikaži druge knjige',
|
||||
'books_sort_save' => 'Spremi trenutni poredak',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Poglavlje',
|
||||
'chapters' => 'Poglavlja',
|
||||
'x_chapters' => ':count Poglavlje|:count Poglavlja',
|
||||
'chapters_popular' => 'Popularna poglavlja',
|
||||
'chapters_new' => 'Novo poglavlje',
|
||||
'chapters_create' => 'Napravi novo poglavlje',
|
||||
'chapters_delete' => 'Izbriši poglavlje',
|
||||
'chapters_delete_named' => 'Izbriši poglavlje :chapterName',
|
||||
'chapters_delete_explain' => 'Ovo će izbrisati poglavlje naziva \':chapterName\'. Sve stranice koje postoje u ovom poglavlju će također biti izbrisane.',
|
||||
'chapters_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovo poglavlje?',
|
||||
'chapters_edit' => 'Uredi poglavlje',
|
||||
'chapters_edit_named' => 'Uredi poglavlje :chapterName',
|
||||
'chapters_save' => 'Spremi poglavlje',
|
||||
'chapters_move' => 'Premjesti poglavlje',
|
||||
'chapters_move_named' => 'Premjesti poglavlje :chapterName',
|
||||
'chapter_move_success' => 'Poglavlje premješteno u :bookName',
|
||||
'chapters_permissions' => 'Dozvole poglavlja',
|
||||
'chapters_empty' => 'U ovom poglavlju trenutno nema stranica.',
|
||||
'chapters_permissions_active' => 'Dozvole za poglavlje su aktivne',
|
||||
'chapters_permissions_success' => 'Dozvole za poglavlje su ažurirane',
|
||||
'chapters_search_this' => 'Pretražuj ovo poglavlje',
|
||||
|
||||
// Pages
|
||||
'page' => 'Stranica',
|
||||
'pages' => 'Stranice',
|
||||
'x_pages' => ':count Stranica|:count Stranice',
|
||||
'pages_popular' => 'Popularne stranice',
|
||||
'pages_new' => 'Nova stranica',
|
||||
'pages_attachments' => 'Attachments',
|
||||
'pages_navigation' => 'Page Navigation',
|
||||
'pages_delete' => 'Delete Page',
|
||||
'pages_delete_named' => 'Delete Page :pageName',
|
||||
'pages_delete_draft_named' => 'Delete Draft Page :pageName',
|
||||
'pages_delete_draft' => 'Delete Draft Page',
|
||||
'pages_delete_success' => 'Page deleted',
|
||||
'pages_delete_draft_success' => 'Draft page deleted',
|
||||
'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',
|
||||
'pages_editing_page' => 'Editing Page',
|
||||
'pages_edit_draft_save_at' => 'Draft saved at ',
|
||||
'pages_edit_delete_draft' => 'Delete Draft',
|
||||
'pages_edit_discard_draft' => 'Discard Draft',
|
||||
'pages_edit_set_changelog' => 'Set Changelog',
|
||||
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
|
||||
'pages_edit_enter_changelog' => 'Enter Changelog',
|
||||
'pages_save' => 'Save Page',
|
||||
'pages_title' => 'Page Title',
|
||||
'pages_name' => 'Page Name',
|
||||
'pages_md_editor' => 'Editor',
|
||||
'pages_md_preview' => 'Preview',
|
||||
'pages_md_insert_image' => 'Insert Image',
|
||||
'pages_md_insert_link' => 'Insert Entity Link',
|
||||
'pages_md_insert_drawing' => 'Insert Drawing',
|
||||
'pages_not_in_chapter' => 'Page is not in a chapter',
|
||||
'pages_move' => 'Move Page',
|
||||
'pages_move_success' => 'Page moved to ":parentName"',
|
||||
'pages_copy' => 'Copy Page',
|
||||
'pages_copy_desination' => 'Copy Destination',
|
||||
'pages_copy_success' => 'Page successfully copied',
|
||||
'pages_permissions' => 'Page Permissions',
|
||||
'pages_permissions_success' => 'Page permissions updated',
|
||||
'pages_revision' => 'Revision',
|
||||
'pages_revisions' => 'Page Revisions',
|
||||
'pages_revisions_named' => 'Page Revisions for :pageName',
|
||||
'pages_revision_named' => 'Page Revision for :pageName',
|
||||
'pages_revision_restored_from' => 'Restored from #:id; :summary',
|
||||
'pages_revisions_created_by' => 'Created By',
|
||||
'pages_revisions_date' => 'Revision Date',
|
||||
'pages_revisions_number' => '#',
|
||||
'pages_revisions_numbered' => 'Revision #:id',
|
||||
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
|
||||
'pages_revisions_changelog' => 'Changelog',
|
||||
'pages_revisions_changes' => 'Changes',
|
||||
'pages_revisions_current' => 'Trenutna verzija',
|
||||
'pages_revisions_preview' => 'Pregled',
|
||||
'pages_revisions_restore' => 'Vrati',
|
||||
'pages_revisions_none' => 'Ova stranica nema promjena',
|
||||
'pages_copy_link' => 'Iskopiraj link',
|
||||
'pages_edit_content_link' => 'Uredi sadržaj',
|
||||
'pages_permissions_active' => 'Dozvole za stranicu su aktivne',
|
||||
'pages_initial_revision' => 'Prvo izdavanje',
|
||||
'pages_initial_name' => 'Nova stranica',
|
||||
'pages_editing_draft_notification' => 'Trenutno uređujete skicu koja je posljednji put snimljena :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Ova stranica je ažurirana nakon tog vremena. Preporučujemo da odbacite ovu skicu.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count korisnika je počelo sa uređivanjem ove stranice',
|
||||
'start_b' => ':userName je počeo/la sa uređivanjem ove stranice',
|
||||
'time_a' => 'od kada je stranica posljednji put ažurirana',
|
||||
'time_b' => 'u posljednjih :minCount minuta',
|
||||
'message' => ':start :time. Pazite da jedni drugima ne prepišete promjene!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Skica je odbačena, uređivač je ažuriran sa trenutnim sadržajem stranice',
|
||||
'pages_specific' => 'Specifična stranica',
|
||||
'pages_is_template' => 'Predložak stranice',
|
||||
|
||||
// Editor Sidebar
|
||||
'page_tags' => 'Oznake stranice',
|
||||
'chapter_tags' => 'Oznake poglavlja',
|
||||
'book_tags' => 'Oznake knjige',
|
||||
'shelf_tags' => 'Oznake police',
|
||||
'tag' => 'Oznaka',
|
||||
'tags' => 'Oznake',
|
||||
'tag_name' => 'Naziv oznake',
|
||||
'tag_value' => 'Vrijednost oznake (nije obavezno)',
|
||||
'tags_explain' => "Dodaj nekoliko oznaka da bi sadržaj bio bolje kategorisan. \n Možeš dodati vrijednost oznaci za dublju organizaciju.",
|
||||
'tags_add' => 'Dodaj još jednu oznaku',
|
||||
'tags_remove' => 'Ukloni ovu oznaku',
|
||||
'attachments' => 'Prilozi',
|
||||
'attachments_explain' => 'Učitajte fajlove ili priložite poveznice da bi ih prikazali na stranici. Oni su onda vidljivi u navigaciji sa strane.',
|
||||
'attachments_explain_instant_save' => 'Sve promjene se snimaju odmah.',
|
||||
'attachments_items' => 'Priložene stavke',
|
||||
'attachments_upload' => 'Učitaj fajl',
|
||||
'attachments_link' => 'Zakači link',
|
||||
'attachments_set_link' => 'Postavi link',
|
||||
'attachments_delete' => 'Jeste li sigurni da želite obrisati ovaj prilog?',
|
||||
'attachments_dropzone' => 'Spustite fajlove ili pritisnite ovdje da priložite fajl',
|
||||
'attachments_no_files' => 'Niti jedan fajl nije prenesen',
|
||||
'attachments_explain_link' => 'Možete zakačiti link ako ne želite učitati fajl. To može biti link druge stranice ili link za fajl u oblaku.',
|
||||
'attachments_link_name' => 'Naziv linka',
|
||||
'attachment_link' => 'Link poveznice',
|
||||
'attachments_link_url' => 'Link do fajla',
|
||||
'attachments_link_url_hint' => 'Url stranice ili fajla',
|
||||
'attach' => 'Zakači',
|
||||
'attachments_insert_link' => 'Dodaj priloženi link na stranicu',
|
||||
'attachments_edit_file' => 'Uredi fajl',
|
||||
'attachments_edit_file_name' => 'Naziv fajla',
|
||||
'attachments_edit_drop_upload' => 'Spusti fajlove ili pritisni ovdje da učitaš i prepišeš',
|
||||
'attachments_order_updated' => 'Attachment order updated',
|
||||
'attachments_updated_success' => 'Attachment details updated',
|
||||
'attachments_deleted' => 'Attachment deleted',
|
||||
'attachments_file_uploaded' => 'File successfully uploaded',
|
||||
'attachments_file_updated' => 'File successfully updated',
|
||||
'attachments_link_attached' => 'Link successfully attached to page',
|
||||
'templates' => 'Templates',
|
||||
'templates_set_as_template' => 'Page is a template',
|
||||
'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' => 'Replace page content',
|
||||
'templates_append_content' => 'Append to page content',
|
||||
'templates_prepend_content' => 'Prepend to page content',
|
||||
|
||||
// Profile View
|
||||
'profile_user_for_x' => 'User for :time',
|
||||
'profile_created_content' => 'Created Content',
|
||||
'profile_not_created_pages' => ':userName has not created any pages',
|
||||
'profile_not_created_chapters' => ':userName has not created any chapters',
|
||||
'profile_not_created_books' => ':userName has not created any books',
|
||||
'profile_not_created_shelves' => ':userName has not created any shelves',
|
||||
|
||||
// Comments
|
||||
'comment' => 'Comment',
|
||||
'comments' => 'Comments',
|
||||
'comment_add' => 'Add Comment',
|
||||
'comment_placeholder' => 'Leave a comment here',
|
||||
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
|
||||
'comment_save' => 'Save Comment',
|
||||
'comment_saving' => 'Saving comment...',
|
||||
'comment_deleting' => 'Deleting comment...',
|
||||
'comment_new' => 'New Comment',
|
||||
'comment_created' => 'commented :createDiff',
|
||||
'comment_updated' => 'Updated :updateDiff by :username',
|
||||
'comment_deleted_success' => 'Comment deleted',
|
||||
'comment_created_success' => 'Comment added',
|
||||
'comment_updated_success' => 'Comment updated',
|
||||
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
|
||||
'comment_in_reply_to' => 'In reply to :commentId',
|
||||
|
||||
// Revision
|
||||
'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
|
||||
'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
|
||||
'revision_delete_success' => 'Revision deleted',
|
||||
'revision_cannot_delete_latest' => 'Cannot delete the latest revision.'
|
||||
];
|
102
resources/lang/bs/errors.php
Normal file
102
resources/lang/bs/errors.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
/**
|
||||
* Text shown in error messaging.
|
||||
*/
|
||||
return [
|
||||
|
||||
// Permissions
|
||||
'permission' => 'Nemate ovlaštenje da pristupite ovoj stranici.',
|
||||
'permissionJson' => 'Nemate ovlaštenje da izvršite tu akciju.',
|
||||
|
||||
// Auth
|
||||
'error_user_exists_different_creds' => 'Korisnik sa e-mailom :email već postoji ali sa različitim podacima.',
|
||||
'email_already_confirmed' => 'E-mail je već potvrđen, pokušajte se prijaviti.',
|
||||
'email_confirmation_invalid' => 'Ovaj token za potvrdu nije ispravan ili je već iskorišten, molimo vas pokušajte se registrovati ponovno.',
|
||||
'email_confirmation_expired' => 'Ovaj token za potvrdu je istekao, novi e-mail za potvrdu je poslan.',
|
||||
'email_confirmation_awaiting' => 'E-mail adresa za račun koji se koristi mora biti potvrđena',
|
||||
'ldap_fail_anonymous' => 'LDAP pristup nije uspio koristeći anonimno povezivanje',
|
||||
'ldap_fail_authed' => 'LDAP pristup nije uspio koristeći date detalje lozinke i dn',
|
||||
'ldap_extension_not_installed' => 'LDAP PHP ekstenzija nije instalirana',
|
||||
'ldap_cannot_connect' => 'Nije se moguće povezati sa ldap serverom, incijalna konekcija nije uspjela',
|
||||
'saml_already_logged_in' => 'Već prijavljeni',
|
||||
'saml_user_not_registered' => 'Korisnik :user nije registrovan i automatska registracija je onemogućena',
|
||||
'saml_no_email_address' => 'E-mail adresa za ovog korisnika nije nađena u podacima dobijenim od eksternog autentifikacijskog sistema',
|
||||
'saml_invalid_response_id' => 'Proces, koji je pokrenula ova aplikacija, nije prepoznao zahtjev od eksternog sistema za autentifikaciju. Navigacija nazad nakon prijave može uzrokovati ovaj problem.',
|
||||
'saml_fail_authed' => 'Prijava koristeći :system nije uspjela, sistem nije obezbijedio uspješnu autorizaciju',
|
||||
'social_no_action_defined' => 'Nema definisane akcije',
|
||||
'social_login_bad_response' => "Došlo je do greške prilikom prijave preko :socialAccount :\n:error",
|
||||
'social_account_in_use' => 'Ovaj :socialAccount račun se već koristi, pokušajte se prijaviti putem :socialAccount opcije.',
|
||||
'social_account_email_in_use' => 'E-mail :email se već koristi. Ako već imate račun možete povezati vaš :socialAccount račun u postavkama profila.',
|
||||
'social_account_existing' => 'Ovaj :socialAccount je već povezan sa vašim profilom.',
|
||||
'social_account_already_used_existing' => 'Drugi korisnik već koristi ovaj :socialAccount.',
|
||||
'social_account_not_used' => 'Ovaj :socialAccount nije povezan ni sa jednim korisnikom. Povežite ga u postavkama profila. ',
|
||||
'social_account_register_instructions' => 'Ako još uvijek nemate račun, možete se registrovati koristeći :socialAccount opciju.',
|
||||
'social_driver_not_found' => 'Driver društvene mreže nije pronađen',
|
||||
'social_driver_not_configured' => 'Vaše :socialAccount postavke nisu konfigurisane ispravno.',
|
||||
'invite_token_expired' => 'Pozivni link je istekao. Možete umjesto toga pokušati da resetujete lozinku.',
|
||||
|
||||
// System
|
||||
'path_not_writable' => 'Na putanju fajla :filePath se ne može učitati. Potvrdite da je omogućeno pisanje na server.',
|
||||
'cannot_get_image_from_url' => 'Nije moguće dobiti sliku sa :url',
|
||||
'cannot_create_thumbs' => 'Server ne može kreirati sličice. Provjerite da imate instaliranu GD PHP ekstenziju.',
|
||||
'server_upload_limit' => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',
|
||||
'uploaded' => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',
|
||||
'image_upload_error' => 'Desila se greška prilikom učitavanja slike',
|
||||
'image_upload_type_error' => 'Vrsta slike koja se učitava je neispravna',
|
||||
'file_upload_timeout' => 'Vrijeme učitavanja fajla je isteklo.',
|
||||
|
||||
// Attachments
|
||||
'attachment_not_found' => 'Prilog nije pronađen',
|
||||
|
||||
// Pages
|
||||
'page_draft_autosave_fail' => 'Snimanje skice nije uspjelo. Provjerite da ste povezani na internet prije snimanja ove stranice',
|
||||
'page_custom_home_deletion' => 'Stranicu nije moguće izbrisati dok se koristi kao početna stranica',
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Entitet nije pronađen',
|
||||
'bookshelf_not_found' => 'Polica za knjige nije pronađena',
|
||||
'book_not_found' => 'Knjiga nije pronađena',
|
||||
'page_not_found' => 'Stranica nije pronađena',
|
||||
'chapter_not_found' => 'Poglavlje nije pronađeno',
|
||||
'selected_book_not_found' => 'Odabrana knjiga nije pronađena',
|
||||
'selected_book_chapter_not_found' => 'Odabrana knjiga ili poglavlje nije pronađeno',
|
||||
'guests_cannot_save_drafts' => 'Gosti ne mogu snimati skice',
|
||||
|
||||
// Users
|
||||
'users_cannot_delete_only_admin' => 'Ne možete izbrisati jedinog administratora',
|
||||
'users_cannot_delete_guest' => 'Ne možete izbrisati gost korisnika',
|
||||
|
||||
// Roles
|
||||
'role_cannot_be_edited' => 'Ova uloga ne može biti mijenjana',
|
||||
'role_system_cannot_be_deleted' => 'Ova uloga je sistemska uloga i ne može biti izbrisana',
|
||||
'role_registration_default_cannot_delete' => 'Ova uloga ne može biti izbrisana dok je postavljena kao osnovna registracijska uloga',
|
||||
'role_cannot_remove_only_admin' => 'Ovaj korisnik je jedini korisnik sa ulogom administratora. Postavite ulogu administratora drugom korisniku prije nego je uklonite ovdje.',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Desila se greška prilikom dobavljanja komentara.',
|
||||
'cannot_add_comment_to_draft' => 'Ne možete dodati komentare na skicu.',
|
||||
'comment_add' => 'Desila se greška prilikom dodavanja / ažuriranja komentara.',
|
||||
'comment_delete' => 'Desila se greška prilikom brisanja komentara.',
|
||||
'empty_comment' => 'Nemoguće dodati prazan komentar.',
|
||||
|
||||
// Error pages
|
||||
'404_page_not_found' => 'Stranica nije pronađena',
|
||||
'sorry_page_not_found' => 'Stranica koju ste tražili nije pronađena.',
|
||||
'sorry_page_not_found_permission_warning' => 'Ako ste očekivali da ova stranica postoji, možda nemate privilegije da joj pristupite.',
|
||||
'return_home' => 'Nazad na početnu stranu',
|
||||
'error_occurred' => 'Desila se greška',
|
||||
'app_down' => ':appName trenutno nije u funkciji',
|
||||
'back_soon' => 'Biti će uskoro u funkciji.',
|
||||
|
||||
// API errors
|
||||
'api_no_authorization_found' => 'Na zahtjevu nije pronađen token za autorizaciju',
|
||||
'api_bad_authorization_format' => 'Token za autorizaciju je pronađen u zahtjevu ali je format neispravan',
|
||||
'api_user_token_not_found' => 'Nije pronađen odgovarajući API token za pruženi token autorizacije',
|
||||
'api_incorrect_token_secret' => 'Tajni ključ naveden za dati korišteni API token nije tačan',
|
||||
'api_user_no_api_permission' => 'Vlasnik korištenog API tokena nema dozvolu za upućivanje API poziva',
|
||||
'api_user_token_expired' => 'Autorizacijski token je istekao',
|
||||
|
||||
// Settings & Maintenance
|
||||
'maintenance_test_email_failure' => 'Došlo je do greške prilikom slanja testnog e-maila:',
|
||||
|
||||
];
|
12
resources/lang/bs/pagination.php
Normal file
12
resources/lang/bs/pagination.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* Pagination Language Lines
|
||||
* The following language lines are used by the paginator library to build
|
||||
* the simple pagination links.
|
||||
*/
|
||||
return [
|
||||
|
||||
'previous' => '« Prethodna',
|
||||
'next' => 'Sljedeća »',
|
||||
|
||||
];
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user