mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-20 20:31:25 +08:00
Merge branch 'development' into release
This commit is contained in:
@ -263,7 +263,11 @@ OIDC_ISSUER_DISCOVER=false
|
||||
OIDC_PUBLIC_KEY=null
|
||||
OIDC_AUTH_ENDPOINT=null
|
||||
OIDC_TOKEN_ENDPOINT=null
|
||||
OIDC_ADDITIONAL_SCOPES=null
|
||||
OIDC_DUMP_USER_DETAILS=false
|
||||
OIDC_USER_TO_GROUPS=false
|
||||
OIDC_GROUPS_CLAIM=groups
|
||||
OIDC_REMOVE_FROM_GROUPS=false
|
||||
|
||||
# Disable default third-party services such as Gravatar and Draw.IO
|
||||
# Service-specific options will override this option
|
||||
@ -295,7 +299,7 @@ APP_DEFAULT_DARK_MODE=false
|
||||
# Page revision limit
|
||||
# Number of page revisions to keep in the system before deleting old revisions.
|
||||
# If set to 'false' a limit will not be enforced.
|
||||
REVISION_LIMIT=50
|
||||
REVISION_LIMIT=100
|
||||
|
||||
# Recycle Bin Lifetime
|
||||
# The number of days that content will remain in the recycle bin before
|
||||
|
6
.github/translators.txt
vendored
6
.github/translators.txt
vendored
@ -138,7 +138,7 @@ Xiphoseer :: German
|
||||
MerlinSVK (merlinsvk) :: Slovak
|
||||
Kauê Sena (kaue.sena.ks) :: Portuguese, Brazilian
|
||||
MatthieuParis :: French
|
||||
Douradinho :: Portuguese, Brazilian
|
||||
Douradinho :: Portuguese, Brazilian; Portuguese
|
||||
Gaku Yaguchi (tama11) :: Japanese
|
||||
johnroyer :: Chinese Traditional
|
||||
jackaaa :: Chinese Traditional
|
||||
@ -270,3 +270,7 @@ Nanang Setia Budi (sefidananang) :: Indonesian
|
||||
Андрей Павлов (andrei.pavlov) :: Russian
|
||||
Alex Navarro (alex.n.navarro) :: Portuguese, Brazilian
|
||||
Ji-Hyeon Gim (PotatoGim) :: Korean
|
||||
Mihai Ochian (soulstorm19) :: Romanian
|
||||
HeartCore :: German Informal; German
|
||||
simon.pct :: French
|
||||
okaeiz :: Persian
|
||||
|
@ -30,6 +30,11 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
*/
|
||||
protected $tokenEndpoint;
|
||||
|
||||
/**
|
||||
* Scopes to use for the OIDC authorization call.
|
||||
*/
|
||||
protected array $scopes = ['openid', 'profile', 'email'];
|
||||
|
||||
/**
|
||||
* Returns the base URL for authorizing a client.
|
||||
*/
|
||||
@ -54,6 +59,15 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an additional scope to this provider upon the default.
|
||||
*/
|
||||
public function addScope(string $scope): void
|
||||
{
|
||||
$this->scopes[] = $scope;
|
||||
$this->scopes = array_unique($this->scopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default scopes used by this provider.
|
||||
*
|
||||
@ -62,7 +76,7 @@ class OidcOAuthProvider extends AbstractProvider
|
||||
*/
|
||||
protected function getDefaultScopes(): array
|
||||
{
|
||||
return ['openid', 'profile', 'email'];
|
||||
return $this->scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace BookStack\Auth\Access\Oidc;
|
||||
|
||||
use function auth;
|
||||
use BookStack\Auth\Access\GroupSyncService;
|
||||
use BookStack\Auth\Access\LoginService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
@ -10,6 +11,7 @@ use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use function config;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
@ -26,15 +28,21 @@ class OidcService
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
protected HttpClient $httpClient;
|
||||
protected GroupSyncService $groupService;
|
||||
|
||||
/**
|
||||
* OpenIdService constructor.
|
||||
*/
|
||||
public function __construct(RegistrationService $registrationService, LoginService $loginService, HttpClient $httpClient)
|
||||
{
|
||||
public function __construct(
|
||||
RegistrationService $registrationService,
|
||||
LoginService $loginService,
|
||||
HttpClient $httpClient,
|
||||
GroupSyncService $groupService
|
||||
) {
|
||||
$this->registrationService = $registrationService;
|
||||
$this->loginService = $loginService;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->groupService = $groupService;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,10 +125,31 @@ class OidcService
|
||||
*/
|
||||
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
|
||||
{
|
||||
return new OidcOAuthProvider($settings->arrayForProvider(), [
|
||||
$provider = new OidcOAuthProvider($settings->arrayForProvider(), [
|
||||
'httpClient' => $this->httpClient,
|
||||
'optionProvider' => new HttpBasicAuthOptionProvider(),
|
||||
]);
|
||||
|
||||
foreach ($this->getAdditionalScopes() as $scope) {
|
||||
$provider->addScope($scope);
|
||||
}
|
||||
|
||||
return $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any user-defined addition/custom scopes to apply to the authentication request.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getAdditionalScopes(): array
|
||||
{
|
||||
$scopeConfig = $this->config()['additional_scopes'] ?: '';
|
||||
|
||||
$scopeArr = explode(',', $scopeConfig);
|
||||
$scopeArr = array_map(fn (string $scope) => trim($scope), $scopeArr);
|
||||
|
||||
return array_filter($scopeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -145,10 +174,32 @@ class OidcService
|
||||
return implode(' ', $displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the assigned groups from the id token.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getUserGroups(OidcIdToken $token): array
|
||||
{
|
||||
$groupsAttr = $this->config()['groups_claim'];
|
||||
if (empty($groupsAttr)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupsList = Arr::get($token->getAllClaims(), $groupsAttr);
|
||||
if (!is_array($groupsList)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($groupsList, function ($val) {
|
||||
return is_string($val);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the details of a user from an ID token.
|
||||
*
|
||||
* @return array{name: string, email: string, external_id: string}
|
||||
* @return array{name: string, email: string, external_id: string, groups: string[]}
|
||||
*/
|
||||
protected function getUserDetails(OidcIdToken $token): array
|
||||
{
|
||||
@ -158,6 +209,7 @@ class OidcService
|
||||
'external_id' => $id,
|
||||
'email' => $token->getClaim('email'),
|
||||
'name' => $this->getUserDisplayName($token, $id),
|
||||
'groups' => $this->getUserGroups($token),
|
||||
];
|
||||
}
|
||||
|
||||
@ -209,6 +261,12 @@ class OidcService
|
||||
throw new OidcException($exception->getMessage());
|
||||
}
|
||||
|
||||
if ($this->shouldSyncGroups()) {
|
||||
$groups = $userDetails['groups'];
|
||||
$detachExisting = $this->config()['remove_from_groups'];
|
||||
$this->groupService->syncUserWithFoundGroups($user, $groups, $detachExisting);
|
||||
}
|
||||
|
||||
$this->loginService->login($user, 'oidc');
|
||||
|
||||
return $user;
|
||||
@ -221,4 +279,12 @@ class OidcService
|
||||
{
|
||||
return config('oidc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if groups should be synced.
|
||||
*/
|
||||
protected function shouldSyncGroups(): bool
|
||||
{
|
||||
return $this->config()['user_to_groups'] !== false;
|
||||
}
|
||||
}
|
||||
|
@ -74,9 +74,8 @@ class PermissionApplicator
|
||||
}
|
||||
|
||||
foreach ($chain as $currentEntity) {
|
||||
|
||||
if (is_null($currentEntity->restricted)) {
|
||||
throw new InvalidArgumentException("Entity restricted field used but has not been loaded");
|
||||
throw new InvalidArgumentException('Entity restricted field used but has not been loaded');
|
||||
}
|
||||
|
||||
if ($currentEntity->restricted) {
|
||||
|
@ -249,6 +249,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
$this->avatarUrl = $avatar;
|
||||
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ return [
|
||||
// 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.
|
||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
||||
'revision_limit' => env('REVISION_LIMIT', 100),
|
||||
|
||||
// The number of days that content will remain in the recycle bin before
|
||||
// being considered for auto-removal. It is not a guarantee that content will
|
||||
@ -75,7 +75,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
|
@ -32,4 +32,16 @@ return [
|
||||
// OAuth2 endpoints.
|
||||
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
|
||||
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
|
||||
|
||||
// Add extra scopes, upon those required, to the OIDC authentication request
|
||||
// Multiple values can be provided comma seperated.
|
||||
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
|
||||
|
||||
// Group sync options
|
||||
// Enable syncing, upon login, of OIDC groups to BookStack roles
|
||||
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),
|
||||
// Attribute, within a OIDC ID token, to find group names within
|
||||
'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'),
|
||||
// When syncing groups, remove any groups that no longer match. Otherwise sync only adds new groups.
|
||||
'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false),
|
||||
];
|
||||
|
@ -5,6 +5,7 @@ namespace BookStack\Console\Commands;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\CommentRepo;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RegenerateCommentContent extends Command
|
||||
{
|
||||
@ -43,9 +44,9 @@ class RegenerateCommentContent extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
$connection = DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
Comment::query()->chunk(100, function ($comments) {
|
||||
@ -55,7 +56,9 @@ class RegenerateCommentContent extends Command
|
||||
}
|
||||
});
|
||||
|
||||
\DB::setDefaultConnection($connection);
|
||||
DB::setDefaultConnection($connection);
|
||||
$this->comment('Comment HTML content has been regenerated');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -50,5 +50,7 @@ class RegeneratePermissions extends Command
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
59
app/Console/Commands/RegenerateReferences.php
Normal file
59
app/Console/Commands/RegenerateReferences.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\References\ReferenceStore;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RegenerateReferences extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-references {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Regenerate all the cross-item model reference index';
|
||||
|
||||
protected ReferenceStore $references;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ReferenceStore $references)
|
||||
{
|
||||
$this->references = $references;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = DB::getDefaultConnection();
|
||||
|
||||
if ($this->option('database')) {
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
$this->references->updateForAllPages();
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
|
||||
$this->comment('References have been regenerated');
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Search\SearchIndex;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\References\ReferenceUpdater;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@ -57,11 +58,16 @@ abstract class BookChild extends Entity
|
||||
*/
|
||||
public function changeBook(int $newBookId): Entity
|
||||
{
|
||||
$oldUrl = $this->getUrl();
|
||||
$this->book_id = $newBookId;
|
||||
$this->refreshSlug();
|
||||
$this->save();
|
||||
$this->refresh();
|
||||
|
||||
if ($oldUrl !== $this->getUrl()) {
|
||||
app()->make(ReferenceUpdater::class)->updateEntityPageReferences($this, $oldUrl);
|
||||
}
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if ($this instanceof Chapter) {
|
||||
foreach ($this->pages()->withTrashed()->get() as $page) {
|
||||
|
@ -86,7 +86,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
*/
|
||||
public function coverImageTypeKey(): string
|
||||
{
|
||||
return 'cover_shelf';
|
||||
return 'cover_bookshelf';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,7 +11,6 @@ use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\JointPermissionBuilder;
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Interfaces\Deletable;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
@ -19,6 +18,9 @@ use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use BookStack\References\Reference;
|
||||
use BookStack\Search\SearchIndex;
|
||||
use BookStack\Search\SearchTerm;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Carbon\Carbon;
|
||||
@ -202,6 +204,22 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
return $this->morphMany(Deletion::class, 'deletable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the references pointing from this entity to other items.
|
||||
*/
|
||||
public function referencesFrom(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Reference::class, 'from');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the references pointing to this entity from other items.
|
||||
*/
|
||||
public function referencesTo(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Reference::class, 'to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this instance or class is a certain type of entity.
|
||||
* Examples of $type are 'page', 'book', 'chapter'.
|
||||
|
@ -6,6 +6,7 @@ use BookStack\Actions\TagRepo;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\HasCoverImage;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\References\ReferenceUpdater;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
@ -13,11 +14,13 @@ class BaseRepo
|
||||
{
|
||||
protected TagRepo $tagRepo;
|
||||
protected ImageRepo $imageRepo;
|
||||
protected ReferenceUpdater $referenceUpdater;
|
||||
|
||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo, ReferenceUpdater $referenceUpdater)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
$this->imageRepo = $imageRepo;
|
||||
$this->referenceUpdater = $referenceUpdater;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,10 +51,12 @@ class BaseRepo
|
||||
*/
|
||||
public function update(Entity $entity, array $input)
|
||||
{
|
||||
$oldUrl = $entity->getUrl();
|
||||
|
||||
$entity->fill($input);
|
||||
$entity->updated_by = user()->id;
|
||||
|
||||
if ($entity->isDirty('name')) {
|
||||
if ($entity->isDirty('name') || empty($entity->slug)) {
|
||||
$entity->refreshSlug();
|
||||
}
|
||||
|
||||
@ -64,6 +69,10 @@ class BaseRepo
|
||||
|
||||
$entity->rebuildPermissions();
|
||||
$entity->indexForSearch();
|
||||
|
||||
if ($oldUrl !== $entity->getUrl()) {
|
||||
$this->referenceUpdater->updateEntityPageReferences($entity, $oldUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,8 +86,9 @@ class BaseRepo
|
||||
public function updateCoverImage($entity, ?UploadedFile $coverImage, bool $removeImage = false)
|
||||
{
|
||||
if ($coverImage) {
|
||||
$imageType = $entity->coverImageTypeKey();
|
||||
$this->imageRepo->destroyImage($entity->cover);
|
||||
$image = $this->imageRepo->saveNew($coverImage, 'cover_book', $entity->id, 512, 512, true);
|
||||
$image = $this->imageRepo->saveNew($coverImage, $imageType, $entity->id, 512, 512, true);
|
||||
$entity->cover()->associate($image);
|
||||
$entity->save();
|
||||
}
|
||||
|
@ -16,20 +16,31 @@ use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\References\ReferenceStore;
|
||||
use BookStack\References\ReferenceUpdater;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class PageRepo
|
||||
{
|
||||
protected $baseRepo;
|
||||
protected BaseRepo $baseRepo;
|
||||
protected RevisionRepo $revisionRepo;
|
||||
protected ReferenceStore $referenceStore;
|
||||
protected ReferenceUpdater $referenceUpdater;
|
||||
|
||||
/**
|
||||
* PageRepo constructor.
|
||||
*/
|
||||
public function __construct(BaseRepo $baseRepo)
|
||||
{
|
||||
public function __construct(
|
||||
BaseRepo $baseRepo,
|
||||
RevisionRepo $revisionRepo,
|
||||
ReferenceStore $referenceStore,
|
||||
ReferenceUpdater $referenceUpdater
|
||||
) {
|
||||
$this->baseRepo = $baseRepo;
|
||||
$this->revisionRepo = $revisionRepo;
|
||||
$this->referenceStore = $referenceStore;
|
||||
$this->referenceUpdater = $referenceUpdater;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,6 +50,7 @@ class PageRepo
|
||||
*/
|
||||
public function getById(int $id, array $relations = ['book']): Page
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = Page::visible()->with($relations)->find($id);
|
||||
|
||||
if (!$page) {
|
||||
@ -70,17 +82,7 @@ class PageRepo
|
||||
*/
|
||||
public function getByOldSlug(string $bookSlug, string $pageSlug): ?Page
|
||||
{
|
||||
/** @var ?PageRevision $revision */
|
||||
$revision = PageRevision::query()
|
||||
->whereHas('page', function (Builder $query) {
|
||||
$query->scopes('visible');
|
||||
})
|
||||
->where('slug', '=', $pageSlug)
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)
|
||||
->orderBy('created_at', 'desc')
|
||||
->with('page')
|
||||
->first();
|
||||
$revision = $this->revisionRepo->getBySlugs($bookSlug, $pageSlug);
|
||||
|
||||
return $revision->page ?? null;
|
||||
}
|
||||
@ -112,7 +114,7 @@ class PageRepo
|
||||
public function getParentFromSlugs(string $bookSlug, string $chapterSlug = null): Entity
|
||||
{
|
||||
if ($chapterSlug !== null) {
|
||||
return $chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
|
||||
return Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
|
||||
}
|
||||
|
||||
return Book::visible()->where('slug', '=', $bookSlug)->firstOrFail();
|
||||
@ -123,9 +125,7 @@ class PageRepo
|
||||
*/
|
||||
public function getUserDraft(Page $page): ?PageRevision
|
||||
{
|
||||
$revision = $this->getUserDraftQuery($page)->first();
|
||||
|
||||
return $revision;
|
||||
return $this->revisionRepo->getLatestDraftForCurrentUser($page);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -165,11 +165,10 @@ class PageRepo
|
||||
$draft->draft = false;
|
||||
$draft->revision_count = 1;
|
||||
$draft->priority = $this->getNewPriority($draft);
|
||||
$draft->refreshSlug();
|
||||
$draft->save();
|
||||
|
||||
$this->savePageRevision($draft, trans('entities.pages_initial_revision'));
|
||||
$draft->indexForSearch();
|
||||
$this->revisionRepo->storeNewForPage($draft, trans('entities.pages_initial_revision'));
|
||||
$this->referenceStore->updateForPage($draft);
|
||||
$draft->refresh();
|
||||
|
||||
Activity::add(ActivityType::PAGE_CREATE, $draft);
|
||||
@ -189,13 +188,14 @@ class PageRepo
|
||||
|
||||
$this->updateTemplateStatusAndContentFromInput($page, $input);
|
||||
$this->baseRepo->update($page, $input);
|
||||
$this->referenceStore->updateForPage($page);
|
||||
|
||||
// Update with new details
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->getUserDraftQuery($page)->delete();
|
||||
$this->revisionRepo->deleteDraftsForCurrentUser($page);
|
||||
|
||||
// Save a revision after updating
|
||||
$summary = trim($input['summary'] ?? '');
|
||||
@ -203,7 +203,7 @@ class PageRepo
|
||||
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
|
||||
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
|
||||
if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
|
||||
$this->savePageRevision($page, $summary);
|
||||
$this->revisionRepo->storeNewForPage($page, $summary);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::PAGE_UPDATE, $page);
|
||||
@ -239,32 +239,6 @@ class PageRepo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
*/
|
||||
protected function savePageRevision(Page $page, string $summary = null): PageRevision
|
||||
{
|
||||
$revision = new PageRevision();
|
||||
|
||||
$revision->name = $page->name;
|
||||
$revision->html = $page->html;
|
||||
$revision->markdown = $page->markdown;
|
||||
$revision->text = $page->text;
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$revision->created_by = user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->revision_number = $page->revision_count;
|
||||
$revision->save();
|
||||
|
||||
$this->deleteOldRevisions($page);
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
*/
|
||||
@ -280,7 +254,7 @@ class PageRepo
|
||||
}
|
||||
|
||||
// Otherwise, save the data to a revision
|
||||
$draft = $this->getPageRevisionToUpdate($page);
|
||||
$draft = $this->revisionRepo->getNewDraftForCurrentUser($page);
|
||||
$draft->fill($input);
|
||||
|
||||
if (!empty($input['markdown'])) {
|
||||
@ -314,6 +288,7 @@ class PageRepo
|
||||
*/
|
||||
public function restoreRevision(Page $page, int $revisionId): Page
|
||||
{
|
||||
$oldUrl = $page->getUrl();
|
||||
$page->revision_count++;
|
||||
|
||||
/** @var PageRevision $revision */
|
||||
@ -332,9 +307,14 @@ class PageRepo
|
||||
$page->refreshSlug();
|
||||
$page->save();
|
||||
$page->indexForSearch();
|
||||
$this->referenceStore->updateForPage($page);
|
||||
|
||||
$summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
|
||||
$this->savePageRevision($page, $summary);
|
||||
$this->revisionRepo->storeNewForPage($page, $summary);
|
||||
|
||||
if ($oldUrl !== $page->getUrl()) {
|
||||
$this->referenceUpdater->updateEntityPageReferences($page, $oldUrl);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::PAGE_RESTORE, $page);
|
||||
Activity::add(ActivityType::REVISION_RESTORE, $revision);
|
||||
@ -393,48 +373,6 @@ class PageRepo
|
||||
return $parentClass::visible()->where('id', '=', $entityId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page revision to update for the given page.
|
||||
* Checks for an existing revisions before providing a fresh one.
|
||||
*/
|
||||
protected function getPageRevisionToUpdate(Page $page): PageRevision
|
||||
{
|
||||
$drafts = $this->getUserDraftQuery($page)->get();
|
||||
if ($drafts->count() > 0) {
|
||||
return $drafts->first();
|
||||
}
|
||||
|
||||
$draft = new PageRevision();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = user()->id;
|
||||
$draft->type = 'update_draft';
|
||||
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old revisions, for the given page, from the system.
|
||||
*/
|
||||
protected function deleteOldRevisions(Page $page)
|
||||
{
|
||||
$revisionLimit = config('app.revision_limit');
|
||||
if ($revisionLimit === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$revisionsToDelete = PageRevision::query()
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(intval($revisionLimit))
|
||||
->take(10)
|
||||
->get(['id']);
|
||||
if ($revisionsToDelete->count() > 0) {
|
||||
PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority for a page.
|
||||
*/
|
||||
@ -450,15 +388,4 @@ class PageRepo
|
||||
|
||||
return (new BookContents($page->book))->getLastPriority() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query to find the user's draft copies of the given page.
|
||||
*/
|
||||
protected function getUserDraftQuery(Page $page)
|
||||
{
|
||||
return PageRevision::query()->where('created_by', '=', user()->id)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
|
131
app/Entities/Repos/RevisionRepo.php
Normal file
131
app/Entities/Repos/RevisionRepo.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class RevisionRepo
|
||||
{
|
||||
/**
|
||||
* Get a revision by its stored book and page slug values.
|
||||
*/
|
||||
public function getBySlugs(string $bookSlug, string $pageSlug): ?PageRevision
|
||||
{
|
||||
/** @var ?PageRevision $revision */
|
||||
$revision = PageRevision::query()
|
||||
->whereHas('page', function (Builder $query) {
|
||||
$query->scopes('visible');
|
||||
})
|
||||
->where('slug', '=', $pageSlug)
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)
|
||||
->orderBy('created_at', 'desc')
|
||||
->with('page')
|
||||
->first();
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest draft revision, for the given page, belonging to the current user.
|
||||
*/
|
||||
public function getLatestDraftForCurrentUser(Page $page): ?PageRevision
|
||||
{
|
||||
/** @var ?PageRevision $revision */
|
||||
$revision = $this->queryForCurrentUserDraft($page->id)->first();
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all drafts revisions, for the given page, belonging to the current user.
|
||||
*/
|
||||
public function deleteDraftsForCurrentUser(Page $page): void
|
||||
{
|
||||
$this->queryForCurrentUserDraft($page->id)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user update_draft page revision to update for the given page.
|
||||
* Checks for an existing revisions before providing a fresh one.
|
||||
*/
|
||||
public function getNewDraftForCurrentUser(Page $page): PageRevision
|
||||
{
|
||||
$draft = $this->getLatestDraftForCurrentUser($page);
|
||||
|
||||
if ($draft) {
|
||||
return $draft;
|
||||
}
|
||||
|
||||
$draft = new PageRevision();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = user()->id;
|
||||
$draft->type = 'update_draft';
|
||||
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new revision in the system for the given page.
|
||||
*/
|
||||
public function storeNewForPage(Page $page, string $summary = null): PageRevision
|
||||
{
|
||||
$revision = new PageRevision();
|
||||
|
||||
$revision->name = $page->name;
|
||||
$revision->html = $page->html;
|
||||
$revision->markdown = $page->markdown;
|
||||
$revision->text = $page->text;
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$revision->created_by = user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->revision_number = $page->revision_count;
|
||||
$revision->save();
|
||||
|
||||
$this->deleteOldRevisions($page);
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete old revisions, for the given page, from the system.
|
||||
*/
|
||||
protected function deleteOldRevisions(Page $page)
|
||||
{
|
||||
$revisionLimit = config('app.revision_limit');
|
||||
if ($revisionLimit === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$revisionsToDelete = PageRevision::query()
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->skip(intval($revisionLimit))
|
||||
->take(10)
|
||||
->get(['id']);
|
||||
|
||||
if ($revisionsToDelete->count() > 0) {
|
||||
PageRevision::query()->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query update draft revisions for the current user.
|
||||
*/
|
||||
protected function queryForCurrentUserDraft(int $pageId): Builder
|
||||
{
|
||||
return PageRevision::query()
|
||||
->where('created_by', '=', user()->id)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $pageId)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
@ -235,7 +235,7 @@ class ExportFormatter
|
||||
$linksOutput = [];
|
||||
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
|
||||
|
||||
// Replace image src with base64 encoded image strings
|
||||
// Update relative links to be absolute, with instance url
|
||||
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
|
||||
foreach ($linksOutput[0] as $index => $linkMatch) {
|
||||
$oldLinkString = $linkMatch;
|
||||
@ -248,7 +248,6 @@ class ExportFormatter
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any relative links with system domain
|
||||
return $htmlContent;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@ namespace BookStack\Entities\Tools;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
@ -372,23 +374,30 @@ class PageContent
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find page and skip this if page not found
|
||||
// Find page to use, and default replacement to empty string for non-matches.
|
||||
/** @var ?Page $matchedPage */
|
||||
$matchedPage = Page::visible()->find($pageId);
|
||||
if ($matchedPage === null) {
|
||||
$html = str_replace($fullMatch, '', $html);
|
||||
continue;
|
||||
$replacement = '';
|
||||
|
||||
if ($matchedPage && count($splitInclude) === 1) {
|
||||
// If we only have page id, just insert all page html and continue.
|
||||
$replacement = $matchedPage->html;
|
||||
} elseif ($matchedPage && count($splitInclude) > 1) {
|
||||
// Otherwise, if our include tag defines a section, load that specific content
|
||||
$innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
|
||||
$replacement = trim($innerContent);
|
||||
}
|
||||
|
||||
// If we only have page id, just insert all page html and continue.
|
||||
if (count($splitInclude) === 1) {
|
||||
$html = str_replace($fullMatch, $matchedPage->html, $html);
|
||||
continue;
|
||||
}
|
||||
$themeReplacement = Theme::dispatch(
|
||||
ThemeEvents::PAGE_INCLUDE_PARSE,
|
||||
$includeId,
|
||||
$replacement,
|
||||
clone $this->page,
|
||||
$matchedPage ? (clone $matchedPage) : null,
|
||||
);
|
||||
|
||||
// Create and load HTML into a document
|
||||
$innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]);
|
||||
$html = str_replace($fullMatch, trim($innerContent), $html);
|
||||
// Perform the content replacement
|
||||
$html = str_replace($fullMatch, $themeReplacement ?? $replacement, $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
|
@ -376,6 +376,8 @@ class TrashCan
|
||||
$entity->searchTerms()->delete();
|
||||
$entity->deletions()->delete();
|
||||
$entity->favourites()->delete();
|
||||
$entity->referencesTo()->delete();
|
||||
$entity->referencesFrom()->delete();
|
||||
|
||||
if ($entity instanceof HasCoverImage && $entity->cover()->exists()) {
|
||||
$imageService = app()->make(ImageService::class);
|
||||
|
@ -3,9 +3,9 @@
|
||||
namespace BookStack\Http\Controllers\Api;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
use BookStack\Entities\Tools\SearchResultsFormatter;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use BookStack\Search\SearchOptions;
|
||||
use BookStack\Search\SearchResultsFormatter;
|
||||
use BookStack\Search\SearchRunner;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchApiController extends ApiController
|
||||
|
@ -15,19 +15,22 @@ use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class BookController extends Controller
|
||||
{
|
||||
protected $bookRepo;
|
||||
protected $entityContextManager;
|
||||
protected BookRepo $bookRepo;
|
||||
protected ShelfContext $shelfContext;
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo)
|
||||
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo, ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->bookRepo = $bookRepo;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
$this->shelfContext = $entityContextManager;
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,7 +47,7 @@ class BookController extends Controller
|
||||
$popular = $this->bookRepo->getPopular(4);
|
||||
$new = $this->bookRepo->getRecentlyCreated(4);
|
||||
|
||||
$this->entityContextManager->clearShelfContext();
|
||||
$this->shelfContext->clearShelfContext();
|
||||
|
||||
$this->setPageTitle(trans('entities.books'));
|
||||
|
||||
@ -122,7 +125,7 @@ class BookController extends Controller
|
||||
|
||||
View::incrementFor($book);
|
||||
if ($request->has('shelf')) {
|
||||
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
||||
$this->shelfContext->setShelfContext(intval($request->get('shelf')));
|
||||
}
|
||||
|
||||
$this->setPageTitle($book->getShortName());
|
||||
@ -133,6 +136,7 @@ class BookController extends Controller
|
||||
'bookChildren' => $bookChildren,
|
||||
'bookParentShelves' => $bookParentShelves,
|
||||
'activity' => $activities->entityActivity($book, 20, 1),
|
||||
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@ -18,11 +19,13 @@ class BookshelfController extends Controller
|
||||
{
|
||||
protected BookshelfRepo $shelfRepo;
|
||||
protected ShelfContext $shelfContext;
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext)
|
||||
public function __construct(BookshelfRepo $shelfRepo, ShelfContext $shelfContext, ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->shelfRepo = $shelfRepo;
|
||||
$this->shelfContext = $shelfContext;
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,6 +127,7 @@ class BookshelfController extends Controller
|
||||
'activity' => $activities->entityActivity($shelf, 20, 1),
|
||||
'order' => $order,
|
||||
'sort' => $sort,
|
||||
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -13,20 +13,20 @@ use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
protected $chapterRepo;
|
||||
protected ChapterRepo $chapterRepo;
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
/**
|
||||
* ChapterController constructor.
|
||||
*/
|
||||
public function __construct(ChapterRepo $chapterRepo)
|
||||
public function __construct(ChapterRepo $chapterRepo, ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->chapterRepo = $chapterRepo;
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,13 +77,14 @@ class ChapterController extends Controller
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
|
||||
return view('chapters.show', [
|
||||
'book' => $chapter->book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
'book' => $chapter->book,
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($chapter),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,9 @@ use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
protected $imageRepo;
|
||||
protected $imageService;
|
||||
protected ImageRepo $imageRepo;
|
||||
protected ImageService $imageService;
|
||||
|
||||
/**
|
||||
* ImageController constructor.
|
||||
*/
|
||||
public function __construct(ImageRepo $imageRepo, ImageService $imageService)
|
||||
{
|
||||
$this->imageRepo = $imageRepo;
|
||||
@ -33,7 +30,7 @@ class ImageController extends Controller
|
||||
*/
|
||||
public function showImage(string $path)
|
||||
{
|
||||
if (!$this->imageService->pathExistsInLocalSecure($path)) {
|
||||
if (!$this->imageService->pathAccessibleInLocalSecure($path)) {
|
||||
throw (new NotFoundException(trans('errors.image_not_found')))
|
||||
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
||||
->setDetails(trans('errors.image_not_found_details'));
|
||||
|
@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers;
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Notifications\TestEmail;
|
||||
use BookStack\References\ReferenceStore;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -74,6 +75,24 @@ class MaintenanceController extends Controller
|
||||
$this->showErrorNotification($errorMessage);
|
||||
}
|
||||
|
||||
return redirect('/settings/maintenance#image-cleanup')->withInput();
|
||||
return redirect('/settings/maintenance#image-cleanup');
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to regenerate the reference index in the system.
|
||||
*/
|
||||
public function regenerateReferences(ReferenceStore $referenceStore)
|
||||
{
|
||||
$this->checkPermission('settings-manage');
|
||||
$this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'regenerate-references');
|
||||
|
||||
try {
|
||||
$referenceStore->updateForAllPages();
|
||||
$this->showSuccessNotification(trans('settings.maint_regen_references_success'));
|
||||
} catch (\Exception $exception) {
|
||||
$this->showErrorNotification($exception->getMessage());
|
||||
}
|
||||
|
||||
return redirect('/settings/maintenance#regenerate-references');
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ use BookStack\Entities\Tools\PageEditorData;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Http\Request;
|
||||
@ -23,13 +24,15 @@ use Throwable;
|
||||
class PageController extends Controller
|
||||
{
|
||||
protected PageRepo $pageRepo;
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
*/
|
||||
public function __construct(PageRepo $pageRepo)
|
||||
public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,6 +163,7 @@ class PageController extends Controller
|
||||
'pageNav' => $pageNav,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($page),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -27,9 +27,9 @@ class PageRevisionController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$revisions = $page->revisions()->select([
|
||||
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
|
||||
'type', 'revision_number', 'summary',
|
||||
])
|
||||
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
|
||||
'type', 'revision_number', 'summary',
|
||||
])
|
||||
->selectRaw("IF(markdown = '', false, true) as is_markdown")
|
||||
->with(['page.book', 'createdBy'])
|
||||
->get();
|
||||
@ -38,7 +38,7 @@ class PageRevisionController extends Controller
|
||||
|
||||
return view('pages.revisions', [
|
||||
'revisions' => $revisions,
|
||||
'page' => $page,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
|
77
app/Http/Controllers/ReferenceController.php
Normal file
77
app/Http/Controllers/ReferenceController.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\References\ReferenceFetcher;
|
||||
|
||||
class ReferenceController extends Controller
|
||||
{
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
|
||||
public function __construct(ReferenceFetcher $referenceFetcher)
|
||||
{
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the references to a given page.
|
||||
*/
|
||||
public function page(string $bookSlug, string $pageSlug)
|
||||
{
|
||||
/** @var Page $page */
|
||||
$page = Page::visible()->whereSlugs($bookSlug, $pageSlug)->firstOrFail();
|
||||
$references = $this->referenceFetcher->getPageReferencesToEntity($page);
|
||||
|
||||
return view('pages.references', [
|
||||
'page' => $page,
|
||||
'references' => $references,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the references to a given chapter.
|
||||
*/
|
||||
public function chapter(string $bookSlug, string $chapterSlug)
|
||||
{
|
||||
/** @var Chapter $chapter */
|
||||
$chapter = Chapter::visible()->whereSlugs($bookSlug, $chapterSlug)->firstOrFail();
|
||||
$references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
|
||||
|
||||
return view('chapters.references', [
|
||||
'chapter' => $chapter,
|
||||
'references' => $references,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the references to a given book.
|
||||
*/
|
||||
public function book(string $slug)
|
||||
{
|
||||
$book = Book::visible()->where('slug', '=', $slug)->firstOrFail();
|
||||
$references = $this->referenceFetcher->getPageReferencesToEntity($book);
|
||||
|
||||
return view('books.references', [
|
||||
'book' => $book,
|
||||
'references' => $references,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the references to a given shelf.
|
||||
*/
|
||||
public function shelf(string $slug)
|
||||
{
|
||||
$shelf = Bookshelf::visible()->where('slug', '=', $slug)->firstOrFail();
|
||||
$references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
|
||||
|
||||
return view('shelves.references', [
|
||||
'shelf' => $shelf,
|
||||
'references' => $references,
|
||||
]);
|
||||
}
|
||||
}
|
@ -3,10 +3,10 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Queries\Popular;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
use BookStack\Entities\Tools\SearchResultsFormatter;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use BookStack\Entities\Tools\SiblingFetcher;
|
||||
use BookStack\Search\SearchOptions;
|
||||
use BookStack\Search\SearchResultsFormatter;
|
||||
use BookStack\Search\SearchRunner;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
|
@ -2,59 +2,18 @@
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use BookStack\Util\LanguageManager;
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* Array of right-to-left locales.
|
||||
*/
|
||||
protected $rtlLocales = ['ar', 'fa', 'he'];
|
||||
protected LanguageManager $languageManager;
|
||||
|
||||
/**
|
||||
* Map of BookStack locale names to best-estimate system locale names.
|
||||
* Locales can often be found by running `locale -a` on a linux system.
|
||||
*/
|
||||
protected $localeMap = [
|
||||
'ar' => 'ar',
|
||||
'bg' => 'bg_BG',
|
||||
'bs' => 'bs_BA',
|
||||
'ca' => 'ca',
|
||||
'da' => 'da_DK',
|
||||
'de' => 'de_DE',
|
||||
'de_informal' => 'de_DE',
|
||||
'en' => 'en_GB',
|
||||
'es' => 'es_ES',
|
||||
'es_AR' => 'es_AR',
|
||||
'et' => 'et_EE',
|
||||
'eu' => 'eu_ES',
|
||||
'fa' => 'fa_IR',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'hr' => 'hr_HR',
|
||||
'id' => 'id_ID',
|
||||
'it' => 'it_IT',
|
||||
'ja' => 'ja',
|
||||
'ko' => 'ko_KR',
|
||||
'lt' => 'lt_LT',
|
||||
'lv' => 'lv_LV',
|
||||
'nl' => 'nl_NL',
|
||||
'nb' => 'nb_NO',
|
||||
'pl' => 'pl_PL',
|
||||
'pt' => 'pt_PT',
|
||||
'pt_BR' => 'pt_BR',
|
||||
'ru' => 'ru',
|
||||
'sk' => 'sk_SK',
|
||||
'sl' => 'sl_SI',
|
||||
'sv' => 'sv_SE',
|
||||
'uk' => 'uk_UA',
|
||||
'vi' => 'vi_VN',
|
||||
'zh_CN' => 'zh_CN',
|
||||
'zh_TW' => 'zh_TW',
|
||||
'tr' => 'tr_TR',
|
||||
];
|
||||
public function __construct(LanguageManager $languageManager)
|
||||
{
|
||||
$this->languageManager = $languageManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
@ -66,76 +25,23 @@ class Localization
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
// Get and record the default language in the config
|
||||
$defaultLang = config('app.locale');
|
||||
config()->set('app.default_locale', $defaultLang);
|
||||
|
||||
$locale = $this->getUserLocale($request, $defaultLang);
|
||||
config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
|
||||
// Get the user's language and record that in the config for use in views
|
||||
$userLang = $this->languageManager->getUserLanguage($request, $defaultLang);
|
||||
config()->set('app.lang', str_replace('_', '-', $this->languageManager->getIsoName($userLang)));
|
||||
|
||||
// Set text direction
|
||||
if (in_array($locale, $this->rtlLocales)) {
|
||||
if ($this->languageManager->isRTL($userLang)) {
|
||||
config()->set('app.rtl', true);
|
||||
}
|
||||
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
$this->setSystemDateLocale($locale);
|
||||
app()->setLocale($userLang);
|
||||
Carbon::setLocale($userLang);
|
||||
$this->languageManager->setPhpDateTimeLocale($userLang);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale specifically for the currently logged in user if available.
|
||||
*/
|
||||
protected function getUserLocale(Request $request, string $default): string
|
||||
{
|
||||
try {
|
||||
$user = user();
|
||||
} catch (\Exception $exception) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($user->isDefault() && config('app.auto_detect_locale')) {
|
||||
return $this->autoDetectLocale($request, $default);
|
||||
}
|
||||
|
||||
return setting()->getUser($user, 'language', $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect the visitors locale by matching locales in their headers
|
||||
* against the locales supported by BookStack.
|
||||
*/
|
||||
protected function autoDetectLocale(Request $request, string $default): string
|
||||
{
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (in_array($lang, $availableLocales)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO version of a BookStack language name.
|
||||
*/
|
||||
public function getLocaleIso(string $locale): string
|
||||
{
|
||||
return $this->localeMap[$locale] ?? $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system date locale for localized date formatting.
|
||||
* Will try both the standard locale name and the UTF8 variant.
|
||||
*/
|
||||
protected function setSystemDateLocale(string $locale)
|
||||
{
|
||||
$systemLocale = $this->getLocaleIso($locale);
|
||||
$set = setlocale(LC_TIME, $systemLocale);
|
||||
if ($set === false) {
|
||||
setlocale(LC_TIME, $systemLocale . '.utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
102
app/References/CrossLinkParser.php
Normal file
102
app/References/CrossLinkParser.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use BookStack\Model;
|
||||
use BookStack\References\ModelResolvers\BookLinkModelResolver;
|
||||
use BookStack\References\ModelResolvers\BookshelfLinkModelResolver;
|
||||
use BookStack\References\ModelResolvers\ChapterLinkModelResolver;
|
||||
use BookStack\References\ModelResolvers\CrossLinkModelResolver;
|
||||
use BookStack\References\ModelResolvers\PageLinkModelResolver;
|
||||
use BookStack\References\ModelResolvers\PagePermalinkModelResolver;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
||||
class CrossLinkParser
|
||||
{
|
||||
/**
|
||||
* @var CrossLinkModelResolver[]
|
||||
*/
|
||||
protected array $modelResolvers;
|
||||
|
||||
public function __construct(array $modelResolvers)
|
||||
{
|
||||
$this->modelResolvers = $modelResolvers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract any found models within the given HTML content.
|
||||
*
|
||||
* @return Model[]
|
||||
*/
|
||||
public function extractLinkedModels(string $html): array
|
||||
{
|
||||
$models = [];
|
||||
|
||||
$links = $this->getLinksFromContent($html);
|
||||
|
||||
foreach ($links as $link) {
|
||||
$model = $this->linkToModel($link);
|
||||
if (!is_null($model)) {
|
||||
$models[get_class($model) . ':' . $model->id] = $model;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of href values from the given document.
|
||||
*
|
||||
* @returns string[]
|
||||
*/
|
||||
protected function getLinksFromContent(string $html): array
|
||||
{
|
||||
$links = [];
|
||||
|
||||
$html = '<body>' . $html . '</body>';
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$xPath = new DOMXPath($doc);
|
||||
$anchors = $xPath->query('//a[@href]');
|
||||
|
||||
/** @var \DOMElement $anchor */
|
||||
foreach ($anchors as $anchor) {
|
||||
$links[] = $anchor->getAttribute('href');
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to resolve the given link to a model using the instance model resolvers.
|
||||
*/
|
||||
protected function linkToModel(string $link): ?Model
|
||||
{
|
||||
foreach ($this->modelResolvers as $resolver) {
|
||||
$model = $resolver->resolve($link);
|
||||
if (!is_null($model)) {
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with a pre-defined set of model resolvers, specifically for the
|
||||
* default set of entities within BookStack.
|
||||
*/
|
||||
public static function createWithEntityResolvers(): self
|
||||
{
|
||||
return new self([
|
||||
new PagePermalinkModelResolver(),
|
||||
new PageLinkModelResolver(),
|
||||
new ChapterLinkModelResolver(),
|
||||
new BookLinkModelResolver(),
|
||||
new BookshelfLinkModelResolver(),
|
||||
]);
|
||||
}
|
||||
}
|
26
app/References/ModelResolvers/BookLinkModelResolver.php
Normal file
26
app/References/ModelResolvers/BookLinkModelResolver.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Model;
|
||||
|
||||
class BookLinkModelResolver implements CrossLinkModelResolver
|
||||
{
|
||||
public function resolve(string $link): ?Model
|
||||
{
|
||||
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
|
||||
$matches = [];
|
||||
$match = preg_match($pattern, $link, $matches);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookSlug = $matches[1];
|
||||
|
||||
/** @var ?Book $model */
|
||||
$model = Book::query()->where('slug', '=', $bookSlug)->first(['id']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
26
app/References/ModelResolvers/BookshelfLinkModelResolver.php
Normal file
26
app/References/ModelResolvers/BookshelfLinkModelResolver.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Model;
|
||||
|
||||
class BookshelfLinkModelResolver implements CrossLinkModelResolver
|
||||
{
|
||||
public function resolve(string $link): ?Model
|
||||
{
|
||||
$pattern = '/^' . preg_quote(url('/shelves'), '/') . '\/([\w-]+)' . '([#?\/]|$)/';
|
||||
$matches = [];
|
||||
$match = preg_match($pattern, $link, $matches);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$shelfSlug = $matches[1];
|
||||
|
||||
/** @var ?Bookshelf $model */
|
||||
$model = Bookshelf::query()->where('slug', '=', $shelfSlug)->first(['id']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
27
app/References/ModelResolvers/ChapterLinkModelResolver.php
Normal file
27
app/References/ModelResolvers/ChapterLinkModelResolver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Model;
|
||||
|
||||
class ChapterLinkModelResolver implements CrossLinkModelResolver
|
||||
{
|
||||
public function resolve(string $link): ?Model
|
||||
{
|
||||
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/chapter\/' . '([\w-]+)' . '([#?\/]|$)/';
|
||||
$matches = [];
|
||||
$match = preg_match($pattern, $link, $matches);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookSlug = $matches[1];
|
||||
$chapterSlug = $matches[2];
|
||||
|
||||
/** @var ?Chapter $model */
|
||||
$model = Chapter::query()->whereSlugs($bookSlug, $chapterSlug)->first(['id']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
13
app/References/ModelResolvers/CrossLinkModelResolver.php
Normal file
13
app/References/ModelResolvers/CrossLinkModelResolver.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
interface CrossLinkModelResolver
|
||||
{
|
||||
/**
|
||||
* Resolve the given href link value to a model.
|
||||
*/
|
||||
public function resolve(string $link): ?Model;
|
||||
}
|
27
app/References/ModelResolvers/PageLinkModelResolver.php
Normal file
27
app/References/ModelResolvers/PageLinkModelResolver.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
|
||||
class PageLinkModelResolver implements CrossLinkModelResolver
|
||||
{
|
||||
public function resolve(string $link): ?Model
|
||||
{
|
||||
$pattern = '/^' . preg_quote(url('/books'), '/') . '\/([\w-]+)' . '\/page\/' . '([\w-]+)' . '([#?\/]|$)/';
|
||||
$matches = [];
|
||||
$match = preg_match($pattern, $link, $matches);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bookSlug = $matches[1];
|
||||
$pageSlug = $matches[2];
|
||||
|
||||
/** @var ?Page $model */
|
||||
$model = Page::query()->whereSlugs($bookSlug, $pageSlug)->first(['id']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
25
app/References/ModelResolvers/PagePermalinkModelResolver.php
Normal file
25
app/References/ModelResolvers/PagePermalinkModelResolver.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References\ModelResolvers;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
|
||||
class PagePermalinkModelResolver implements CrossLinkModelResolver
|
||||
{
|
||||
public function resolve(string $link): ?Model
|
||||
{
|
||||
$pattern = '/^' . preg_quote(url('/link'), '/') . '\/(\d+)/';
|
||||
$matches = [];
|
||||
$match = preg_match($pattern, $link, $matches);
|
||||
if (!$match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$id = intval($matches[1]);
|
||||
/** @var ?Page $model */
|
||||
$model = Page::query()->find($id, ['id']);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
27
app/References/Reference.php
Normal file
27
app/References/Reference.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property int $from_id
|
||||
* @property string $from_type
|
||||
* @property int $to_id
|
||||
* @property string $to_type
|
||||
*/
|
||||
class Reference extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
public function from(): MorphTo
|
||||
{
|
||||
return $this->morphTo('from');
|
||||
}
|
||||
|
||||
public function to(): MorphTo
|
||||
{
|
||||
return $this->morphTo('to');
|
||||
}
|
||||
}
|
62
app/References/ReferenceFetcher.php
Normal file
62
app/References/ReferenceFetcher.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
|
||||
class ReferenceFetcher
|
||||
{
|
||||
protected PermissionApplicator $permissions;
|
||||
|
||||
public function __construct(PermissionApplicator $permissions)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query and return the page references pointing to the given entity.
|
||||
* Loads the commonly required relations while taking permissions into account.
|
||||
*/
|
||||
public function getPageReferencesToEntity(Entity $entity): Collection
|
||||
{
|
||||
$baseQuery = $entity->referencesTo()
|
||||
->where('from_type', '=', (new Page())->getMorphClass())
|
||||
->with([
|
||||
'from' => fn (Relation $query) => $query->select(Page::$listAttributes),
|
||||
'from.book' => fn (Relation $query) => $query->scopes('visible'),
|
||||
'from.chapter' => fn (Relation $query) => $query->scopes('visible'),
|
||||
]);
|
||||
|
||||
$references = $this->permissions->restrictEntityRelationQuery(
|
||||
$baseQuery,
|
||||
'references',
|
||||
'from_id',
|
||||
'from_type'
|
||||
)->get();
|
||||
|
||||
return $references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the count of page references pointing to the given entity.
|
||||
* Takes permissions into account.
|
||||
*/
|
||||
public function getPageReferenceCountToEntity(Entity $entity): int
|
||||
{
|
||||
$baseQuery = $entity->referencesTo()
|
||||
->where('from_type', '=', (new Page())->getMorphClass());
|
||||
|
||||
$count = $this->permissions->restrictEntityRelationQuery(
|
||||
$baseQuery,
|
||||
'references',
|
||||
'from_id',
|
||||
'from_type'
|
||||
)->count();
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
69
app/References/ReferenceStore.php
Normal file
69
app/References/ReferenceStore.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ReferenceStore
|
||||
{
|
||||
/**
|
||||
* Update the outgoing references for the given page.
|
||||
*/
|
||||
public function updateForPage(Page $page): void
|
||||
{
|
||||
$this->updateForPages([$page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the outgoing references for all pages in the system.
|
||||
*/
|
||||
public function updateForAllPages(): void
|
||||
{
|
||||
Reference::query()
|
||||
->where('from_type', '=', (new Page())->getMorphClass())
|
||||
->delete();
|
||||
|
||||
Page::query()->select(['id', 'html'])->chunk(100, function (Collection $pages) {
|
||||
$this->updateForPages($pages->all());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the outgoing references for the pages in the given array.
|
||||
*
|
||||
* @param Page[] $pages
|
||||
*/
|
||||
protected function updateForPages(array $pages): void
|
||||
{
|
||||
if (count($pages) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parser = CrossLinkParser::createWithEntityResolvers();
|
||||
$references = [];
|
||||
|
||||
$pageIds = array_map(fn (Page $page) => $page->id, $pages);
|
||||
Reference::query()
|
||||
->where('from_type', '=', $pages[0]->getMorphClass())
|
||||
->whereIn('from_id', $pageIds)
|
||||
->delete();
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$models = $parser->extractLinkedModels($page->html);
|
||||
|
||||
foreach ($models as $model) {
|
||||
$references[] = [
|
||||
'from_id' => $page->id,
|
||||
'from_type' => $page->getMorphClass(),
|
||||
'to_id' => $model->id,
|
||||
'to_type' => $model->getMorphClass(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_chunk($references, 1000) as $referenceDataChunk) {
|
||||
Reference::query()->insert($referenceDataChunk);
|
||||
}
|
||||
}
|
||||
}
|
122
app/References/ReferenceUpdater.php
Normal file
122
app/References/ReferenceUpdater.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\References;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\RevisionRepo;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
||||
class ReferenceUpdater
|
||||
{
|
||||
protected ReferenceFetcher $referenceFetcher;
|
||||
protected RevisionRepo $revisionRepo;
|
||||
|
||||
public function __construct(ReferenceFetcher $referenceFetcher, RevisionRepo $revisionRepo)
|
||||
{
|
||||
$this->referenceFetcher = $referenceFetcher;
|
||||
$this->revisionRepo = $revisionRepo;
|
||||
}
|
||||
|
||||
public function updateEntityPageReferences(Entity $entity, string $oldLink)
|
||||
{
|
||||
$references = $this->getReferencesToUpdate($entity);
|
||||
$newLink = $entity->getUrl();
|
||||
|
||||
/** @var Reference $reference */
|
||||
foreach ($references as $reference) {
|
||||
/** @var Page $page */
|
||||
$page = $reference->from;
|
||||
$this->updateReferencesWithinPage($page, $oldLink, $newLink);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Reference[]
|
||||
*/
|
||||
protected function getReferencesToUpdate(Entity $entity): array
|
||||
{
|
||||
/** @var Reference[] $references */
|
||||
$references = $this->referenceFetcher->getPageReferencesToEntity($entity)->values()->all();
|
||||
|
||||
if ($entity instanceof Book) {
|
||||
$pages = $entity->pages()->get(['id']);
|
||||
$chapters = $entity->chapters()->get(['id']);
|
||||
$children = $pages->concat($chapters);
|
||||
foreach ($children as $bookChild) {
|
||||
$childRefs = $this->referenceFetcher->getPageReferencesToEntity($bookChild)->values()->all();
|
||||
array_push($references, ...$childRefs);
|
||||
}
|
||||
}
|
||||
|
||||
$deduped = [];
|
||||
foreach ($references as $reference) {
|
||||
$key = $reference->from_id . ':' . $reference->from_type;
|
||||
$deduped[$key] = $reference;
|
||||
}
|
||||
|
||||
return array_values($deduped);
|
||||
}
|
||||
|
||||
protected function updateReferencesWithinPage(Page $page, string $oldLink, string $newLink)
|
||||
{
|
||||
$page = (clone $page)->refresh();
|
||||
$html = $this->updateLinksInHtml($page->html, $oldLink, $newLink);
|
||||
$markdown = $this->updateLinksInMarkdown($page->markdown, $oldLink, $newLink);
|
||||
|
||||
$page->html = $html;
|
||||
$page->markdown = $markdown;
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
|
||||
$summary = trans('entities.pages_references_update_revision');
|
||||
$this->revisionRepo->storeNewForPage($page, $summary);
|
||||
}
|
||||
|
||||
protected function updateLinksInMarkdown(string $markdown, string $oldLink, string $newLink): string
|
||||
{
|
||||
if (empty($markdown)) {
|
||||
return $markdown;
|
||||
}
|
||||
|
||||
$commonLinkRegex = '/(\[.*?\]\()' . preg_quote($oldLink, '/') . '(.*?\))/i';
|
||||
$markdown = preg_replace($commonLinkRegex, '$1' . $newLink . '$2', $markdown);
|
||||
|
||||
$referenceLinkRegex = '/(\[.*?\]:\s?)' . preg_quote($oldLink, '/') . '(.*?)($|\s)/i';
|
||||
$markdown = preg_replace($referenceLinkRegex, '$1' . $newLink . '$2$3', $markdown);
|
||||
|
||||
return $markdown;
|
||||
}
|
||||
|
||||
protected function updateLinksInHtml(string $html, string $oldLink, string $newLink): string
|
||||
{
|
||||
if (empty($html)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html = '<body>' . $html . '</body>';
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$xPath = new DOMXPath($doc);
|
||||
$anchors = $xPath->query('//a[@href]');
|
||||
|
||||
/** @var \DOMElement $anchor */
|
||||
foreach ($anchors as $anchor) {
|
||||
$link = $anchor->getAttribute('href');
|
||||
$updated = str_ireplace($oldLink, $newLink, $link);
|
||||
$anchor->setAttribute('href', $updated);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||
foreach ($topElems as $child) {
|
||||
$html .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Search;
|
||||
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\SearchTerm;
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
@ -1,30 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Search;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchOptions
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $searches = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $exacts = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $tags = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $filters = [];
|
||||
public array $searches = [];
|
||||
public array $exacts = [];
|
||||
public array $tags = [];
|
||||
public array $filters = [];
|
||||
|
||||
/**
|
||||
* Create a new instance from a search string.
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Search;
|
||||
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Entities\Models\Entity;
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
namespace BookStack\Search;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
||||
use BookStack\Auth\User;
|
||||
@ -8,7 +8,6 @@ use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\SearchTerm;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
@ -29,7 +28,7 @@ class SearchRunner
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
protected array $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* Retain a cache of score adjusted terms for specific search options.
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
namespace BookStack\Search;
|
||||
|
||||
use BookStack\Model;
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace BookStack\Theming;
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
|
||||
/**
|
||||
* The ThemeEvents used within BookStack.
|
||||
*
|
||||
@ -60,8 +62,7 @@ class ThemeEvents
|
||||
|
||||
/**
|
||||
* Commonmark environment configure.
|
||||
* Provides the commonmark library environment for customization
|
||||
* before it's used to render markdown content.
|
||||
* Provides the commonmark library environment for customization before it's 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
|
||||
@ -69,6 +70,21 @@ class ThemeEvents
|
||||
*/
|
||||
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
|
||||
|
||||
/**
|
||||
* Page include parse event.
|
||||
* Runs when a page include tag is being parsed, typically when page content is being processed for viewing.
|
||||
* Provides the "include tag" reference string, the default BookStack replacement content for the tag,
|
||||
* the current page being processed, and the page that's being referenced by the include tag.
|
||||
* The referenced page may be null where the page does not exist or where permissions prevent visibility.
|
||||
* If the listener returns a non-null value, that will be used as the replacement HTML content instead.
|
||||
*
|
||||
* @param string $tagReference
|
||||
* @param string $replacementHTML
|
||||
* @param Page $currentPage
|
||||
* @param ?Page $referencedPage
|
||||
*/
|
||||
const PAGE_INCLUDE_PARSE = 'page_include_parse';
|
||||
|
||||
/**
|
||||
* Web before middleware action.
|
||||
* Runs before the request is handled but after all other middleware apart from those
|
||||
|
@ -41,7 +41,7 @@ class AttachmentService
|
||||
|
||||
// Change to our secure-attachment disk if any of the local options
|
||||
// are used to prevent escaping that location.
|
||||
if ($storageType === 'local' || $storageType === 'local_secure') {
|
||||
if ($storageType === 'local' || $storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
|
||||
$storageType = 'local_secure_attachments';
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,9 @@
|
||||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
@ -24,20 +27,15 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ImageService
|
||||
{
|
||||
protected $imageTool;
|
||||
protected $cache;
|
||||
protected ImageManager $imageTool;
|
||||
protected Cache $cache;
|
||||
protected $storageUrl;
|
||||
protected $image;
|
||||
protected $fileSystem;
|
||||
protected FilesystemManager $fileSystem;
|
||||
|
||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
/**
|
||||
* ImageService constructor.
|
||||
*/
|
||||
public function __construct(Image $image, ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
|
||||
public function __construct(ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageTool = $imageTool;
|
||||
$this->fileSystem = $fileSystem;
|
||||
$this->cache = $cache;
|
||||
@ -55,9 +53,18 @@ class ImageService
|
||||
* Check if local secure image storage (Fetched behind authentication)
|
||||
* is currently active in the instance.
|
||||
*/
|
||||
protected function usingSecureImages(): bool
|
||||
protected function usingSecureImages(string $imageType = 'gallery'): bool
|
||||
{
|
||||
return $this->getStorageDiskName('gallery') === 'local_secure_images';
|
||||
return $this->getStorageDiskName($imageType) === 'local_secure_images';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "local secure restricted" (Fetched behind auth, with permissions enforced)
|
||||
* is currently active in the instance.
|
||||
*/
|
||||
protected function usingSecureRestrictedImages()
|
||||
{
|
||||
return config('filesystems.images') === 'local_secure_restricted';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,7 +75,7 @@ class ImageService
|
||||
{
|
||||
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
|
||||
|
||||
if ($this->getStorageDiskName($imageType) === 'local_secure_images') {
|
||||
if ($this->usingSecureImages($imageType)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
@ -87,7 +94,9 @@ class ImageService
|
||||
$storageType = 'local';
|
||||
}
|
||||
|
||||
if ($storageType === 'local_secure') {
|
||||
// Rename local_secure options to get our image specific storage driver which
|
||||
// is scoped to the relevant image directories.
|
||||
if ($storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
|
||||
$storageType = 'local_secure_images';
|
||||
}
|
||||
|
||||
@ -179,8 +188,8 @@ class ImageService
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
|
||||
$image = $this->image->newInstance();
|
||||
$image->forceFill($imageDetails)->save();
|
||||
$image = (new Image())->forceFill($imageDetails);
|
||||
$image->save();
|
||||
|
||||
return $image;
|
||||
}
|
||||
@ -451,7 +460,7 @@ class ImageService
|
||||
$types = ['gallery', 'drawio'];
|
||||
$deletedPaths = [];
|
||||
|
||||
$this->image->newQuery()->whereIn('type', $types)
|
||||
Image::query()->whereIn('type', $types)
|
||||
->chunk(1000, function ($images) use ($checkRevisions, &$deletedPaths, $dryRun) {
|
||||
foreach ($images as $image) {
|
||||
$searchQuery = '%' . basename($image->path) . '%';
|
||||
@ -492,6 +501,14 @@ class ImageService
|
||||
}
|
||||
|
||||
$storagePath = $this->adjustPathForStorageDisk($storagePath);
|
||||
|
||||
// Apply access control when local_secure_restricted images are active
|
||||
if ($this->usingSecureRestrictedImages()) {
|
||||
if (!$this->checkUserHasAccessToRelationOfImageAtPath($storagePath)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$storage = $this->getStorageDisk();
|
||||
$imageData = null;
|
||||
if ($storage->exists($storagePath)) {
|
||||
@ -511,14 +528,19 @@ class ImageService
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given path exists in the local secure image system.
|
||||
* Returns false if local_secure is not in use.
|
||||
* Check if the given path exists and is accessible in the local secure image system.
|
||||
* Returns false if local_secure is not in use, if the file does not exist, if the
|
||||
* file is likely not a valid image, or if permission does not allow access.
|
||||
*/
|
||||
public function pathExistsInLocalSecure(string $imagePath): bool
|
||||
public function pathAccessibleInLocalSecure(string $imagePath): bool
|
||||
{
|
||||
/** @var FilesystemAdapter $disk */
|
||||
$disk = $this->getStorageDisk('gallery');
|
||||
|
||||
if ($this->usingSecureRestrictedImages() && !$this->checkUserHasAccessToRelationOfImageAtPath($imagePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check local_secure is active
|
||||
return $this->usingSecureImages()
|
||||
&& $disk instanceof FilesystemAdapter
|
||||
@ -528,6 +550,55 @@ class ImageService
|
||||
&& strpos($disk->getMimetype($imagePath), 'image/') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the current user has access to the relation
|
||||
* of the image at the given path.
|
||||
*/
|
||||
protected function checkUserHasAccessToRelationOfImageAtPath(string $path): bool
|
||||
{
|
||||
if (strpos($path, '/uploads/images/') === 0) {
|
||||
$path = substr($path, 15);
|
||||
}
|
||||
|
||||
// Strip thumbnail element from path if existing
|
||||
$originalPathSplit = array_filter(explode('/', $path), function (string $part) {
|
||||
$resizedDir = (strpos($part, 'thumbs-') === 0 || strpos($part, 'scaled-') === 0);
|
||||
$missingExtension = strpos($part, '.') === false;
|
||||
|
||||
return !($resizedDir && $missingExtension);
|
||||
});
|
||||
|
||||
// Build a database-format image path and search for the image entry
|
||||
$fullPath = '/uploads/images/' . ltrim(implode('/', $originalPathSplit), '/');
|
||||
$image = Image::query()->where('path', '=', $fullPath)->first();
|
||||
|
||||
if (is_null($image)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$imageType = $image->type;
|
||||
|
||||
// Allow user or system (logo) images
|
||||
// (No specific relation control but may still have access controlled by auth)
|
||||
if ($imageType === 'user' || $imageType === 'system') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($imageType === 'gallery' || $imageType === 'drawio') {
|
||||
return Page::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||
}
|
||||
|
||||
if ($imageType === 'cover_book') {
|
||||
return Book::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||
}
|
||||
|
||||
if ($imageType === 'cover_bookshelf') {
|
||||
return Bookshelf::visible()->where('id', '=', $image->uploaded_to)->exists();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the given path, if existing, provide a response that will stream the image contents.
|
||||
*/
|
||||
|
@ -45,10 +45,11 @@ class HtmlContentFilter
|
||||
$badIframes = $xPath->query('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
|
||||
static::removeNodes($badIframes);
|
||||
|
||||
// Remove tags hiding JavaScript or data uris in values attribute.
|
||||
// Remove attributes, within svg children, hiding JavaScript or data uris.
|
||||
// A bunch of svg element and attribute combinations expose xss possibilities.
|
||||
// For example, SVG animate tag can exploit javascript in values.
|
||||
$badValuesTags = $xPath->query('//*[' . static::xpathContains('@values', 'data:') . '] | //*[' . static::xpathContains('@values', 'javascript:') . ']');
|
||||
static::removeNodes($badValuesTags);
|
||||
$badValuesAttrs = $xPath->query('//svg//@*[' . static::xpathContains('.', 'data:') . '] | //svg//@*[' . static::xpathContains('.', 'javascript:') . ']');
|
||||
static::removeAttributes($badValuesAttrs);
|
||||
|
||||
// Remove elements with a xlink:href attribute
|
||||
// Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
|
||||
|
133
app/Util/LanguageManager.php
Normal file
133
app/Util/LanguageManager.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Util;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LanguageManager
|
||||
{
|
||||
/**
|
||||
* Array of right-to-left language options.
|
||||
*/
|
||||
protected array $rtlLanguages = ['ar', 'fa', 'he'];
|
||||
|
||||
/**
|
||||
* Map of BookStack language names to best-estimate ISO and windows locale names.
|
||||
* Locales can often be found by running `locale -a` on a linux system.
|
||||
* Windows locales can be found at:
|
||||
* https://docs.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=msvc-170.
|
||||
*
|
||||
* @var array<string, array{iso: string, windows: string}>
|
||||
*/
|
||||
protected array $localeMap = [
|
||||
'ar' => ['iso' => 'ar', 'windows' => 'Arabic'],
|
||||
'bg' => ['iso' => 'bg_BG', 'windows' => 'Bulgarian'],
|
||||
'bs' => ['iso' => 'bs_BA', 'windows' => 'Bosnian (Latin)'],
|
||||
'ca' => ['iso' => 'ca', 'windows' => 'Catalan'],
|
||||
'da' => ['iso' => 'da_DK', 'windows' => 'Danish'],
|
||||
'de' => ['iso' => 'de_DE', 'windows' => 'German'],
|
||||
'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'],
|
||||
'en' => ['iso' => 'en_GB', 'windows' => 'English'],
|
||||
'es' => ['iso' => 'es_ES', 'windows' => 'Spanish'],
|
||||
'es_AR' => ['iso' => 'es_AR', 'windows' => 'Spanish'],
|
||||
'et' => ['iso' => 'et_EE', 'windows' => 'Estonian'],
|
||||
'eu' => ['iso' => 'eu_ES', 'windows' => 'Basque'],
|
||||
'fa' => ['iso' => 'fa_IR', 'windows' => 'Persian'],
|
||||
'fr' => ['iso' => 'fr_FR', 'windows' => 'French'],
|
||||
'he' => ['iso' => 'he_IL', 'windows' => 'Hebrew'],
|
||||
'hr' => ['iso' => 'hr_HR', 'windows' => 'Croatian'],
|
||||
'hu' => ['iso' => 'hu_HU', 'windows' => 'Hungarian'],
|
||||
'id' => ['iso' => 'id_ID', 'windows' => 'Indonesian'],
|
||||
'it' => ['iso' => 'it_IT', 'windows' => 'Italian'],
|
||||
'ja' => ['iso' => 'ja', 'windows' => 'Japanese'],
|
||||
'ko' => ['iso' => 'ko_KR', 'windows' => 'Korean'],
|
||||
'lt' => ['iso' => 'lt_LT', 'windows' => 'Lithuanian'],
|
||||
'lv' => ['iso' => 'lv_LV', 'windows' => 'Latvian'],
|
||||
'nl' => ['iso' => 'nl_NL', 'windows' => 'Dutch'],
|
||||
'nb' => ['iso' => 'nb_NO', 'windows' => 'Norwegian (Bokmal)'],
|
||||
'pl' => ['iso' => 'pl_PL', 'windows' => 'Polish'],
|
||||
'pt' => ['iso' => 'pt_PT', 'windows' => 'Portuguese'],
|
||||
'pt_BR' => ['iso' => 'pt_BR', 'windows' => 'Portuguese'],
|
||||
'ro' => ['iso' => 'ro_RO', 'windows' => 'Romanian'],
|
||||
'ru' => ['iso' => 'ru', 'windows' => 'Russian'],
|
||||
'sk' => ['iso' => 'sk_SK', 'windows' => 'Slovak'],
|
||||
'sl' => ['iso' => 'sl_SI', 'windows' => 'Slovenian'],
|
||||
'sv' => ['iso' => 'sv_SE', 'windows' => 'Swedish'],
|
||||
'uk' => ['iso' => 'uk_UA', 'windows' => 'Ukrainian'],
|
||||
'vi' => ['iso' => 'vi_VN', 'windows' => 'Vietnamese'],
|
||||
'zh_CN' => ['iso' => 'zh_CN', 'windows' => 'Chinese (Simplified)'],
|
||||
'zh_TW' => ['iso' => 'zh_TW', 'windows' => 'Chinese (Traditional)'],
|
||||
'tr' => ['iso' => 'tr_TR', 'windows' => 'Turkish'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the language specifically for the currently logged-in user if available.
|
||||
*/
|
||||
public function getUserLanguage(Request $request, string $default): string
|
||||
{
|
||||
try {
|
||||
$user = user();
|
||||
} catch (\Exception $exception) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($user->isDefault() && config('app.auto_detect_locale')) {
|
||||
return $this->autoDetectLocale($request, $default);
|
||||
}
|
||||
|
||||
return setting()->getUser($user, 'language', $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given BookStack language value is a right-to-left language.
|
||||
*/
|
||||
public function isRTL(string $language): bool
|
||||
{
|
||||
return in_array($language, $this->rtlLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect the visitors locale by matching locales in their headers
|
||||
* against the locales supported by BookStack.
|
||||
*/
|
||||
protected function autoDetectLocale(Request $request, string $default): string
|
||||
{
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (in_array($lang, $availableLocales)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO version of a BookStack language name.
|
||||
*/
|
||||
public function getIsoName(string $language): string
|
||||
{
|
||||
return $this->localeMap[$language]['iso'] ?? $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the system date locale for localized date formatting.
|
||||
* Will try both the standard locale name and the UTF8 variant.
|
||||
*/
|
||||
public function setPhpDateTimeLocale(string $language): void
|
||||
{
|
||||
$isoLang = $this->localeMap[$language]['iso'] ?? false;
|
||||
|
||||
$locales = array_filter([
|
||||
$isoLang ? $isoLang . '.utf8' : false,
|
||||
$isoLang ?: false,
|
||||
$isoLang ? str_replace('_', '-', $isoLang) : false,
|
||||
$this->localeMap[$language]['windows'] ?? false,
|
||||
$language,
|
||||
]);
|
||||
|
||||
if (!empty($locales)) {
|
||||
setlocale(LC_TIME, ...$locales);
|
||||
}
|
||||
}
|
||||
}
|
915
composer.lock
generated
915
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateReferencesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('references', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('from_id')->index();
|
||||
$table->string('from_type', 25)->index();
|
||||
$table->unsignedInteger('to_id')->index();
|
||||
$table->string('to_type', 25)->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('references');
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FixShelfCoverImageTypes extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// This updates the 'type' field for images, uploaded as shelf cover images,
|
||||
// to be cover_bookshelf instead of cover_book.
|
||||
// This does not fix their paths, since fixing that requires a more complicated operation,
|
||||
// but their path does not affect functionality at time of this fix.
|
||||
|
||||
$shelfImageIds = DB::table('bookshelves')
|
||||
->whereNotNull('image_id')
|
||||
->pluck('image_id')
|
||||
->values()->all();
|
||||
|
||||
if (count($shelfImageIds) > 0) {
|
||||
DB::table('images')
|
||||
->where('type', '=', 'cover_book')
|
||||
->whereIn('id', $shelfImageIds)
|
||||
->update(['type' => 'cover_bookshelf']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
DB::table('images')
|
||||
->where('type', '=', 'cover_bookshelf')
|
||||
->update(['type' => 'cover_book']);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Search\SearchIndex;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
@ -8,7 +8,7 @@ use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Search\SearchIndex;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
12
readme.md
12
readme.md
@ -64,7 +64,7 @@ Below is a high-level road map view for BookStack to provide a sense of directio
|
||||
- **Platform REST API** - *(Most actions implemented, maturing)*
|
||||
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
|
||||
- **Editor Alignment & Review** - *(Done)*
|
||||
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
|
||||
- *Review the page editors with the goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
|
||||
- **Permission System Review** - *(In Progress)*
|
||||
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
|
||||
- **Installation & Deployment Process Revamp**
|
||||
@ -77,12 +77,12 @@ BookStack releases are each assigned a date-based version number in the format `
|
||||
- `v20.12` - New feature released launched during December 2020.
|
||||
- `v21.06.2` - Second patch release upon the June 2021 feature release.
|
||||
|
||||
Patch releases are generally fairly minor, primarily intended for fixes and therefore is fairly unlikely to cause breakages upon update.
|
||||
Patch releases are generally fairly minor, primarily intended for fixes and therefore are fairly unlikely to cause breakages upon update.
|
||||
Feature releases are generally larger, bringing new features in addition to fixes and enhancements. These releases have a greater chance of introducing breaking changes upon update, so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/).
|
||||
|
||||
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
|
||||
|
||||
Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
|
||||
Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
|
||||
|
||||
## 🛠️ Development & Testing
|
||||
|
||||
@ -170,7 +170,7 @@ NB : For some editors like Visual Studio Code, you might need to map your worksp
|
||||
|
||||
## 🌎 Translations
|
||||
|
||||
Translations for text within BookStack is managed through the [BookStack project on Crowdin](https://crowdin.com/project/bookstack). Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time. Crowdin is the preferred way to provide translations, otherwise the raw translations files can be found within the `resources/lang` path.
|
||||
Translations for text within BookStack is managed through the [BookStack project on Crowdin](https://crowdin.com/project/bookstack). Some strings have colon-prefixed variables such as `:userName`. Leave these values as they are as they will be replaced at run-time. Crowdin is the preferred way to provide translations, otherwise the raw translations files can be found within the `resources/lang` path.
|
||||
|
||||
If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://github.com/BookStackApp/BookStack/issues/new?template=language_request.md).
|
||||
|
||||
@ -180,7 +180,7 @@ Please note, translations in BookStack are provided to the "Crowdin Global Trans
|
||||
|
||||
Feel free to create issues to request new features or to report bugs & problems. Just please follow the template given when creating the issue.
|
||||
|
||||
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project.
|
||||
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 into 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 `development` branch since they will be merged back into `development` 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.
|
||||
|
||||
@ -239,4 +239,4 @@ Note: This is not an exhaustive list of all libraries and projects that would be
|
||||
* [Bacon/BaconQrCode](https://github.com/Bacon/BaconQrCode) - _[BSD-2-Clause](https://github.com/Bacon/BaconQrCode/blob/master/LICENSE)_
|
||||
* [phpseclib](https://github.com/phpseclib/phpseclib) - _[MIT](https://github.com/phpseclib/phpseclib/blob/master/LICENSE)_
|
||||
* [Clockwork](https://github.com/itsgoingd/clockwork) - _[MIT](https://github.com/itsgoingd/clockwork/blob/master/LICENSE)_
|
||||
* [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan) - _[MIT](https://github.com/phpstan/phpstan/blob/master/LICENSE) and [MIT](https://github.com/nunomaduro/larastan/blob/master/LICENSE.md)_
|
||||
* [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan) - _[MIT](https://github.com/phpstan/phpstan/blob/master/LICENSE) and [MIT](https://github.com/nunomaduro/larastan/blob/master/LICENSE.md)_
|
||||
|
@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 389 B |
3
resources/icons/reference.svg
Normal file
3
resources/icons/reference.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m24 44.15c-0.4 0-0.79167-0.06667-1.175-0.2-0.38333-0.13333-0.70833-0.33333-0.975-0.6l-17.2-17.2c-0.26667-0.26667-0.46667-0.59167-0.6-0.975s-0.2-0.775-0.2-1.175 0.066667-0.79167 0.2-1.175c0.13333-0.38333 0.33333-0.70833 0.6-0.975l17.2-17.2c0.26667-0.26667 0.59167-0.46667 0.975-0.6s0.775-0.2 1.175-0.2 0.79167 0.066667 1.175 0.2c0.38333 0.13333 0.70833 0.33333 0.975 0.6l17.2 17.2c0.26667 0.26667 0.46667 0.59167 0.6 0.975s0.2 0.775 0.2 1.175-0.06667 0.79167-0.2 1.175c-0.13333 0.38333-0.33333 0.70833-0.6 0.975l-17.2 17.2c-0.26667 0.26667-0.59167 0.46667-0.975 0.6s-0.775 0.2-1.175 0.2zm4.05-18.65-5.15 5.15c-0.3 0.3-0.44167 0.65-0.425 1.05 0.01667 0.4 0.175 0.75 0.475 1.05s0.65833 0.45 1.075 0.45 0.775-0.15 1.075-0.45l7.7-7.7c0.3-0.3 0.45-0.65 0.45-1.05s-0.15-0.75-0.45-1.05l-7.75-7.75c-0.3-0.3-0.65-0.45-1.05-0.45s-0.75 0.15-1.05 0.45-0.45 0.65833-0.45 1.075 0.15 0.775 0.45 1.075l5.1 5.15h-12.4c-0.43333 0-0.79167 0.14167-1.075 0.425s-0.425 0.64167-0.425 1.075 0.14167 0.79167 0.425 1.075 0.64167 0.425 1.075 0.425z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -37,12 +37,8 @@ class DropDown {
|
||||
if (this.moveMenu) {
|
||||
this.body.appendChild(this.menu);
|
||||
this.menu.style.position = 'fixed';
|
||||
if (this.direction === 'right') {
|
||||
this.menu.style.right = `${(menuOriginalRect.right - menuOriginalRect.width)}px`;
|
||||
} else {
|
||||
this.menu.style.left = `${menuOriginalRect.left}px`;
|
||||
}
|
||||
this.menu.style.width = `${menuOriginalRect.width}px`;
|
||||
this.menu.style.left = `${menuOriginalRect.left}px`;
|
||||
heightOffset = dropUpwards ? (window.innerHeight - menuOriginalRect.top - toggleHeight / 2) : menuOriginalRect.top;
|
||||
}
|
||||
|
||||
@ -94,6 +90,7 @@ class DropDown {
|
||||
this.menu.style.position = '';
|
||||
this.menu.style[this.direction] = '';
|
||||
this.menu.style.width = '';
|
||||
this.menu.style.left = '';
|
||||
this.container.appendChild(this.menu);
|
||||
}
|
||||
|
||||
|
@ -18,11 +18,13 @@ function showDrawingManager(mceEditor, selectedNode = null) {
|
||||
// Show image manager
|
||||
window.ImageManager.show(function (image) {
|
||||
if (selectedNode) {
|
||||
let imgElem = selectedNode.querySelector('img');
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', image.url);
|
||||
pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
|
||||
const imgElem = selectedNode.querySelector('img');
|
||||
pageEditor.undoManager.transact(function () {
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', image.url);
|
||||
pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
|
||||
});
|
||||
} else {
|
||||
let imgHTML = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
|
||||
const imgHTML = `<div drawio-diagram="${image.id}" contenteditable="false"><img src="${image.url}"></div>`;
|
||||
pageEditor.insertContent(imgHTML);
|
||||
}
|
||||
}, 'drawio');
|
||||
@ -53,8 +55,10 @@ async function updateContent(pngData) {
|
||||
let imgElem = currentNode.querySelector('img');
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, options.pageId);
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||
pageEditor.undoManager.transact(function () {
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||
});
|
||||
} catch (err) {
|
||||
handleUploadError(err);
|
||||
}
|
||||
@ -66,8 +70,10 @@ async function updateContent(pngData) {
|
||||
DrawIO.close();
|
||||
try {
|
||||
const img = await DrawIO.upload(pngData, options.pageId);
|
||||
pageEditor.dom.setAttrib(id, 'src', img.url);
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||
pageEditor.undoManager.transact(function () {
|
||||
pageEditor.dom.setAttrib(id, 'src', img.url);
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||
});
|
||||
} catch (err) {
|
||||
pageEditor.dom.remove(id);
|
||||
handleUploadError(err);
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'تم إعادة فرز الكتاب بنجاح',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'تم إنشاء رف كتب',
|
||||
'bookshelf_create_notification' => 'تم إنشاء الرف بنجاح',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'تم تحديث الرف',
|
||||
'bookshelf_update_notification' => 'تم تحديث الرف بنجاح',
|
||||
'bookshelf_delete' => 'تم تحديث الرف',
|
||||
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => 'تم إضافة ":name" إلى المفضلة لديك',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'مُحدث :timeLength',
|
||||
'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',
|
||||
'meta_owned_name' => 'Owned by :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'اختيار الكيان',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'صور',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'رف',
|
||||
'shelves' => 'الأرفف',
|
||||
'x_shelves' => ':count رف|:count أرفف',
|
||||
'shelves_long' => 'أرفف الكتب',
|
||||
'shelves_empty' => 'لم ينشأ أي رف',
|
||||
'shelves_create' => 'إنشاء رف جديد',
|
||||
'shelves_popular' => 'أرفف رائجة',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
|
||||
'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
|
||||
'shelves_edit_named' => 'تحرير رف الكتب :name',
|
||||
'shelves_edit' => 'تحرير رف الكتب',
|
||||
'shelves_delete' => 'حذف رف الكتب',
|
||||
'shelves_delete_named' => 'حذف رف الكتب :name',
|
||||
'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب المسمى ':name'، ولن تحذف الكتب المتضمنة فيه.",
|
||||
'shelves_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الرف؟',
|
||||
'shelves_permissions' => 'أذونات رف الكتب',
|
||||
'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب',
|
||||
'shelves_permissions_active' => 'أذونات رف الكتب نشطة',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
|
||||
'shelves_copy_permissions' => 'نسخ الأذونات',
|
||||
'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.',
|
||||
'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى :count books',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'كتاب',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'تعديل المحتوى',
|
||||
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
|
||||
'pages_initial_revision' => 'نشر مبدئي',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'صفحة جديدة',
|
||||
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'الكيان غير موجود',
|
||||
'bookshelf_not_found' => 'رف الكتب غير موجود',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'لم يتم العثور على الكتاب',
|
||||
'page_not_found' => 'لم يتم العثور على الصفحة',
|
||||
'chapter_not_found' => 'لم يتم العثور على الفصل',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'تهانينا! كما تلقيت إشعار هذا البريد الإلكتروني، يبدو أن إعدادات البريد الإلكتروني الخاص بك قد تم تكوينها بشكل صحيح.',
|
||||
'maint_recycle_bin_desc' => 'تُرسل الأرفف والكتب والفصول والصفحات المحذوفة إلى سلة المحذوفات حتى يمكن استعادتها أو حذفها نهائيًا. قد يتم إزالة العناصر الأقدم في سلة المحذوفات تلقائيًا بعد فترة اعتمادًا على تكوين النظام.',
|
||||
'maint_recycle_bin_open' => 'افتح سلة المحذوفات',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'سلة المحذوفات',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.',
|
||||
'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.',
|
||||
'role_asset_admins' => 'يُمنح المسؤولين حق الوصول تلقائيًا إلى جميع المحتويات ولكن هذه الخيارات قد تعرض خيارات واجهة المستخدم أو تخفيها.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'الكل',
|
||||
'role_own' => 'ما يخص',
|
||||
'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Книгата е преподредена успешно',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'създаден рафт',
|
||||
'bookshelf_create_notification' => 'Рафтът е създаден успешно',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'обновен рафт',
|
||||
'bookshelf_update_notification' => 'Рафтът е обновен успешно',
|
||||
'bookshelf_delete' => 'изтрит рафт',
|
||||
'bookshelf_delete_notification' => 'Рафтът е изтрит успешно',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" е добавен към любими успешно',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Актуализирано :timeLength',
|
||||
'meta_updated_name' => 'Актуализирано преди :timeLength от :user',
|
||||
'meta_owned_name' => 'Притежавано от :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Избор на обект',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Изображения',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Рафт',
|
||||
'shelves' => 'Рафтове',
|
||||
'x_shelves' => ':count Рафт|:count Рафтове',
|
||||
'shelves_long' => 'Рафтове с книги',
|
||||
'shelves_empty' => 'Няма създадени рафтове',
|
||||
'shelves_create' => 'Създай нов рафт',
|
||||
'shelves_popular' => 'Популярни рафтове',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'Този рафт няма добавени книги',
|
||||
'shelves_edit_and_assign' => 'Редактирай рафта за да добавиш книги',
|
||||
'shelves_edit_named' => 'Редактирай рафт с книги :name',
|
||||
'shelves_edit' => 'Редактирай рафт с книги',
|
||||
'shelves_delete' => 'Изтрий рафт с книги',
|
||||
'shelves_delete_named' => 'Изтрий рафт с книги :name',
|
||||
'shelves_delete_explain' => "Ще бъде изтрит рафта с книги със следното име ':name'. Съдържащите се книги няма да бъдат изтрити.",
|
||||
'shelves_delete_confirmation' => 'Сигурни ли сте, че искате да изтриете този рафт с книги?',
|
||||
'shelves_permissions' => 'Настройки за достъп до рафта с книги',
|
||||
'shelves_permissions_updated' => 'Настройките за достъп до рафта с книги е обновен',
|
||||
'shelves_permissions_active' => 'Настройките за достъп до рафта с книги е активен',
|
||||
'shelves_permissions_cascade_warning' => 'Привилегиите на рафтовете не се разпространяват автоматично към съдържаните в тях книги. Това е така, защото една книга може да съществува на няколко различни рафта. Въпреки това, привилегиите могат да бъдат копирани до книгите вътре чрез опцията отдолу.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'Копирай настойките за достъп към книгите',
|
||||
'shelves_copy_permissions' => 'Копирай настройките за достъп',
|
||||
'shelves_copy_permissions_explain' => 'Това ще приложи настоящите настройки за достъп на този рафт с книги за всички книги, съдържащи се в него. Преди да активирате, уверете се, че всички промени в настройките за достъп на този рафт са запазени.',
|
||||
'shelves_copy_permission_success' => 'Настройките за достъп на рафта с книги бяха копирани върху :count books',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Книга',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Редактиране на съдържанието',
|
||||
'pages_permissions_active' => 'Настройките за достъп до страницата са активни',
|
||||
'pages_initial_revision' => 'Първо публикуване',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'Нова страница',
|
||||
'pages_editing_draft_notification' => 'В момента редактирате чернова, която беше последно обновена :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Тази страница беше актуализирана от тогава. Препоръчително е да изтриете настоящата чернова.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Обектът не е намерен',
|
||||
'bookshelf_not_found' => 'Рафтът не е намерен',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Книгата не е намерена',
|
||||
'page_not_found' => 'Страницата не е намерена',
|
||||
'chapter_not_found' => 'Главата не е намерена',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Поздравления! След като получихте този имейл, Вашите имейл настройки са конфигурирани правилно.',
|
||||
'maint_recycle_bin_desc' => 'Изтрити рафти, книги, глави и страници се преместват в кошчето, откъдето можете да ги възстановите или изтриете завинаги. Стари съдържания в кошчето ще бъдат изтрити автоматично след време, в зависимост от настройките на системата.',
|
||||
'maint_recycle_bin_open' => 'Отвори Кошчето',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Кошче',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',
|
||||
'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',
|
||||
'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'Всички',
|
||||
'role_own' => 'Собствени',
|
||||
'role_controlled_by_asset' => 'Контролирани от актива, към който са качени',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'je ažurirao/la policu za knjige',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" je dodan u tvoje favorite',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Ažurirana :timeLength',
|
||||
'meta_updated_name' => 'Ažurirana :timeLength od :user',
|
||||
'meta_owned_name' => 'Vlasnik je :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Odaberi entitet',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Slike',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'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',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'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_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'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',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Book',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Uredi sadržaj',
|
||||
'pages_permissions_active' => 'Dozvole za stranicu su aktivne',
|
||||
'pages_initial_revision' => 'Prvo izdavanje',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'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.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Entitet nije pronađen',
|
||||
'bookshelf_not_found' => 'Polica za knjige nije pronađena',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Knjiga nije pronađena',
|
||||
'page_not_found' => 'Stranica nije pronađena',
|
||||
'chapter_not_found' => 'Poglavlje nije pronađeno',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
|
||||
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
|
||||
'maint_recycle_bin_open' => 'Open Recycle Bin',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Recycle Bin',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
|
||||
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
|
||||
'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'All',
|
||||
'role_own' => 'Own',
|
||||
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'ha actualitzat el prestatge',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_delete' => 'ha suprimit un prestatge',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Actualitzat :timeLength',
|
||||
'meta_updated_name' => 'Actualitzat :timeLength per :user',
|
||||
'meta_owned_name' => 'Propietat de :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Selecciona una entitat',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Imatges',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Prestatge',
|
||||
'shelves' => 'Prestatges',
|
||||
'x_shelves' => ':count prestatge|:count prestatges',
|
||||
'shelves_long' => 'Prestatges',
|
||||
'shelves_empty' => 'No hi ha cap prestatge creat',
|
||||
'shelves_create' => 'Crea un prestatge nou',
|
||||
'shelves_popular' => 'Prestatges populars',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'Aquest prestatge no té cap llibre assignat',
|
||||
'shelves_edit_and_assign' => 'Editeu el prestatge per a assignar-hi llibres',
|
||||
'shelves_edit_named' => 'Edita el prestatge :name',
|
||||
'shelves_edit' => 'Edita el prestatge',
|
||||
'shelves_delete' => 'Suprimeix el prestatge',
|
||||
'shelves_delete_named' => 'Suprimeix el prestatge :name',
|
||||
'shelves_delete_explain' => "Se suprimirà el prestatge amb el nom ':name'. Els llibres que contingui no se suprimiran.",
|
||||
'shelves_delete_confirmation' => 'Segur que voleu suprimir aquest prestatge?',
|
||||
'shelves_permissions' => 'Permisos del prestatge',
|
||||
'shelves_permissions_updated' => 'S\'han actualitzat els permisos del prestatge',
|
||||
'shelves_permissions_active' => 'S\'han activat els permisos del prestatge',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'Copia els permisos als llibres',
|
||||
'shelves_copy_permissions' => 'Copia els permisos',
|
||||
'shelves_copy_permissions_explain' => 'Això aplicarà la configuració de permisos actual d\'aquest prestatge a tots els llibres que contingui. Abans d\'activar-ho, assegureu-vos que hàgiu desat qualsevol canvi als permisos d\'aquest prestatge.',
|
||||
'shelves_copy_permission_success' => 'S\'han copiat els permisos del prestatge a :count llibres',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Llibre',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Edita el contingut',
|
||||
'pages_permissions_active' => 'S\'han activat els permisos de la pàgina',
|
||||
'pages_initial_revision' => 'Publicació inicial',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'Pàgina nova',
|
||||
'pages_editing_draft_notification' => 'Esteu editant un esborrany que es va desar per darrer cop :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Aquesta pàgina s\'ha actualitzat d\'ençà d\'aleshores. Us recomanem que descarteu aquest esborrany.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'No s\'ha trobat l\'entitat',
|
||||
'bookshelf_not_found' => 'No s\'ha trobat el prestatge',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'No s\'ha trobat el llibre',
|
||||
'page_not_found' => 'No s\'ha trobat la pàgina',
|
||||
'chapter_not_found' => 'No s\'ha trobat el capítol',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Enhorabona! Com que heu rebut aquesta notificació per correu electrònic, la vostra configuració del correu electrònic sembla que està ben configurada.',
|
||||
'maint_recycle_bin_desc' => 'Els prestatges, llibres, capítols i pàgines eliminats s\'envien a la paperera de reciclatge perquè es puguin restaurar o suprimir de manera permanent. Pot ser que els elements més antics de la paperera de reciclatge se suprimeixin automàticament després d\'un temps, depenent de la configuració del sistema.',
|
||||
'maint_recycle_bin_open' => 'Obre la paperera de reciclatge',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Paperera de reciclatge',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Tingueu en compte que l\'accés a qualsevol dels tres permisos de dalt pot permetre que un usuari alteri els seus propis permisos o els privilegis d\'altres usuaris del sistema. Assigneu rols amb aquests permisos només a usuaris de confiança.',
|
||||
'role_asset_desc' => 'Aquests permisos controlen l\'accés per defecte als recursos del sistema. Els permisos de llibres, capítols i pàgines tindran més importància que aquests permisos.',
|
||||
'role_asset_admins' => 'Els administradors tenen accés automàticament a tot el contingut, però aquestes opcions poden mostrar o amagar opcions de la interfície d\'usuari.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'Tot',
|
||||
'role_own' => 'Propi',
|
||||
'role_controlled_by_asset' => 'Controlat pel recurs en què es pugen',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Kniha byla úspěšně seřazena',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'vytvořil/a knihovnu',
|
||||
'bookshelf_create_notification' => 'Knihovna byla úspěšně vytvořena',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'aktualizoval/a knihovnu',
|
||||
'bookshelf_update_notification' => 'Knihovna byla úspěšně aktualizována',
|
||||
'bookshelf_delete' => 'odstranil/a knihovnu',
|
||||
'bookshelf_delete_notification' => 'Knihovna byla úspěšně smazána',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" byla přidána do Vašich oblíbených',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Aktualizováno :timeLength',
|
||||
'meta_updated_name' => 'Aktualizováno :timeLength uživatelem :user',
|
||||
'meta_owned_name' => 'Vlastník :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Výběr entity',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Obrázky',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Knihovna',
|
||||
'shelves' => 'Knihovny',
|
||||
'x_shelves' => '{0}:count knihoven|{1}:count knihovna|[2,4]:count knihovny|[5,*]:count knihoven',
|
||||
'shelves_long' => 'Knihovny',
|
||||
'shelves_empty' => 'Nebyly vytvořeny žádné knihovny',
|
||||
'shelves_create' => 'Vytvořit novou knihovnu',
|
||||
'shelves_popular' => 'Populární knihovny',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'Tato knihovna neobsahuje žádné knihy',
|
||||
'shelves_edit_and_assign' => 'Upravit knihovnu a přiřadit knihy',
|
||||
'shelves_edit_named' => 'Upravit knihovnu :name',
|
||||
'shelves_edit' => 'Upravit knihovnu',
|
||||
'shelves_delete' => 'Odstranit knihovnu',
|
||||
'shelves_delete_named' => 'Odstranit knihovnu :name',
|
||||
'shelves_delete_explain' => "Toto odstraní knihovnu ‚:name‘. Vložené knihy nebudou odstraněny.",
|
||||
'shelves_delete_confirmation' => 'Opravdu chcete odstranit tuto knihovnu?',
|
||||
'shelves_permissions' => 'Oprávnění knihovny',
|
||||
'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována',
|
||||
'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována',
|
||||
'shelves_permissions_cascade_warning' => 'Oprávnění v Knihovnách nejsou automaticky kaskádována do obsažených knih. To proto, že kniha může existovat ve více Knihovnách. Oprávnění však lze zkopírovat do podřízených knih pomocí níže uvedené možnosti.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',
|
||||
'shelves_copy_permissions' => 'Kopírovat oprávnění',
|
||||
'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
|
||||
'shelves_copy_permission_success' => 'Oprávnění knihovny byla zkopírována na :count knih',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Kniha',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Upravit obsah',
|
||||
'pages_permissions_active' => 'Oprávnění stránky byla aktivována',
|
||||
'pages_initial_revision' => 'První vydání',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'Nová stránka',
|
||||
'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Tato stránka se od té doby změnila. Je doporučeno aktuální koncept zahodit.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Prvek nenalezen',
|
||||
'bookshelf_not_found' => 'Knihovna nenalezena',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Kniha nenalezena',
|
||||
'page_not_found' => 'Stránka nenalezena',
|
||||
'chapter_not_found' => 'Kapitola nenalezena',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.',
|
||||
'maint_recycle_bin_desc' => 'Odstraněné knihovny, knihy, kapitoly a stránky se přesouvají do Koše, aby je bylo možné obnovit nebo trvale smazat. Starší položky v koši mohou být po čase automaticky odstraněny v závislosti na konfiguraci systému.',
|
||||
'maint_recycle_bin_open' => 'Otevřít Koš',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Koš',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',
|
||||
'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',
|
||||
'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'Vše',
|
||||
'role_own' => 'Vlastní',
|
||||
'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Ail-archebwyd y llyfr yn llwyddiannus',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'creu silff lyfrau',
|
||||
'bookshelf_create_notification' => 'Silff lyfrau wedi\'i chreu\'n llwyddiannus',
|
||||
'bookshelf_create_from_book' => 'converted book to bookshelf',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Book successfully converted to a shelf',
|
||||
'bookshelf_update' => 'silff lyfrau wedi\'i diweddaru',
|
||||
'bookshelf_update_notification' => 'Diweddarwyd y silff lyfrau yn llwyddiannus',
|
||||
'bookshelf_delete' => 'silff lyfrau wedi\'i dileu',
|
||||
'bookshelf_delete_notification' => 'Silff lyfrau wedi\'i dileu\'n llwyddiannus',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => 'Mae ":name" wedi\'i ychwanegu at eich ffefrynnau',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Updated :timeLength',
|
||||
'meta_updated_name' => 'Updated :timeLength by :user',
|
||||
'meta_owned_name' => 'Owned by :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Entity Select',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Images',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Shelf',
|
||||
'shelves' => 'Shelves',
|
||||
'x_shelves' => ':count Shelf|:count Shelves',
|
||||
'shelves_long' => 'Bookshelves',
|
||||
'shelves_empty' => 'No shelves have been created',
|
||||
'shelves_create' => 'Create New Shelf',
|
||||
'shelves_popular' => 'Popular Shelves',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'This shelf has no books assigned to it',
|
||||
'shelves_edit_and_assign' => 'Edit shelf to assign books',
|
||||
'shelves_edit_named' => 'Edit Bookshelf :name',
|
||||
'shelves_edit' => 'Edit Bookshelf',
|
||||
'shelves_delete' => 'Delete Bookshelf',
|
||||
'shelves_delete_named' => 'Delete Bookshelf :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_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'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',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Book',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Edit Content',
|
||||
'pages_permissions_active' => 'Page Permissions Active',
|
||||
'pages_initial_revision' => 'Initial publish',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'New Page',
|
||||
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Convert Chapter',
|
||||
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Endid heb ei ganfod',
|
||||
'bookshelf_not_found' => 'Heb ddod o hyd i\'r silff lyfrau',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Ni chanfuwyd y llyfr',
|
||||
'page_not_found' => 'Heb ganfod y dudalen',
|
||||
'chapter_not_found' => 'Pennod heb ei chanfod',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
|
||||
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
|
||||
'maint_recycle_bin_open' => 'Open Recycle Bin',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Recycle Bin',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
|
||||
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
|
||||
'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'All',
|
||||
'role_own' => 'Own',
|
||||
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Bogen blev re-sorteret',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'oprettede bogreol',
|
||||
'bookshelf_create_notification' => 'Bogreolen blev oprettet',
|
||||
'bookshelf_create_from_book' => 'omdannede bog til bogreol',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Bogen blev omdannet til en bogreal',
|
||||
'bookshelf_update' => 'opdaterede bogreolen',
|
||||
'bookshelf_update_notification' => 'Bogreolen blev opdateret',
|
||||
'bookshelf_delete' => 'slettede bogreol',
|
||||
'bookshelf_delete_notification' => 'Bogreolen blev slettet',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" er blevet tilføjet til dine favoritter',
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'meta_updated' => 'Opdateret :timeLength',
|
||||
'meta_updated_name' => 'Opdateret :timeLength af :user',
|
||||
'meta_owned_name' => 'Ejet af :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Vælg emne',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'images' => 'Billeder',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Reol',
|
||||
'shelves' => 'Reoler',
|
||||
'x_shelves' => ':count reol|:count reoler',
|
||||
'shelves_long' => 'Bogreoler',
|
||||
'shelves_empty' => 'Ingen reoler er blevet oprettet',
|
||||
'shelves_create' => 'Opret ny reol',
|
||||
'shelves_popular' => 'Populære reoler',
|
||||
@ -91,20 +91,20 @@ return [
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_empty_contents' => 'Denne reol har ingen bøger tilknyttet til den',
|
||||
'shelves_edit_and_assign' => 'Rediger reol for at tilføje bøger',
|
||||
'shelves_edit_named' => 'Rediger reol :name',
|
||||
'shelves_edit' => 'Rediger reol',
|
||||
'shelves_delete' => 'Slet reol',
|
||||
'shelves_delete_named' => 'Slet bogreol :name',
|
||||
'shelves_delete_explain' => "Dette vil slette bogreolen med navn ':name'. Bøger heri vil ikke blive slettet.",
|
||||
'shelves_delete_confirmation' => 'Er du sikker på at du vil slette denne bogreol?',
|
||||
'shelves_permissions' => 'Reoltilladelser',
|
||||
'shelves_permissions_updated' => 'Reoltilladelser opdateret',
|
||||
'shelves_permissions_active' => 'Aktive reoltilladelser',
|
||||
'shelves_permissions_cascade_warning' => 'Tilladelser på reoler nedarves ikke automatisk til indeholdte bøger. Dette skyldes, at en bog kan eksistere på flere hylder. Tilladelser kan dog kopieres ned til underliggende bøger ved hjælp af muligheden, der findes nedenfor.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'Kopier tilladelser til bøger',
|
||||
'shelves_copy_permissions' => 'Kopier tilladelser',
|
||||
'shelves_copy_permissions_explain' => 'Dette vil anvende de aktuelle tilladelsesindstillinger på denne boghylde på alle bøger indeholdt i. Før aktivering skal du sikre dig, at ændringer i tilladelserne til denne boghylde er blevet gemt.',
|
||||
'shelves_copy_permission_success' => 'Reolstilladelser kopieret til :count bøger',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Bog',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Redigér indhold',
|
||||
'pages_permissions_active' => 'Aktive sidetilladelser',
|
||||
'pages_initial_revision' => 'Første udgivelse',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'Ny side',
|
||||
'pages_editing_draft_notification' => 'Du redigerer en kladde der sidst var gemt :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Siden har været opdateret siden da. Det er anbefalet at du kasserer denne kladde.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
|
||||
'convert_chapter' => 'Omdan Kapitel',
|
||||
'convert_chapter_confirm' => 'Er du sikker på at du vil omdanne dette kapitel?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Emne ikke fundet',
|
||||
'bookshelf_not_found' => 'Bogreol ikke fundet',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Bog ikke fundet',
|
||||
'page_not_found' => 'Side ikke fundet',
|
||||
'chapter_not_found' => 'Kapitel ikke fundet',
|
||||
|
@ -89,6 +89,10 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Tillykke! Da du har modtaget denne mailnotifikation, ser det ud som om, at dine mailindstillinger er opsat korrekt.',
|
||||
'maint_recycle_bin_desc' => 'Slettede hylder, bøger, kapitler og sider overføres til papirkurven, så de kan gendannes eller slettes permanent. Ældre elementer i papirkurven fjernes automatisk efter et stykke tid afhængigt af systemets konfiguration.',
|
||||
'maint_recycle_bin_open' => 'Åbn papirkurven',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Papirkurv',
|
||||
@ -157,6 +161,7 @@ return [
|
||||
'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.',
|
||||
'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og "assets" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.',
|
||||
'role_asset_admins' => 'Administratorer får automatisk adgang til alt indhold, men disse indstillinger kan vise eller skjule UI-indstillinger.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'Alle',
|
||||
'role_own' => 'Eget',
|
||||
'role_controlled_by_asset' => 'Styres af det medie/"asset", de uploades til',
|
||||
@ -295,6 +300,7 @@ return [
|
||||
'pl' => 'Polski',
|
||||
'pt' => 'Português',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Русский',
|
||||
'sk' => 'Slovensky',
|
||||
'sl' => 'Slovenščina',
|
||||
|
@ -38,14 +38,14 @@ return [
|
||||
'book_sort_notification' => 'Das Buch wurde erfolgreich umsortiert',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'erstelltes Bücherregal',
|
||||
'bookshelf_create_notification' => 'Das Bücherregal wurde erfolgreich erstellt',
|
||||
'bookshelf_create_from_book' => 'umgewandeltes Buch zum Regal',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Buch erfolgreich in ein Regal konvertiert',
|
||||
'bookshelf_update' => 'hat das Bücherregal geändert',
|
||||
'bookshelf_update_notification' => 'Das Bücherregal wurde erfolgreich geändert',
|
||||
'bookshelf_delete' => 'hat das Bücherregal gelöscht',
|
||||
'bookshelf_delete_notification' => 'Das Bücherregal wurde erfolgreich gelöscht',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
|
||||
|
@ -25,12 +25,12 @@ return [
|
||||
'forgot_password' => 'Passwort vergessen?',
|
||||
'remember_me' => 'Angemeldet bleiben',
|
||||
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
|
||||
'create_account' => 'Account registrieren',
|
||||
'already_have_account' => 'Bereits ein Konto erstellt?',
|
||||
'dont_have_account' => 'Noch kein Konto erstellt?',
|
||||
'create_account' => 'Account erstellen',
|
||||
'already_have_account' => 'Sie haben bereits einen Account?',
|
||||
'dont_have_account' => 'Sie haben noch keinen Account?',
|
||||
'social_login' => 'Mit Sozialem Netzwerk anmelden',
|
||||
'social_registration' => 'Mit Sozialem Netzwerk registrieren',
|
||||
'social_registration_text' => 'Mit einer dieser Dienste registrieren oder anmelden',
|
||||
'social_registration_text' => 'Mit einem anderen Dienst registrieren oder anmelden.',
|
||||
|
||||
'register_thanks' => 'Vielen Dank für Ihre Registrierung!',
|
||||
'register_confirm' => 'Bitte prüfen Sie Ihren Posteingang und bestätigen Sie die Registrierung.',
|
||||
@ -75,7 +75,7 @@ return [
|
||||
'user_invite_email_action' => 'Account-Passwort festlegen',
|
||||
'user_invite_page_welcome' => 'Willkommen bei :appName!',
|
||||
'user_invite_page_text' => 'Um die Anmeldung abzuschließen und Zugriff auf :appName zu bekommen muss noch ein Passwort festgelegt werden. Dieses wird in Zukunft zum Einloggen benötigt.',
|
||||
'user_invite_page_confirm_button' => 'Passwort wiederholen',
|
||||
'user_invite_page_confirm_button' => 'Passwort bestätigen',
|
||||
'user_invite_success_login' => 'Passwort gesetzt, Sie sollten nun in der Lage sein, sich mit Ihrem Passwort an :appName anzumelden!',
|
||||
|
||||
// Multi-factor Authentication
|
||||
|
@ -9,7 +9,7 @@ return [
|
||||
'confirm' => 'Bestätigen',
|
||||
'back' => 'Zurück',
|
||||
'save' => 'Speichern',
|
||||
'continue' => 'Weiter',
|
||||
'continue' => 'Fortfahren',
|
||||
'select' => 'Auswählen',
|
||||
'toggle_all' => 'Alle umschalten',
|
||||
'more' => 'Mehr',
|
||||
@ -25,7 +25,7 @@ return [
|
||||
'actions' => 'Aktionen',
|
||||
'view' => 'Anzeigen',
|
||||
'view_all' => 'Alle anzeigen',
|
||||
'create' => 'Anlegen',
|
||||
'create' => 'Erstellen',
|
||||
'update' => 'Aktualisieren',
|
||||
'edit' => 'Bearbeiten',
|
||||
'sort' => 'Sortieren',
|
||||
@ -48,7 +48,7 @@ return [
|
||||
'filter_active' => 'Gesetzte Filter:',
|
||||
'filter_clear' => 'Filter löschen',
|
||||
'download' => 'Herunterladen',
|
||||
'open_in_tab' => 'In Neuem Tab öffnen',
|
||||
'open_in_tab' => 'In neuem Tab öffnen',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sortieroptionen',
|
||||
|
@ -157,7 +157,7 @@ return [
|
||||
'about' => 'Über den Editor',
|
||||
'about_title' => 'Über den WYSIWYG-Editor',
|
||||
'editor_license' => 'Editorlizenz & Copyright',
|
||||
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
|
||||
'editor_tiny_license' => 'Dieser Editor wurde mithilfe von :tinyLink erstellt, der unter der MIT-Lizenz bereitgestellt wird.',
|
||||
'editor_tiny_license_link' => 'Die Copyright- und Lizenzdetails von TinyMCE finden Sie hier.',
|
||||
'save_continue' => 'Speichern & Fortfahren',
|
||||
'callouts_cycle' => '(Drücken Sie weiter, um durch Typen umzuschalten)',
|
||||
|
@ -23,8 +23,9 @@ return [
|
||||
'meta_updated' => 'Zuletzt aktualisiert: :timeLength',
|
||||
'meta_updated_name' => 'Zuletzt aktualisiert: :timeLength von :user',
|
||||
'meta_owned_name' => 'Im Besitz von :user',
|
||||
'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
|
||||
'entity_select' => 'Eintrag auswählen',
|
||||
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
|
||||
'entity_select_lack_permission' => 'Sie haben nicht die benötigte Berechtigung, um dieses Element auszuwählen',
|
||||
'images' => 'Bilder',
|
||||
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
|
||||
'my_recently_viewed' => 'Kürzlich von mir angesehen',
|
||||
@ -77,7 +78,6 @@ return [
|
||||
'shelf' => 'Regal',
|
||||
'shelves' => 'Regale',
|
||||
'x_shelves' => ':count Regal|:count Regale',
|
||||
'shelves_long' => 'Bücherregal',
|
||||
'shelves_empty' => 'Es wurden noch keine Regale angelegt',
|
||||
'shelves_create' => 'Erzeuge ein Regal',
|
||||
'shelves_popular' => 'Beliebte Regale',
|
||||
@ -88,23 +88,23 @@ return [
|
||||
'shelves_save' => 'Regal speichern',
|
||||
'shelves_books' => 'Bücher in diesem Regal',
|
||||
'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',
|
||||
'shelves_drag_books' => 'Drag books below to add them to this shelf',
|
||||
'shelves_drag_books' => 'Ziehen Sie Bücher nach unten, um sie diesem Regal hinzuzufügen',
|
||||
'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',
|
||||
'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',
|
||||
'shelves_edit_named' => 'Bücherregal :name bearbeiten',
|
||||
'shelves_edit' => 'Bücherregal bearbeiten',
|
||||
'shelves_delete' => 'Bücherregal löschen',
|
||||
'shelves_delete_named' => 'Bücherregal :name löschen',
|
||||
'shelves_delete_explain' => "Sie sind im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
|
||||
'shelves_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Bücherregal löschen wollen?',
|
||||
'shelves_permissions' => 'Regal-Berechtigungen',
|
||||
'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
|
||||
'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
|
||||
'shelves_permissions_cascade_warning' => 'Die Berechtigungen in Bücherregalen werden nicht automatisch auf enthaltene Bücher kaskadiert, weil ein Buch in mehreren Regalen existieren kann. Berechtigungen können jedoch mit der unten stehenden Option in untergeordnete Bücher kopiert werden.',
|
||||
'shelves_edit_named' => 'Edit Shelf :name',
|
||||
'shelves_edit' => 'Edit Shelf',
|
||||
'shelves_delete' => 'Delete Shelf',
|
||||
'shelves_delete_named' => 'Delete Shelf :name',
|
||||
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
|
||||
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
|
||||
'shelves_permissions' => 'Shelf Permissions',
|
||||
'shelves_permissions_updated' => 'Shelf Permissions Updated',
|
||||
'shelves_permissions_active' => 'Shelf Permissions Active',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
|
||||
'shelves_copy_permissions' => 'Berechtigungen kopieren',
|
||||
'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
|
||||
'shelves_copy_permission_success' => 'Regal-Berechtigungen wurden zu :count Büchern kopiert',
|
||||
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
|
||||
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'Buch',
|
||||
@ -171,7 +171,7 @@ return [
|
||||
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
|
||||
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
|
||||
'chapters_search_this' => 'Dieses Kapitel durchsuchen',
|
||||
'chapter_sort_book' => 'Sort Book',
|
||||
'chapter_sort_book' => 'Buch sortieren',
|
||||
|
||||
// Pages
|
||||
'page' => 'Seite',
|
||||
@ -248,6 +248,7 @@ return [
|
||||
'pages_edit_content_link' => 'Inhalt bearbeiten',
|
||||
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
|
||||
'pages_initial_revision' => 'Erste Veröffentlichung',
|
||||
'pages_references_update_revision' => 'System auto-update of internal links',
|
||||
'pages_initial_name' => 'Neue Seite',
|
||||
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
|
||||
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
|
||||
@ -369,4 +370,9 @@ return [
|
||||
'convert_to_book_desc' => 'Sie können dieses Kapitel zu einem neuen Buch mit dem gleichen Inhalt umwandeln. Alle Berechtigungen für dieses Kapitel werden in das neue Buch übernommen, aber alle vom ursprünglichen Buch vererbten Berechtigungen werden nicht übernommen, daher kann es zu Änderungen im Zugriff kommen.',
|
||||
'convert_chapter' => 'Kapitel umwandeln',
|
||||
'convert_chapter_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel umwandeln möchten?',
|
||||
|
||||
// References
|
||||
'references' => 'References',
|
||||
'references_none' => 'There are no tracked references to this item.',
|
||||
'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
|
||||
];
|
||||
|
@ -58,7 +58,7 @@ return [
|
||||
|
||||
// Entities
|
||||
'entity_not_found' => 'Eintrag nicht gefunden',
|
||||
'bookshelf_not_found' => 'Regal nicht gefunden',
|
||||
'bookshelf_not_found' => 'Shelf not found',
|
||||
'book_not_found' => 'Buch nicht gefunden',
|
||||
'page_not_found' => 'Seite nicht gefunden',
|
||||
'chapter_not_found' => 'Kapitel nicht gefunden',
|
||||
|
@ -92,6 +92,10 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'maint_send_test_email_mail_text' => 'Glückwunsch! Da Sie diese E-Mail Benachrichtigung erhalten haben, scheinen Ihre E-Mail-Einstellungen korrekt konfiguriert zu sein.',
|
||||
'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Gegenstände im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.',
|
||||
'maint_recycle_bin_open' => 'Papierkorb öffnen',
|
||||
'maint_regen_references' => 'Regenerate References',
|
||||
'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
|
||||
'maint_regen_references_success' => 'Reference index has been regenerated!',
|
||||
'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Papierkorb',
|
||||
@ -160,6 +164,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'roles_system_warning' => 'Beachten Sie, dass der Zugriff auf eine der oben genannten drei Berechtigungen einem Benutzer erlauben kann, seine eigenen Berechtigungen oder die Rechte anderer im System zu ändern. Weisen Sie nur Rollen, mit diesen Berechtigungen, vertrauenswürdigen Benutzern zu.',
|
||||
'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
|
||||
'role_asset_admins' => 'Administratoren erhalten automatisch Zugriff auf alle Inhalte, aber diese Optionen können Oberflächenoptionen ein- oder ausblenden.',
|
||||
'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
|
||||
'role_all' => 'Alle',
|
||||
'role_own' => 'Eigene',
|
||||
'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
|
||||
@ -298,6 +303,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'pl' => 'Polnisch',
|
||||
'pt' => 'Portugiesisch',
|
||||
'pt_BR' => 'Portugiesisch (Brasilien)',
|
||||
'ro' => 'Română',
|
||||
'ru' => 'Russisch',
|
||||
'sk' => 'Slowenisch',
|
||||
'sl' => 'Slowenisch',
|
||||
|
@ -7,45 +7,45 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'erstellt Seite',
|
||||
'page_create_notification' => 'Die Seite wurde erfolgreich erstellt',
|
||||
'page_create_notification' => 'Seite erfolgreich erstellt',
|
||||
'page_update' => 'aktualisiert Seite',
|
||||
'page_update_notification' => 'Die Seite wurde erfolgreich aktualisiert',
|
||||
'page_update_notification' => 'Seite erfolgreich aktualisiert',
|
||||
'page_delete' => 'löscht Seite',
|
||||
'page_delete_notification' => 'Die Seite wurde erfolgreich gelöscht',
|
||||
'page_delete_notification' => 'Seite erfolgreich gelöscht',
|
||||
'page_restore' => 'stellt Seite wieder her',
|
||||
'page_restore_notification' => 'Die Seite wurde erfolgreich wiederhergestellt',
|
||||
'page_restore_notification' => 'Seite erfolgreich wiederhergestellt',
|
||||
'page_move' => 'verschiebt Seite',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'erstellt Kapitel',
|
||||
'chapter_create_notification' => 'Das Kapitel wurde erfolgreich erstellt',
|
||||
'chapter_create_notification' => 'Kapitel erfolgreich erstellt',
|
||||
'chapter_update' => 'aktualisiert Kapitel',
|
||||
'chapter_update_notification' => 'Das Kapitel wurde erfolgreich aktualisiert',
|
||||
'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert',
|
||||
'chapter_delete' => 'löscht Kapitel',
|
||||
'chapter_delete_notification' => 'Das Kapitel wurde erfolgreich gelöscht',
|
||||
'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht',
|
||||
'chapter_move' => 'verschiebt Kapitel',
|
||||
|
||||
// Books
|
||||
'book_create' => 'erstellt Buch',
|
||||
'book_create_notification' => 'Das Buch wurde erfolgreich erstellt',
|
||||
'book_create_notification' => 'Buch erfolgreich erstellt',
|
||||
'book_create_from_chapter' => 'umgewandeltes Kapitel zum Buch',
|
||||
'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch konvertiert',
|
||||
'book_update' => 'aktualisiert Buch',
|
||||
'book_update_notification' => 'Das Buch wurde erfolgreich aktualisiert',
|
||||
'book_update_notification' => 'Buch erfolgreich aktualisiert',
|
||||
'book_delete' => 'löscht Buch',
|
||||
'book_delete_notification' => 'Das Buch wurde erfolgreich gelöscht',
|
||||
'book_delete_notification' => 'Buch erfolgreich gelöscht',
|
||||
'book_sort' => 'sortiert Buch',
|
||||
'book_sort_notification' => 'Das Buch wurde erfolgreich umsortiert',
|
||||
'book_sort_notification' => 'Buch erfolgreich umsortiert',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'erstelltes Bücherregal',
|
||||
'bookshelf_create_notification' => 'Das Bücherregal wurde erfolgreich erstellt',
|
||||
'bookshelf_create_from_book' => 'umgewandeltes Buch zum Regal',
|
||||
'bookshelf_create' => 'created shelf',
|
||||
'bookshelf_create_notification' => 'Shelf successfully created',
|
||||
'bookshelf_create_from_book' => 'converted book to shelf',
|
||||
'bookshelf_create_from_book_notification' => 'Buch erfolgreich in ein Regal konvertiert',
|
||||
'bookshelf_update' => 'aktualisiert Bücherregal',
|
||||
'bookshelf_update_notification' => 'Das Bücherregal wurde erfolgreich geändert',
|
||||
'bookshelf_delete' => 'löscht Bücherregal',
|
||||
'bookshelf_delete_notification' => 'Das Bücherregal wurde erfolgreich gelöscht',
|
||||
'bookshelf_update' => 'updated shelf',
|
||||
'bookshelf_update_notification' => 'Shelf successfully updated',
|
||||
'bookshelf_delete' => 'deleted shelf',
|
||||
'bookshelf_delete_notification' => 'Shelf successfully deleted',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
|
||||
@ -57,11 +57,11 @@ return [
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'erstellter Webhook',
|
||||
'webhook_create_notification' => 'Webhook wurde erfolgreich eingerichtet',
|
||||
'webhook_create_notification' => 'Webhook erfolgreich eingerichtet',
|
||||
'webhook_update' => 'aktualisierter Webhook',
|
||||
'webhook_update_notification' => 'Webhook wurde erfolgreich aktualisiert',
|
||||
'webhook_update_notification' => 'Webhook erfolgreich aktualisiert',
|
||||
'webhook_delete' => 'gelöschter Webhook',
|
||||
'webhook_delete_notification' => 'Webhook wurde erfolgreich gelöscht',
|
||||
'webhook_delete_notification' => 'Webhook erfolgreich gelöscht',
|
||||
|
||||
// Users
|
||||
'user_update_notification' => 'Benutzer erfolgreich aktualisiert',
|
||||
|
@ -48,7 +48,7 @@ return [
|
||||
'filter_active' => 'Gesetzte Filter:',
|
||||
'filter_clear' => 'Filter löschen',
|
||||
'download' => 'Herunterladen',
|
||||
'open_in_tab' => 'In Neuem Tab öffnen',
|
||||
'open_in_tab' => 'In Tab öffnen',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sortieroptionen',
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user