Merge branch 'master' into release

This commit is contained in:
Dan Brown 2021-10-08 22:25:05 +01:00
commit a8de717d9b
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
63 changed files with 365 additions and 127 deletions

View File

@ -190,3 +190,5 @@ Hl2run :: Slovak
Ngo Tri Hoai (trihoai) :: Vietnamese
Atalonica :: Catalan
慕容潭谈 (591442386) :: Chinese Simplified
Radim Pesek (ramess18) :: Czech
anastasiia.motylko :: Ukrainian

View File

@ -37,9 +37,14 @@ return [
'root' => public_path(),
],
'local_secure' => [
'local_secure_attachments' => [
'driver' => 'local',
'root' => storage_path(),
'root' => storage_path('uploads/files/'),
],
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
],
's3' => [

View File

@ -5,6 +5,7 @@ namespace BookStack\Entities\Models;
use BookStack\Auth\User;
use BookStack\Model;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class PageRevision.
@ -14,11 +15,13 @@ use Carbon\Carbon;
* @property string $book_slug
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property string $type
* @property string $summary
* @property string $markdown
* @property string $html
* @property int $revision_number
* @property Page $page
*/
class PageRevision extends Model
{
@ -26,20 +29,16 @@ class PageRevision extends Model
/**
* Get the user that created the page revision.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the page this revision originates from.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
public function page(): BelongsTo
{
return $this->belongsTo(Page::class);
}

View File

@ -21,8 +21,6 @@ class PageEditActivity
/**
* Check if there's active editing being performed on this page.
*
* @return bool
*/
public function hasActiveEditing(): bool
{
@ -43,12 +41,38 @@ class PageEditActivity
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
}
/**
* Get any editor clash warning messages to show for the given draft revision.
*
* @param PageRevision|Page $draft
*
* @return string[]
*/
public function getWarningMessagesForDraft($draft): array
{
$warnings = [];
if ($this->hasActiveEditing()) {
$warnings[] = $this->activeEditingMessage();
}
if ($draft instanceof PageRevision && $this->hasPageBeenUpdatedSinceDraftCreated($draft)) {
$warnings[] = trans('entities.pages_draft_page_changed_since_creation');
}
return $warnings;
}
/**
* Check if the page has been updated since the draft has been saved.
*/
protected function hasPageBeenUpdatedSinceDraftCreated(PageRevision $draft): bool
{
return $draft->page->updated_at->timestamp > $draft->created_at->timestamp;
}
/**
* Get the message to show when the user will be editing one of their drafts.
*
* @param PageRevision $draft
*
* @return string
*/
public function getEditingActiveDraftMessage(PageRevision $draft): string
{

View File

@ -156,7 +156,9 @@ class SearchRunner
})->groupBy('entity_type', 'entity_id');
$entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable() . '.*, s.score')->orderBy('score', 'desc');
})->addSelect($entity->getTable() . '.*')
->selectRaw('s.score')
->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
}

View File

@ -259,13 +259,13 @@ class PageController extends Controller
}
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
$warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $updateTime,
'warning' => implode("\n", $warnings),
'timestamp' => $draft->updated_at->timestamp,
]);
}

View File

@ -30,6 +30,7 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\BookStack\Http\Middleware\PreventAuthenticatedResponseCaching::class,
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
\BookStack\Http\Middleware\RunThemeActions::class,
\BookStack\Http\Middleware\Localization::class,
@ -39,6 +40,7 @@ class Kernel extends HttpKernel
\BookStack\Http\Middleware\EncryptCookies::class,
\BookStack\Http\Middleware\StartSessionIfCookieExists::class,
\BookStack\Http\Middleware\ApiAuthenticate::class,
\BookStack\Http\Middleware\PreventAuthenticatedResponseCaching::class,
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
],
];

View File

@ -0,0 +1,31 @@
<?php
namespace BookStack\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\Response;
class PreventAuthenticatedResponseCaching
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
/** @var Response $response */
$response = $next($request);
if (signedInUser()) {
$response->headers->set('Cache-Control', 'max-age=0, no-store, private');
$response->headers->set('Pragma', 'no-cache');
$response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
}
return $response;
}
}

View File

@ -9,6 +9,7 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use League\Flysystem\Util;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
@ -28,14 +29,38 @@ class AttachmentService
*/
protected function getStorage(): FileSystemInstance
{
$storageType = config('filesystems.attachments');
// Override default location if set to local public to ensure not visible.
if ($storageType === 'local') {
$storageType = 'local_secure';
return $this->fileSystem->disk($this->getStorageDiskName());
}
return $this->fileSystem->disk($storageType);
/**
* Get the name of the storage disk to use.
*/
protected function getStorageDiskName(): string
{
$storageType = config('filesystems.attachments');
// 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') {
$storageType = 'local_secure_attachments';
}
return $storageType;
}
/**
* Change the originally provided path to fit any disk-specific requirements.
* This also ensures the path is kept to the expected root folders.
*/
protected function adjustPathForStorageDisk(string $path): string
{
$path = Util::normalizePath(str_replace('uploads/files/', '', $path));
if ($this->getStorageDiskName() === 'local_secure_attachments') {
return $path;
}
return 'uploads/files/' . $path;
}
/**
@ -45,26 +70,22 @@ class AttachmentService
*/
public function getAttachmentFromStorage(Attachment $attachment): string
{
return $this->getStorage()->get($attachment->path);
return $this->getStorage()->get($this->adjustPathForStorageDisk($attachment->path));
}
/**
* Store a new attachment upon user upload.
*
* @param UploadedFile $uploadedFile
* @param int $page_id
*
* @throws FileUploadException
*
* @return Attachment
*/
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
public function saveNewUpload(UploadedFile $uploadedFile, int $page_id): Attachment
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($uploadedFile);
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
/** @var Attachment $attachment */
$attachment = Attachment::query()->forceCreate([
'name' => $attachmentName,
'path' => $attachmentPath,
'extension' => $uploadedFile->getClientOriginalExtension(),
@ -78,17 +99,12 @@ class AttachmentService
}
/**
* Store a upload, saving to a file and deleting any existing uploads
* Store an upload, saving to a file and deleting any existing uploads
* attached to that file.
*
* @param UploadedFile $uploadedFile
* @param Attachment $attachment
*
* @throws FileUploadException
*
* @return Attachment
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment
{
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
@ -160,8 +176,6 @@ class AttachmentService
/**
* Delete a File from the database and storage.
*
* @param Attachment $attachment
*
* @throws Exception
*/
public function deleteFile(Attachment $attachment)
@ -179,15 +193,13 @@ class AttachmentService
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
*
* @param Attachment $attachment
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storage = $this->getStorage();
$dirPath = dirname($attachment->path);
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
$storage->delete($attachment->path);
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
@ -196,13 +208,9 @@ class AttachmentService
/**
* Store a file in storage with the given filename.
*
* @param UploadedFile $uploadedFile
*
* @throws FileUploadException
*
* @return string
*/
protected function putFileInStorage(UploadedFile $uploadedFile)
protected function putFileInStorage(UploadedFile $uploadedFile): string
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
@ -210,14 +218,14 @@ class AttachmentService
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
$uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
$uploadFileName = Str::random(3) . $uploadFileName;
}
$attachmentPath = $basePath . $uploadFileName;
try {
$storage->put($attachmentPath, $attachmentData);
$storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData);
} catch (Exception $e) {
Log::error('Error when attempting file upload:' . $e->getMessage());

View File

@ -14,6 +14,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use League\Flysystem\Util;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService
@ -38,16 +39,43 @@ class ImageService
/**
* Get the storage that will be used for storing images.
*/
protected function getStorage(string $type = ''): FileSystemInstance
protected function getStorage(string $imageType = ''): FileSystemInstance
{
return $this->fileSystem->disk($this->getStorageDiskName($imageType));
}
/**
* Change the originally provided path to fit any disk-specific requirements.
* This also ensures the path is kept to the expected root folders.
*/
protected function adjustPathForStorageDisk(string $path, string $imageType = ''): string
{
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
if ($this->getStorageDiskName($imageType) === 'local_secure_images') {
return $path;
}
return 'uploads/images/' . $path;
}
/**
* Get the name of the storage disk to use.
*/
protected function getStorageDiskName(string $imageType): string
{
$storageType = config('filesystems.images');
// Ensure system images (App logo) are uploaded to a public space
if ($type === 'system' && $storageType === 'local_secure') {
if ($imageType === 'system' && $storageType === 'local_secure') {
$storageType = 'local';
}
return $this->fileSystem->disk($storageType);
if ($storageType === 'local_secure') {
$storageType = 'local_secure_images';
}
return $storageType;
}
/**
@ -104,7 +132,7 @@ class ImageService
$imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/';
while ($storage->exists($imagePath . $fileName)) {
while ($storage->exists($this->adjustPathForStorageDisk($imagePath . $fileName, $type))) {
$fileName = Str::random(3) . $fileName;
}
@ -114,7 +142,7 @@ class ImageService
}
try {
$this->saveImageDataInPublicSpace($storage, $fullPath, $imageData);
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData);
} catch (Exception $e) {
\Log::error('Error when attempting image upload:' . $e->getMessage());
@ -216,13 +244,13 @@ class ImageService
}
$storage = $this->getStorage($image->type);
if ($storage->exists($thumbFilePath)) {
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
return $this->getPublicUrl($thumbFilePath);
}
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
$thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio);
$this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData);
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData);
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
return $this->getPublicUrl($thumbFilePath);
@ -279,10 +307,9 @@ class ImageService
*/
public function getImageData(Image $image): string
{
$imagePath = $image->path;
$storage = $this->getStorage();
return $storage->get($imagePath);
return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type));
}
/**
@ -292,7 +319,7 @@ class ImageService
*/
public function destroy(Image $image)
{
$this->destroyImagesFromPath($image->path);
$this->destroyImagesFromPath($image->path, $image->type);
$image->delete();
}
@ -300,9 +327,10 @@ class ImageService
* Destroys an image at the given path.
* Searches for image thumbnails in addition to main provided path.
*/
protected function destroyImagesFromPath(string $path): bool
protected function destroyImagesFromPath(string $path, string $imageType): bool
{
$storage = $this->getStorage();
$path = $this->adjustPathForStorageDisk($path, $imageType);
$storage = $this->getStorage($imageType);
$imageFolder = dirname($path);
$imageFileName = basename($path);
@ -326,7 +354,7 @@ class ImageService
}
/**
* Check whether or not a folder is empty.
* Check whether a folder is empty.
*/
protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
{
@ -374,7 +402,7 @@ class ImageService
}
/**
* Convert a image URI to a Base64 encoded string.
* Convert an image URI to a Base64 encoded string.
* Attempts to convert the URL to a system storage url then
* fetch the data from the disk or storage location.
* Returns null if the image data cannot be fetched from storage.
@ -388,6 +416,7 @@ class ImageService
return null;
}
$storagePath = $this->adjustPathForStorageDisk($storagePath);
$storage = $this->getStorage();
$imageData = null;
if ($storage->exists($storagePath)) {

38
composer.lock generated
View File

@ -4613,16 +4613,16 @@
},
{
"name": "symfony/debug",
"version": "v4.4.27",
"version": "v4.4.31",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "2f9160e92eb64c95da7368c867b663a8e34e980c"
"reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/2f9160e92eb64c95da7368c867b663a8e34e980c",
"reference": "2f9160e92eb64c95da7368c867b663a8e34e980c",
"url": "https://api.github.com/repos/symfony/debug/zipball/43ede438d4cb52cd589ae5dc070e9323866ba8e0",
"reference": "43ede438d4cb52cd589ae5dc070e9323866ba8e0",
"shasum": ""
},
"require": {
@ -4661,7 +4661,7 @@
"description": "Provides tools to ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/debug/tree/v4.4.27"
"source": "https://github.com/symfony/debug/tree/v4.4.31"
},
"funding": [
{
@ -4677,7 +4677,7 @@
"type": "tidelift"
}
],
"time": "2021-07-22T07:21:39+00:00"
"time": "2021-09-24T13:30:14+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -6863,16 +6863,16 @@
},
{
"name": "composer/ca-bundle",
"version": "1.2.10",
"version": "1.2.11",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "9fdb22c2e97a614657716178093cd1da90a64aa8"
"reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/9fdb22c2e97a614657716178093cd1da90a64aa8",
"reference": "9fdb22c2e97a614657716178093cd1da90a64aa8",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/0b072d51c5a9c6f3412f7ea3ab043d6603cb2582",
"reference": "0b072d51c5a9c6f3412f7ea3ab043d6603cb2582",
"shasum": ""
},
"require": {
@ -6884,7 +6884,7 @@
"phpstan/phpstan": "^0.12.55",
"psr/log": "^1.0",
"symfony/phpunit-bridge": "^4.2 || ^5",
"symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
"symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0"
},
"type": "library",
"extra": {
@ -6919,7 +6919,7 @@
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues",
"source": "https://github.com/composer/ca-bundle/tree/1.2.10"
"source": "https://github.com/composer/ca-bundle/tree/1.2.11"
},
"funding": [
{
@ -6935,20 +6935,20 @@
"type": "tidelift"
}
],
"time": "2021-06-07T13:58:28+00:00"
"time": "2021-09-25T20:32:43+00:00"
},
{
"name": "composer/composer",
"version": "2.1.8",
"version": "2.1.9",
"source": {
"type": "git",
"url": "https://github.com/composer/composer.git",
"reference": "24d38e9686092de05214cafa187dc282a5d89497"
"reference": "e558c88f28d102d497adec4852802c0dc14c7077"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/composer/zipball/24d38e9686092de05214cafa187dc282a5d89497",
"reference": "24d38e9686092de05214cafa187dc282a5d89497",
"url": "https://api.github.com/repos/composer/composer/zipball/e558c88f28d102d497adec4852802c0dc14c7077",
"reference": "e558c88f28d102d497adec4852802c0dc14c7077",
"shasum": ""
},
"require": {
@ -7017,7 +7017,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/composer/issues",
"source": "https://github.com/composer/composer/tree/2.1.8"
"source": "https://github.com/composer/composer/tree/2.1.9"
},
"funding": [
{
@ -7033,7 +7033,7 @@
"type": "tidelift"
}
],
"time": "2021-09-15T11:55:15+00:00"
"time": "2021-10-05T07:47:38+00:00"
},
{
"name": "composer/metadata-minifier",

View File

@ -40,6 +40,7 @@ class PageEditor {
frequency: 30000,
last: 0,
};
this.shownWarningsCache = new Set();
if (this.pageId !== 0 && this.draftsEnabled) {
window.setTimeout(() => {
@ -119,6 +120,10 @@ class PageEditor {
}
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
this.autoSave.last = Date.now();
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
window.$events.emit('warning', resp.data.warning);
this.shownWarningsCache.add(resp.data.warning);
}
} catch (err) {
// Save the editor content in LocalStorage as a last resort, just in case.
try {

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'صفحة جديدة',
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
'start_b' => ':userName بدأ بتعديل هذه الصفحة',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Нова страница',
'pages_editing_draft_notification' => 'В момента редактирате чернова, която беше последно обновена :timeDiff.',
'pages_draft_edited_notification' => 'Тази страница беше актуализирана от тогава. Препоръчително е да изтриете настоящата чернова.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count потребителя започнаха да редактират настоящата страница',
'start_b' => ':userName в момента редактира тази страница',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nova stranica',
'pages_editing_draft_notification' => 'Trenutno uređujete skicu koja je posljednji put snimljena :timeDiff.',
'pages_draft_edited_notification' => 'Ova stranica je ažurirana nakon tog vremena. Preporučujemo da odbacite ovu skicu.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count korisnika je počelo sa uređivanjem ove stranice',
'start_b' => ':userName je počeo/la sa uređivanjem ove stranice',

View File

@ -234,6 +234,7 @@ return [
'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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count usuaris han començat a editar aquesta pàgina',
'start_b' => ':userName ha començat a editar aquesta pàgina',

View File

@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" byla odstraněna z Vašich oblíbených',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Vícefaktorová metoda byla úspěšně nakonfigurována',
'mfa_remove_method_notification' => 'Vícefaktorová metoda byla úspěšně odstraněna',
// Other
'commented_on' => 'okomentoval/a',

View File

@ -83,16 +83,16 @@ return [
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
'mfa_setup_action' => 'Setup',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_option_totp_title' => 'Mobile App',
'mfa_option_totp_title' => 'Mobilní aplikace',
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Backup Codes',
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
'mfa_gen_confirm_and_enable' => 'Potvrdit a povolit',
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
'mfa_gen_backup_codes_download' => 'Download Codes',
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
'mfa_gen_totp_title' => 'Mobile App Setup',
'mfa_gen_totp_title' => 'Nastavení mobilní aplikace',
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
'mfa_gen_totp_verify_setup' => 'Verify Setup',

View File

@ -39,7 +39,7 @@ return [
'reset' => 'Obnovit',
'remove' => 'Odebrat',
'add' => 'Přidat',
'configure' => 'Configure',
'configure' => 'Nastavit',
'fullscreen' => 'Celá obrazovka',
'favourite' => 'Přidat do oblíbených',
'unfavourite' => 'Odebrat z oblíbených',

View File

@ -99,7 +99,7 @@ return [
'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' => '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_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_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.',
@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nová stránka',
'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen před :timeDiff.',
'pages_draft_edited_notification' => 'Tato stránka se od té doby změnila. Je doporučeno aktuální koncept zahodit.',
'pages_draft_page_changed_since_creation' => 'Tato stránka byla aktualizována od vytvoření tohoto konceptu. Doporučuje se zrušit tento koncept nebo se postarat o to, abyste si nepřepsali žádné již zadané změny.',
'pages_draft_edit_active' => [
'start_a' => 'Uživatelé začali upravovat tuto stránku (celkem :count)',
'start_b' => ':userName začal/a upravovat tuto stránku',

View File

@ -119,7 +119,7 @@ return [
'audit_table_user' => 'Uživatel',
'audit_table_event' => 'Událost',
'audit_table_related' => 'Související položka nebo detail',
'audit_table_ip' => 'IP Address',
'audit_table_ip' => 'IP adresa',
'audit_table_date' => 'Datum aktivity',
'audit_date_from' => 'Časový rozsah od',
'audit_date_to' => 'Časový rozsah do',
@ -139,7 +139,7 @@ return [
'role_details' => 'Detaily role',
'role_name' => 'Název role',
'role_desc' => 'Stručný popis role',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_mfa_enforced' => 'Vyžaduje Vícefaktorové ověření',
'role_external_auth_id' => 'Přihlašovací identifikátory třetích stran',
'role_system' => 'Systémová oprávnění',
'role_manage_users' => 'Správa uživatelů',
@ -149,7 +149,7 @@ return [
'role_manage_page_templates' => 'Správa šablon stránek',
'role_access_api' => 'Přístup k systémovému API',
'role_manage_settings' => 'Správa nastavení aplikace',
'role_export_content' => 'Export content',
'role_export_content' => 'Exportovat obsah',
'role_asset' => 'Obsahová oprávnění',
'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í.',
@ -207,10 +207,10 @@ return [
'users_api_tokens_create' => 'Vytvořit Token',
'users_api_tokens_expires' => 'Vyprší',
'users_api_tokens_docs' => 'API Dokumentace',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa' => 'Vícefázové ověření',
'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users_mfa_configure' => 'Konfigurovat metody',
// API Tokens
'user_api_token_create' => 'Vytvořit API Token',

View File

@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.',
'alpha_num' => ':attribute může obsahovat pouze písmena a číslice.',
'array' => ':attribute musí být pole.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'Zadaný kód není platný nebo již byl použit.',
'before' => ':attribute musí být datum před :date.',
'between' => [
'numeric' => ':attribute musí být hodnota mezi :min a :max.',
@ -99,7 +99,7 @@ return [
],
'string' => ':attribute musí být řetězec znaků.',
'timezone' => ':attribute musí být platná časová zóna.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'Zadaný kód je neplatný nebo vypršel.',
'unique' => ':attribute musí být unikátní.',
'url' => 'Formát :attribute je neplatný.',
'uploaded' => 'Nahrávání :attribute se nezdařilo.',

View File

@ -234,6 +234,7 @@ return [
'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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count brugerer har begyndt at redigere denne side',
'start_b' => ':userName er begyndt at redigere denne side',

View File

@ -234,6 +234,7 @@ return [
'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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
'start_b' => ':userName bearbeitet jetzt diese Seite.',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Neue Seite',
'pages_editing_draft_notification' => 'Du bearbeitest 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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
'start_b' => ':userName bearbeitet jetzt diese Seite.',

View File

@ -234,6 +234,7 @@ return [
'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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Página nueva',
'pages_editing_draft_notification' => 'Está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde ese momento. Se recomienda que cancele este borrador.',
'pages_draft_page_changed_since_creation' => 'Esta página ha sido actualizada desde que se creó este borrador. Se recomienda descartar este borrador o tener cuidado de no sobrescribir ningún cambio en la página.',
'pages_draft_edit_active' => [
'start_a' => ':count usuarios han comenzado a editar esta página',
'start_b' => ':userName ha comenzado a editar esta página',

View File

@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Permisos del Estante',
'shelves_permissions_updated' => 'Permisos del Estante actualizados',
'shelves_permissions_active' => 'Permisos Activos del Estante',
'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_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros contenidos. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.',
'shelves_copy_permissions_to_books' => 'Copiar Permisos a los Libros',
'shelves_copy_permissions' => 'Copiar Permisos',
'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos de este estante a todos los libros contenidos en él. Antes de activarlos, asegúrese que los cambios a los permisos de este estante estén guardados.',
@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Página nueva',
'pages_editing_draft_notification' => 'Usted está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count usuarios han comenzado a editar esta página',
'start_b' => ':userName ha comenzado a editar esta página',

View File

@ -234,6 +234,7 @@ return [
'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.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nouvelle page',
'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été enregistré :timeDiff.',
'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez jeter ce brouillon.',
'pages_draft_page_changed_since_creation' => 'Cette page a été mise à jour depuis que ce brouillon a été créé. Il est recommandé de supprimer ce brouillon ou de veiller à ne pas écraser toute modification de page.',
'pages_draft_edit_active' => [
'start_a' => ':count utilisateurs ont commencé à éditer cette page',
'start_b' => ':userName a commencé à éditer cette page',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'דף חדש',
'pages_editing_draft_notification' => 'הינך עורך טיוטה אשר נשמרה לאחרונה ב :timeDiff',
'pages_draft_edited_notification' => 'דף זה עודכן מאז, מומלץ להתעלם מהטיוטה הזו.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count משתמשים החלו לערוך דף זה',
'start_b' => ':userName החל לערוך דף זה',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nova stranica',
'pages_editing_draft_notification' => 'Uređujete nacrt stranice posljednji put spremljen :timeDiff.',
'pages_draft_edited_notification' => 'Ova je stranica u međuvremenu ažurirana. Preporučujemo da odbacite ovaj nacrt.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count korisnika koji uređuju ovu stranicu',
'start_b' => ':userName je počeo uređivati ovu stranicu',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Új oldal',
'pages_editing_draft_notification' => 'A jelenleg szerkesztett vázlat legutóbb ekkor volt elmentve: :timeDiff.',
'pages_draft_edited_notification' => 'Ezt az oldalt azóta már frissítették. Javasolt ennek a vázlatnak az elvetése.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count felhasználók kezdte el szerkeszteni ezt az oldalt',
'start_b' => ':userName elkezdte szerkeszteni ezt az oldalt',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Halaman Baru',
'pages_editing_draft_notification' => 'Anda sedang menyunting konsep yang terakhir disimpan :timeDiff.',
'pages_draft_edited_notification' => 'Halaman ini telah diperbarui sejak saat itu. Anda disarankan untuk membuang draf ini.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count pengguna sudah mulai mengedit halaman ini',
'start_b' => ':userName telah memulai menyunting halaman ini',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nuova Pagina',
'pages_editing_draft_notification' => 'Stai modificando una bozza che è stata salvata il :timeDiff.',
'pages_draft_edited_notification' => 'Questa pagina è stata aggiornata. È consigliabile scartare questa bozza.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count hanno iniziato a modificare questa pagina',
'start_b' => ':userName ha iniziato a modificare questa pagina',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => '新規ページ',
'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',
'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count人のユーザがページの編集を開始しました',
'start_b' => ':userNameがページの編集を開始しました',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => '제목 없음',
'pages_editing_draft_notification' => ':timeDiff에 초안 문서입니다.',
'pages_draft_edited_notification' => '최근에 수정한 문서이기 때문에 초안 문서를 폐기하는 편이 좋습니다.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count명이 이 문서를 수정하고 있습니다.',
'start_b' => ':userName이 이 문서를 수정하고 있습니다.',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Naujas puslapis',
'pages_editing_draft_notification' => 'Dabar jūs redaguojate juodraštį, kuris paskutinį kartą buvo išsaugotas :timeDiff',
'pages_draft_edited_notification' => 'Šis puslapis buvo redaguotas iki to laiko. Rekomenduojame jums išmesti šį juodraštį.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count naudotojai pradėjo redaguoti šį puslapį',
'start_b' => ':userName pradėjo redaguoti šį puslapį',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Jauna lapa',
'pages_editing_draft_notification' => 'Jūs pašlaik veicat izmaiņas melnrakstā, kurš pēdējo reizi ir saglabāts :timeDiff.',
'pages_draft_edited_notification' => 'Šī lapa ir tikusi atjaunināta. Šo melnrakstu ieteicams atmest.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',
'start_b' => ':userName veic izmaiņas šajā lapā',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Ny side',
'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count forfattere har begynt å endre denne siden.',
'start_b' => ':userName skriver på siden for øyeblikket',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nieuwe pagina',
'pages_editing_draft_notification' => 'U bewerkt momenteel een concept dat voor het laatst is opgeslagen op :timeDiff.',
'pages_draft_edited_notification' => 'Deze pagina is sindsdien bijgewerkt. Het wordt aanbevolen dat u dit concept verwijderd.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count gebruikers zijn begonnen deze pagina te bewerken',
'start_b' => ':userName is begonnen met het bewerken van deze pagina',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nowa strona',
'pages_editing_draft_notification' => 'Edytujesz obecnie wersje roboczą, która była ostatnio zapisana :timeDiff.',
'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tej wersji roboczej.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony',
'start_b' => ':userName edytuje stronę',

View File

@ -6,7 +6,7 @@
return [
// Pages
'page_create' => 'página criada',
'page_create' => 'criou a página',
'page_create_notification' => 'Página criada com sucesso',
'page_update' => 'página atualizada',
'page_update_notification' => 'Página atualizada com sucesso',
@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" foi removido dos seus favoritos',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Método de múltiplos-fatores configurado com sucesso',
'mfa_remove_method_notification' => 'Método de múltiplos-fatores removido com sucesso',
// Other
'commented_on' => 'comentado a',

View File

@ -76,12 +76,12 @@ return [
'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!',
// Multi-factor Authentication
'mfa_setup' => 'Setup Multi-Factor Authentication',
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'mfa_setup_configured' => 'Already configured',
'mfa_setup_reconfigure' => 'Reconfigure',
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
'mfa_setup_action' => 'Setup',
'mfa_setup' => 'Configurar autenticação de múltiplos fatores',
'mfa_setup_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',
'mfa_setup_configured' => 'Já configurado',
'mfa_setup_reconfigure' => 'Reconfigurar',
'mfa_setup_remove_confirmation' => 'Tem a certeza que deseja remover este método de autenticação de múltiplos fatores?',
'mfa_setup_action' => 'Configuração',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_option_totp_title' => 'Mobile App',
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',

View File

@ -39,7 +39,7 @@ return [
'reset' => 'Redefinir',
'remove' => 'Remover',
'add' => 'Adicionar',
'configure' => 'Configure',
'configure' => 'Configurar',
'fullscreen' => 'Ecrã completo',
'favourite' => 'Favorito',
'unfavourite' => 'Retirar Favorito',

View File

@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Permissões da Estante',
'shelves_permissions_updated' => 'Permissões da Estante de Livros Atualizada',
'shelves_permissions_active' => 'Permissões da Estante de Livros Ativas',
'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_permissions_cascade_warning' => 'As permissões nas estantes não são passadas automaticamente em efeito dominó para os livros contidos. Isto acontece porque um livro pode existir em várias prateleiras. As permissões podem, no entanto, ser copiadas para livros filhos usando a opção encontrada abaixo.',
'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',
'shelves_copy_permissions' => 'Copiar Permissões',
'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.',
@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nova Página',
'pages_editing_draft_notification' => 'Você está atualmente a editar um rascunho que foi guardado pela última vez a :timeDiff.',
'pages_draft_edited_notification' => 'Esta página entretanto já foi atualizada. É recomendado que você descarte este rascunho.',
'pages_draft_page_changed_since_creation' => 'Esta página foi atualizada desde que este rascunho foi criado. É recomendável que descarte este rascunho ou tenha cuidado para não sobrescrever nenhuma alteração de página.',
'pages_draft_edit_active' => [
'start_a' => ':count usuários iniciaram a edição dessa página',
'start_b' => ':userName iniciou a edição desta página',

View File

@ -119,7 +119,7 @@ return [
'audit_table_user' => 'Utilizador',
'audit_table_event' => 'Evento',
'audit_table_related' => 'Item ou Detalhe Relacionado',
'audit_table_ip' => 'IP Address',
'audit_table_ip' => 'Endereço de IP',
'audit_table_date' => 'Data da Atividade',
'audit_date_from' => 'Intervalo De',
'audit_date_to' => 'Intervalo Até',
@ -139,7 +139,7 @@ return [
'role_details' => 'Detalhes do Cargo',
'role_name' => 'Nome do Cargo',
'role_desc' => 'Breve Descrição do Cargo',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_mfa_enforced' => 'Exige autenticação de múltiplos fatores',
'role_external_auth_id' => 'IDs de Autenticação Externa',
'role_system' => 'Permissões do Sistema',
'role_manage_users' => 'Gerir utilizadores',
@ -149,7 +149,7 @@ return [
'role_manage_page_templates' => 'Gerir modelos de página',
'role_access_api' => 'Aceder à API do sistema',
'role_manage_settings' => 'Gerir as configurações da aplicação',
'role_export_content' => 'Export content',
'role_export_content' => 'Exportar conteúdo',
'role_asset' => 'Permissões de Ativos',
'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.',
'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.',
@ -207,10 +207,10 @@ return [
'users_api_tokens_create' => 'Criar Token',
'users_api_tokens_expires' => 'Expira',
'users_api_tokens_docs' => 'Documentação da API',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users_mfa' => 'Autenticação Multi-fator',
'users_mfa_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',
'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',
'users_mfa_configure' => 'Configurar Métodos',
// API Tokens
'user_api_token_create' => 'Criar Token de API',

View File

@ -15,7 +15,7 @@ return [
'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e sublinhado.',
'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
'array' => 'O campo :attribute deve ser uma lista(array).',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'O código fornecido não é válido ou já foi utilizado.',
'before' => 'O campo :attribute deve ser uma data anterior à data :date.',
'between' => [
'numeric' => 'O campo :attribute deve estar entre :min e :max.',
@ -99,7 +99,7 @@ return [
],
'string' => 'O campo :attribute deve ser uma string.',
'timezone' => 'O campo :attribute deve conter uma timezone válida.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'O código fornecido não é válido ou já expirou.',
'unique' => 'Já existe um campo/dado de nome :attribute.',
'url' => 'O formato da URL :attribute é inválido.',
'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nova Página',
'pages_editing_draft_notification' => 'Você está atualmente editando um rascunho que foi salvo da última vez em :timeDiff.',
'pages_draft_edited_notification' => 'Essa página foi atualizada desde então. É recomendado que você descarte esse rascunho.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count usuários iniciaram a edição dessa página',
'start_b' => ':userName iniciou a edição dessa página',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Новая страница',
'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.',
'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count пользователей начали редактирование этой страницы',
'start_b' => ':userName начал редактирование этой страницы',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nová stránka',
'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',
'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count používateľov začalo upravovať túto stránku',
'start_b' => ':userName začal upravovať túto stránku',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Nova stran',
'pages_editing_draft_notification' => 'Trenutno urejate osnutek, ki je bil nazadnje shranjen :timeDiff.',
'pages_draft_edited_notification' => 'Ta stran je odtlej posodobljena. Priporočamo, da zavržete ta osnutek.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count uporabnikov je začelo urejati to stran',
'start_b' => ':userName je začel urejati to stran',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Ny sida',
'pages_editing_draft_notification' => 'Du redigerar just nu ett utkast som senast sparades :timeDiff.',
'pages_draft_edited_notification' => 'Denna sida har uppdaterats sen dess. Vi rekommenderar att du förkastar dina ändringar.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count har börjat redigera den här sidan',
'start_b' => ':userName har börjat redigera den här sidan',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Yeni Sayfa',
'pages_editing_draft_notification' => 'Şu anda en son :timeDiff tarihinde kaydedilmiş olan taslağı düzenliyorsunuz.',
'pages_draft_edited_notification' => 'Bu sayfa o zamandan bu zamana güncellenmiş, bu nedenle bu taslağı yok saymanız önerilir.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count kullanıcı, bu sayfayı düzenlemeye başladı',
'start_b' => ':userName, bu sayfayı düzenlemeye başladı',

View File

@ -44,12 +44,12 @@ return [
'bookshelf_delete_notification' => 'Книжкову полицю успішно видалено',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
'favourite_add_notification' => '":ім\'я" було додане до ваших улюлених',
'favourite_remove_notification' => '":ім\'я" було видалено з ваших улюблених',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Багатофакторний метод успішно налаштований',
'mfa_remove_method_notification' => 'Багатофакторний метод успішно видалений',
// Other
'commented_on' => 'прокоментував',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Нова сторінка',
'pages_editing_draft_notification' => 'Ви наразі редагуєте чернетку, що була збережена останньою :timeDiff.',
'pages_draft_edited_notification' => 'З того часу ця сторінка була оновлена. Рекомендуємо відмовитися від цього проекту.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count користувачі(в) почали редагувати цю сторінку',
'start_b' => ':userName розпочав редагування цієї сторінки',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => 'Trang mới',
'pages_editing_draft_notification' => 'Bạn hiện đang chỉnh sửa một bản nháp được lưu cách đây :timeDiff.',
'pages_draft_edited_notification' => 'Trang này đã được cập nhật từ lúc đó. Bạn nên loại bỏ bản nháp này.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count người dùng đang bắt đầu chỉnh sửa trang này',
'start_b' => ':userName đang bắt đầu chỉnh sửa trang này',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => '新页面',
'pages_editing_draft_notification' => '您正在编辑在 :timeDiff 内保存的草稿.',
'pages_draft_edited_notification' => '此后,此页面已经被更新,建议您放弃此草稿。',
'pages_draft_page_changed_since_creation' => '这个页面在您的草稿创建后被其他用户更新了,您目前的草稿不包含新的内容。建议您放弃此草稿,或是注意不要覆盖新的页面更改。',
'pages_draft_edit_active' => [
'start_a' => ':count位用户正在编辑此页面',
'start_b' => '用户“:userName”已经开始编辑此页面',

View File

@ -209,8 +209,8 @@ return [
'users_api_tokens_docs' => 'API文档',
'users_mfa' => '多重身份认证',
'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。',
'users_mfa_x_methods' => ':count 方法已配置|:count 方法已配置',
'users_mfa_configure' => '配置方法',
'users_mfa_x_methods' => ':count 个措施已配置|:count 个措施已配置',
'users_mfa_configure' => '配置安全措施',
// API Tokens
'user_api_token_create' => '创建 API 令牌',

View File

@ -234,6 +234,7 @@ return [
'pages_initial_name' => '新頁面',
'pages_editing_draft_notification' => '您正在編輯最後儲存為 :timeDiff 的草稿。',
'pages_draft_edited_notification' => '此頁面已經被更新過。建議您放棄此草稿。',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_edit_active' => [
'start_a' => ':count 位使用者已經開始編輯此頁面',
'start_b' => '使用者 :userName 已經開始編輯此頁面',

View File

@ -229,6 +229,34 @@ class ExportTest extends TestCase
$resp->assertSee('src="/uploads/svg_test.svg"');
}
public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local()
{
$contents = file_get_contents(public_path('.htaccess'));
config()->set('filesystems.images', 'local');
$page = Page::query()->first();
$page->html = '<img src="http://localhost/uploads/images/../../.htaccess"/>';
$page->save();
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
$resp->assertDontSee(base64_encode($contents));
}
public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local_secure()
{
$testFilePath = storage_path('logs/test.txt');
config()->set('filesystems.images', 'local_secure');
file_put_contents($testFilePath, 'I am a cat');
$page = Page::query()->first();
$page->html = '<img src="http://localhost/uploads/images/../../logs/test.txt"/>';
$page->save();
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
$resp->assertDontSee(base64_encode('I am a cat'));
unlink($testFilePath);
}
public function test_exports_removes_scripts_from_custom_head()
{
$entities = [

View File

@ -4,6 +4,7 @@ namespace Tests\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Repos\PageRepo;
use Tests\TestCase;
@ -80,6 +81,63 @@ class PageDraftTest extends TestCase
->assertElementNotContains('.notification', 'Admin has started editing this page');
}
public function test_draft_save_shows_alert_if_draft_older_than_last_page_update()
{
$admin = $this->getAdmin();
$editor = $this->getEditor();
/** @var Page $page */
$page = Page::query()->first();
$this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
'name' => $page->name,
'html' => '<p>updated draft</p>',
]);
/** @var PageRevision $draft */
$draft = $page->allRevisions()
->where('type', '=', 'update_draft')
->where('created_by', '=', $editor->id)
->first();
$draft->created_at = now()->subMinute(1);
$draft->save();
$this->actingAs($admin)->put($page->refresh()->getUrl(), [
'name' => $page->name,
'html' => '<p>admin update</p>',
]);
$resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
'name' => $page->name,
'html' => '<p>updated draft again</p>',
]);
$resp->assertJson([
'warning' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
]);
}
public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else()
{
$admin = $this->getAdmin();
$editor = $this->getEditor();
/** @var Page $page */
$page = Page::query()->first();
$this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [
'name' => $page->name,
'html' => '<p>updated draft</p>',
]);
$resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
'name' => $page->name,
'html' => '<p>updated draft again</p>',
]);
$resp->assertJson([
'warning' => 'Admin has started editing this page in the last 60 minutes. Take care not to overwrite each other\'s updates!',
]);
}
public function test_draft_pages_show_on_homepage()
{
/** @var Book $book */

View File

@ -119,6 +119,15 @@ class SecurityHeaderTest extends TestCase
$this->assertEquals('base-uri \'self\'', $scriptHeader);
}
public function test_cache_control_headers_are_strict_on_responses_when_logged_in()
{
$this->asEditor();
$resp = $this->get('/');
$resp->assertHeader('Cache-Control', 'max-age=0, no-store, private');
$resp->assertHeader('Pragma', 'no-cache');
$resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
}
/**
* Get the value of the first CSP header of the given type.
*/