]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'laravel_upgrade'
authorDan Brown <redacted>
Thu, 4 Nov 2021 22:42:35 +0000 (22:42 +0000)
committerDan Brown <redacted>
Thu, 4 Nov 2021 22:42:35 +0000 (22:42 +0000)
67 files changed:
LICENSE
app/Actions/CommentRepo.php
app/Auth/Access/SocialAuthService.php
app/Auth/User.php
app/Entities/Tools/PageContent.php
app/Http/Controllers/AttachmentController.php
app/Http/Controllers/Controller.php
app/Http/Controllers/Images/DrawioImageController.php
app/Http/Controllers/Images/ImageController.php
app/Providers/CustomValidationServiceProvider.php
app/Uploads/AttachmentService.php
app/Uploads/ImageRepo.php
app/Uploads/ImageService.php
app/Util/WebSafeMimeSniffer.php [new file with mode: 0644]
composer.json
composer.lock
readme.md
resources/lang/ar/settings.php
resources/lang/bg/settings.php
resources/lang/bs/settings.php
resources/lang/ca/settings.php
resources/lang/cs/settings.php
resources/lang/da/settings.php
resources/lang/de/settings.php
resources/lang/de_informal/settings.php
resources/lang/es/settings.php
resources/lang/es_AR/settings.php
resources/lang/et/entities.php
resources/lang/et/settings.php
resources/lang/et/validation.php
resources/lang/fa/settings.php
resources/lang/fr/settings.php
resources/lang/he/settings.php
resources/lang/hr/settings.php
resources/lang/hu/settings.php
resources/lang/id/settings.php
resources/lang/it/settings.php
resources/lang/ja/settings.php
resources/lang/ko/settings.php
resources/lang/lt/settings.php
resources/lang/lv/auth.php
resources/lang/lv/entities.php
resources/lang/lv/errors.php
resources/lang/lv/settings.php
resources/lang/nb/settings.php
resources/lang/nl/activities.php
resources/lang/nl/auth.php
resources/lang/nl/common.php
resources/lang/nl/entities.php
resources/lang/nl/settings.php
resources/lang/pl/errors.php
resources/lang/pl/settings.php
resources/lang/pt/settings.php
resources/lang/pt_BR/settings.php
resources/lang/ru/settings.php
resources/lang/sk/settings.php
resources/lang/sl/settings.php
resources/lang/sv/settings.php
resources/lang/tr/settings.php
resources/lang/uk/settings.php
resources/lang/vi/settings.php
resources/lang/zh_CN/settings.php
resources/lang/zh_TW/settings.php
tests/Auth/MfaVerificationTest.php
tests/Entity/PageContentTest.php
tests/Uploads/AttachmentTest.php
tests/Uploads/ImageTest.php

diff --git a/LICENSE b/LICENSE
index 61aeaad8c8323f444c5f7b7af001f5e141dd03fa..0ec2e91ab4ee27057cdad87fefdbad2355c2613d 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2020 Dan Brown and the BookStack Project contributors
+Copyright (c) 2015-present, Dan Brown and the BookStack Project contributors
 https://github.com/BookStackApp/BookStack/graphs/contributors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
index 85fb6498a92bca35897e65845f00b87e2ba64a95..8121dfc5cf807b2fb95c2d714e9e7203c7480314 100644 (file)
@@ -66,13 +66,13 @@ class CommentRepo
     /**
      * Delete a comment from the system.
      */
-    public function delete(Comment $comment)
+    public function delete(Comment $comment): void
     {
         $comment->delete();
     }
 
     /**
-     * Convert the given comment markdown text to HTML.
+     * Convert the given comment Markdown to HTML.
      */
     public function commentToHtml(string $commentText): string
     {
index d165e76b121bbe2b6f5064c1b844906272d04f99..23e95970cc3039e9313f6cabe924b1a6b24d0645 100644 (file)
@@ -281,9 +281,6 @@ class SocialAuthService
         if ($driverName === 'google' && config('services.google.select_account')) {
             $driver->with(['prompt' => 'select_account']);
         }
-        if ($driverName === 'azure') {
-            $driver->with(['resource' => 'https://graph.windows.net']);
-        }
 
         if (isset($this->configureForRedirectCallbacks[$driverName])) {
             $this->configureForRedirectCallbacks[$driverName]($driver);
index 68e2ad6253a6bcc83d9dbfa748bc1782a53c9201..dc28aa9b71a80054e59986bcd4a395aeac29a04c 100644 (file)
@@ -28,7 +28,7 @@ use Illuminate\Support\Collection;
 /**
  * Class User.
  *
- * @property string     $id
+ * @property int        $id
  * @property string     $name
  * @property string     $slug
  * @property string     $email
index 9f4ac2893f7fe0857acbf3ea476035f39b887cec..b1323bc68ac0f02b6f3f799a59a965c0f7953fb9 100644 (file)
@@ -9,6 +9,7 @@ use BookStack\Exceptions\ImageUploadException;
 use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Uploads\ImageRepo;
+use BookStack\Uploads\ImageService;
 use BookStack\Util\HtmlContentFilter;
 use DOMDocument;
 use DOMNodeList;
@@ -86,30 +87,13 @@ class PageContent
         $body = $container->childNodes->item(0);
         $childNodes = $body->childNodes;
         $xPath = new DOMXPath($doc);
-        $imageRepo = app()->make(ImageRepo::class);
 
         // Get all img elements with image data blobs
         $imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
         foreach ($imageNodes as $imageNode) {
             $imageSrc = $imageNode->getAttribute('src');
-            [$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
-            $extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
-
-            // Validate extension
-            if (!$imageRepo->imageExtensionSupported($extension)) {
-                $imageNode->setAttribute('src', '');
-                continue;
-            }
-
-            // Save image from data with a random name
-            $imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
-
-            try {
-                $image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $this->page->id);
-                $imageNode->setAttribute('src', $image->url);
-            } catch (ImageUploadException $exception) {
-                $imageNode->setAttribute('src', '');
-            }
+            $newUrl = $this->base64ImageUriToUploadedImageUrl($imageSrc);
+            $imageNode->setAttribute('src', $newUrl);
         }
 
         // Generate inner html as a string
@@ -126,32 +110,57 @@ class PageContent
      */
     protected function extractBase64ImagesFromMarkdown(string $markdown)
     {
-        $imageRepo = app()->make(ImageRepo::class);
         $matches = [];
         preg_match_all('/!\[.*?]\(.*?(data:image\/.*?)[)"\s]/', $markdown, $matches);
 
         foreach ($matches[1] as $base64Match) {
-            [$dataDefinition, $base64ImageData] = explode(',', $base64Match, 2);
-            $extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
+            $newUrl = $this->base64ImageUriToUploadedImageUrl($base64Match);
+            $markdown = str_replace($base64Match, $newUrl, $markdown);
+        }
 
-            // Validate extension
-            if (!$imageRepo->imageExtensionSupported($extension)) {
-                $markdown = str_replace($base64Match, '', $markdown);
-                continue;
-            }
+        return $markdown;
+    }
+
+    /**
+     * Parse the given base64 image URI and return the URL to the created image instance.
+     * Returns an empty string if the parsed URI is invalid or causes an error upon upload.
+     */
+    protected function base64ImageUriToUploadedImageUrl(string $uri): string
+    {
+        $imageRepo = app()->make(ImageRepo::class);
+        $imageInfo = $this->parseBase64ImageUri($uri);
+
+        // Validate extension and content
+        if (empty($imageInfo['data']) || !ImageService::isExtensionSupported($imageInfo['extension'])) {
+            return '';
+        }
 
-            // Save image from data with a random name
-            $imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
+        // Save image from data with a random name
+        $imageName = 'embedded-image-' . Str::random(8) . '.' . $imageInfo['extension'];
 
-            try {
-                $image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $this->page->id);
-                $markdown = str_replace($base64Match, $image->url, $markdown);
-            } catch (ImageUploadException $exception) {
-                $markdown = str_replace($base64Match, '', $markdown);
-            }
+        try {
+            $image = $imageRepo->saveNewFromData($imageName, $imageInfo['data'], 'gallery', $this->page->id);
+        } catch (ImageUploadException $exception) {
+            return '';
         }
 
-        return $markdown;
+        return $image->url;
+    }
+
+    /**
+     * Parse a base64 image URI into the data and extension.
+     *
+     * @return array{extension: array, data: string}
+     */
+    protected function parseBase64ImageUri(string $uri): array
+    {
+        [$dataDefinition, $base64ImageData] = explode(',', $uri, 2);
+        $extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? '');
+
+        return [
+            'extension' => $extension,
+            'data'      => base64_decode($base64ImageData) ?: '',
+        ];
     }
 
     /**
index 56503a694fb06247f17a1f55ef3b57e9ee42ca7d..c08247dadc2ba2ef5000674a7a1a2c13d8b6c83b 100644 (file)
@@ -68,6 +68,7 @@ class AttachmentController extends Controller
             'file' => 'required|file',
         ]);
 
+        /** @var Attachment $attachment */
         $attachment = Attachment::query()->findOrFail($attachmentId);
         $this->checkOwnablePermission('view', $attachment->page);
         $this->checkOwnablePermission('page-update', $attachment->page);
@@ -86,11 +87,10 @@ class AttachmentController extends Controller
 
     /**
      * Get the update form for an attachment.
-     *
-     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
      */
     public function getUpdateForm(string $attachmentId)
     {
+        /** @var Attachment $attachment */
         $attachment = Attachment::query()->findOrFail($attachmentId);
 
         $this->checkOwnablePermission('page-update', $attachment->page);
@@ -173,6 +173,8 @@ class AttachmentController extends Controller
 
     /**
      * Get the attachments for a specific page.
+     *
+     * @throws NotFoundException
      */
     public function listForPage(int $pageId)
     {
index 283a01cfb6a8852f23c0d73c9ef975189572492c..d63280a23e1d46bcdbecac9cc5fe297893e4b5f9 100644 (file)
@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers;
 use BookStack\Facades\Activity;
 use BookStack\Interfaces\Loggable;
 use BookStack\Model;
-use finfo;
+use BookStack\Util\WebSafeMimeSniffer;
 use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Foundation\Validation\ValidatesRequests;
 use Illuminate\Http\Exceptions\HttpResponseException;
@@ -117,8 +117,9 @@ abstract class Controller extends BaseController
     protected function downloadResponse(string $content, string $fileName): Response
     {
         return response()->make($content, 200, [
-            'Content-Type'        => 'application/octet-stream',
-            'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
+            'Content-Type'           => 'application/octet-stream',
+            'Content-Disposition'    => 'attachment; filename="' . $fileName . '"',
+            'X-Content-Type-Options' => 'nosniff',
         ]);
     }
 
@@ -128,12 +129,12 @@ abstract class Controller extends BaseController
      */
     protected function inlineDownloadResponse(string $content, string $fileName): Response
     {
-        $finfo = new finfo(FILEINFO_MIME_TYPE);
-        $mime = $finfo->buffer($content) ?: 'application/octet-stream';
+        $mime = (new WebSafeMimeSniffer())->sniff($content);
 
         return response()->make($content, 200, [
-            'Content-Type'        => $mime,
-            'Content-Disposition' => 'inline; filename="' . $fileName . '"',
+            'Content-Type'           => $mime,
+            'Content-Disposition'    => 'inline; filename="' . $fileName . '"',
+            'X-Content-Type-Options' => 'nosniff',
         ]);
     }
 
index d99bb8e6f6acbb080712e8089bdfe7cfb4e2c36c..b8e4546ff90f53809ebcbfbfe1c5e6d213629bed 100644 (file)
@@ -67,13 +67,12 @@ class DrawioImageController extends Controller
     public function getAsBase64($id)
     {
         $image = $this->imageRepo->getById($id);
-        $page = $image->getPage();
-        if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
+        if (is_null($image) || $image->type !== 'drawio' || !userCan('page-view', $image->getPage())) {
             return $this->jsonError('Image data could not be found');
         }
 
         $imageData = $this->imageRepo->getImageData($image);
-        if ($imageData === null) {
+        if (is_null($imageData)) {
             return $this->jsonError('Image data could not be found');
         }
 
index 4070a0e2fe63d1ec13061aba500b2baaf0c8ea87..231712d5204eb0795f141c9ffba699178b709365 100644 (file)
@@ -7,25 +7,23 @@ use BookStack\Exceptions\NotFoundException;
 use BookStack\Http\Controllers\Controller;
 use BookStack\Uploads\Image;
 use BookStack\Uploads\ImageRepo;
+use BookStack\Uploads\ImageService;
 use Exception;
-use Illuminate\Filesystem\Filesystem as File;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
 class ImageController extends Controller
 {
-    protected $image;
-    protected $file;
     protected $imageRepo;
+    protected $imageService;
 
     /**
      * ImageController constructor.
      */
-    public function __construct(Image $image, File $file, ImageRepo $imageRepo)
+    public function __construct(ImageRepo $imageRepo, ImageService $imageService)
     {
-        $this->image = $image;
-        $this->file = $file;
         $this->imageRepo = $imageRepo;
+        $this->imageService = $imageService;
     }
 
     /**
@@ -35,14 +33,13 @@ class ImageController extends Controller
      */
     public function showImage(string $path)
     {
-        $path = storage_path('uploads/images/' . $path);
-        if (!file_exists($path)) {
+        if (!$this->imageService->pathExistsInLocalSecure($path)) {
             throw (new NotFoundException(trans('errors.image_not_found')))
                 ->setSubtitle(trans('errors.image_not_found_subtitle'))
                 ->setDetails(trans('errors.image_not_found_details'));
         }
 
-        return response()->file($path);
+        return $this->imageService->streamImageFromStorageResponse('gallery', $path);
     }
 
     /**
index c54f48ca31378bbcf8f61f706029f9af118da7c7..ac95099cc7a0298ba4704bee0ca38494a5ae60eb 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace BookStack\Providers;
 
+use BookStack\Uploads\ImageService;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\ServiceProvider;
 
@@ -13,9 +14,9 @@ class CustomValidationServiceProvider extends ServiceProvider
     public function boot(): void
     {
         Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
-            $validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
+            $extension = strtolower($value->getClientOriginalExtension());
 
-            return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
+            return ImageService::isExtensionSupported($extension);
         });
 
         Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
index f7a0918c60930c22b5a7a565a4cafe3fbb462e37..52954d24f976853b4e8cc8baf0adac8d93e0933e 100644 (file)
@@ -27,7 +27,7 @@ class AttachmentService
     /**
      * Get the storage that will be used for storing files.
      */
-    protected function getStorage(): FileSystemInstance
+    protected function getStorageDisk(): FileSystemInstance
     {
         return $this->fileSystem->disk($this->getStorageDiskName());
     }
@@ -70,7 +70,7 @@ class AttachmentService
      */
     public function getAttachmentFromStorage(Attachment $attachment): string
     {
-        return $this->getStorage()->get($this->adjustPathForStorageDisk($attachment->path));
+        return $this->getStorageDisk()->get($this->adjustPathForStorageDisk($attachment->path));
     }
 
     /**
@@ -195,7 +195,7 @@ class AttachmentService
      */
     protected function deleteFileInStorage(Attachment $attachment)
     {
-        $storage = $this->getStorage();
+        $storage = $this->getStorageDisk();
         $dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
 
         $storage->delete($this->adjustPathForStorageDisk($attachment->path));
@@ -213,10 +213,10 @@ class AttachmentService
     {
         $attachmentData = file_get_contents($uploadedFile->getRealPath());
 
-        $storage = $this->getStorage();
+        $storage = $this->getStorageDisk();
         $basePath = 'uploads/files/' . date('Y-m-M') . '/';
 
-        $uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
+        $uploadFileName = Str::random(16) . '-' . $uploadedFile->getClientOriginalExtension();
         while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
             $uploadFileName = Str::random(3) . $uploadFileName;
         }
index c4205e35740d0fce777ac8fd0c358b241dedfd49..5c6228b378df9b6bb35962eb05dcf1684c9d97bf 100644 (file)
@@ -11,34 +11,16 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
 
 class ImageRepo
 {
-    protected $image;
     protected $imageService;
     protected $restrictionService;
-    protected $page;
-
-    protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
 
     /**
      * ImageRepo constructor.
      */
-    public function __construct(
-        Image $image,
-        ImageService $imageService,
-        PermissionService $permissionService,
-        Page $page
-    ) {
-        $this->image = $image;
+    public function __construct(ImageService $imageService, PermissionService $permissionService)
+    {
         $this->imageService = $imageService;
         $this->restrictionService = $permissionService;
-        $this->page = $page;
-    }
-
-    /**
-     * Check if the given image extension is supported by BookStack.
-     */
-    public function imageExtensionSupported(string $extension): bool
-    {
-        return in_array(trim($extension, '. \t\n\r\0\x0B'), static::$supportedExtensions);
     }
 
     /**
@@ -46,7 +28,7 @@ class ImageRepo
      */
     public function getById($id): Image
     {
-        return $this->image->findOrFail($id);
+        return Image::query()->findOrFail($id);
     }
 
     /**
@@ -59,7 +41,7 @@ class ImageRepo
         $hasMore = count($images) > $pageSize;
 
         $returnImages = $images->take($pageSize);
-        $returnImages->each(function ($image) {
+        $returnImages->each(function (Image $image) {
             $this->loadThumbs($image);
         });
 
@@ -81,7 +63,7 @@ class ImageRepo
         string $search = null,
         callable $whereClause = null
     ): array {
-        $imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
+        $imageQuery = Image::query()->where('type', '=', strtolower($type));
 
         if ($uploadedTo !== null) {
             $imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
@@ -112,7 +94,8 @@ class ImageRepo
         int $uploadedTo = null,
         string $search = null
     ): array {
-        $contextPage = $this->page->findOrFail($uploadedTo);
+        /** @var Page $contextPage */
+        $contextPage = Page::visible()->findOrFail($uploadedTo);
         $parentFilter = null;
 
         if ($filterType === 'book' || $filterType === 'page') {
@@ -147,7 +130,7 @@ class ImageRepo
      *
      * @throws ImageUploadException
      */
-    public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0)
+    public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
     {
         $image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
         $this->loadThumbs($image);
@@ -156,13 +139,13 @@ class ImageRepo
     }
 
     /**
-     * Save a drawing the the database.
+     * Save a drawing in the database.
      *
      * @throws ImageUploadException
      */
     public function saveDrawing(string $base64Uri, int $uploadedTo): Image
     {
-        $name = 'Drawing-' . strval(user()->id) . '-' . strval(time()) . '.png';
+        $name = 'Drawing-' . user()->id . '-' . time() . '.png';
 
         return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
     }
@@ -170,7 +153,6 @@ class ImageRepo
     /**
      * Update the details of an image via an array of properties.
      *
-     * @throws ImageUploadException
      * @throws Exception
      */
     public function updateImageDetails(Image $image, $updateDetails): Image
@@ -187,13 +169,11 @@ class ImageRepo
      *
      * @throws Exception
      */
-    public function destroyImage(Image $image = null): bool
+    public function destroyImage(Image $image = null): void
     {
         if ($image) {
             $this->imageService->destroy($image);
         }
-
-        return true;
     }
 
     /**
@@ -201,9 +181,9 @@ class ImageRepo
      *
      * @throws Exception
      */
-    public function destroyByType(string $imageType)
+    public function destroyByType(string $imageType): void
     {
-        $images = $this->image->where('type', '=', $imageType)->get();
+        $images = Image::query()->where('type', '=', $imageType)->get();
         foreach ($images as $image) {
             $this->destroyImage($image);
         }
@@ -211,25 +191,21 @@ class ImageRepo
 
     /**
      * Load thumbnails onto an image object.
-     *
-     * @throws Exception
      */
-    public function loadThumbs(Image $image)
+    public function loadThumbs(Image $image): void
     {
-        $image->thumbs = [
+        $image->setAttribute('thumbs', [
             'gallery' => $this->getThumbnail($image, 150, 150, false),
             'display' => $this->getThumbnail($image, 1680, null, true),
-        ];
+        ]);
     }
 
     /**
      * Get the thumbnail for an image.
      * If $keepRatio is true only the width will be used.
      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
-     *
-     * @throws Exception
      */
-    protected function getThumbnail(Image $image, ?int $width = 220, ?int $height = 220, bool $keepRatio = false): ?string
+    protected function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio): ?string
     {
         try {
             return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
index d6c74c751c774e258aeabd1151a60b52fc52655c..644269731a1b0bc1be8c28278ada4af5198d551a 100644 (file)
@@ -11,11 +11,14 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException;
 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
 use Illuminate\Contracts\Filesystem\Filesystem as Storage;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Str;
 use Intervention\Image\Exception\NotSupportedException;
 use Intervention\Image\ImageManager;
 use League\Flysystem\Util;
+use Psr\SimpleCache\InvalidArgumentException;
 use Symfony\Component\HttpFoundation\File\UploadedFile;
+use Symfony\Component\HttpFoundation\StreamedResponse;
 
 class ImageService
 {
@@ -25,6 +28,8 @@ class ImageService
     protected $image;
     protected $fileSystem;
 
+    protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+
     /**
      * ImageService constructor.
      */
@@ -39,11 +44,20 @@ class ImageService
     /**
      * Get the storage that will be used for storing images.
      */
-    protected function getStorage(string $imageType = ''): FileSystemInstance
+    protected function getStorageDisk(string $imageType = ''): FileSystemInstance
     {
         return $this->fileSystem->disk($this->getStorageDiskName($imageType));
     }
 
+    /**
+     * Check if local secure image storage (Fetched behind authentication)
+     * is currently active in the instance.
+     */
+    protected function usingSecureImages(): bool
+    {
+        return $this->getStorageDiskName('gallery') === 'local_secure_images';
+    }
+
     /**
      * Change the originally provided path to fit any disk-specific requirements.
      * This also ensures the path is kept to the expected root folders.
@@ -126,7 +140,7 @@ class ImageService
      */
     public function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
     {
-        $storage = $this->getStorage($type);
+        $storage = $this->getStorageDisk($type);
         $secureUploads = setting('app-secure-images');
         $fileName = $this->cleanImageFileName($imageName);
 
@@ -144,7 +158,7 @@ class ImageService
         try {
             $this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData);
         } catch (Exception $e) {
-            \Log::error('Error when attempting image upload:' . $e->getMessage());
+            Log::error('Error when attempting image upload:' . $e->getMessage());
 
             throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
         }
@@ -219,17 +233,10 @@ class ImageService
      * If $keepRatio is true only the width will be used.
      * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
      *
-     * @param Image $image
-     * @param int   $width
-     * @param int   $height
-     * @param bool  $keepRatio
-     *
      * @throws Exception
-     * @throws ImageUploadException
-     *
-     * @return string
+     * @throws InvalidArgumentException
      */
-    public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
+    public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
     {
         if ($keepRatio && $this->isGif($image)) {
             return $this->getPublicUrl($image->path);
@@ -243,7 +250,7 @@ class ImageService
             return $this->getPublicUrl($thumbFilePath);
         }
 
-        $storage = $this->getStorage($image->type);
+        $storage = $this->getStorageDisk($image->type);
         if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
             return $this->getPublicUrl($thumbFilePath);
         }
@@ -257,27 +264,16 @@ class ImageService
     }
 
     /**
-     * Resize image data.
-     *
-     * @param string $imageData
-     * @param int    $width
-     * @param int    $height
-     * @param bool   $keepRatio
+     * Resize the image of given data to the specified size, and return the new image data.
      *
      * @throws ImageUploadException
-     *
-     * @return string
      */
-    protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
+    protected function resizeImage(string $imageData, ?int $width, ?int $height, bool $keepRatio): string
     {
         try {
             $thumb = $this->imageTool->make($imageData);
-        } catch (Exception $e) {
-            if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
-                throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
-            }
-
-            throw $e;
+        } catch (ErrorException|NotSupportedException $e) {
+            throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
         }
 
         if ($keepRatio) {
@@ -307,7 +303,7 @@ class ImageService
      */
     public function getImageData(Image $image): string
     {
-        $storage = $this->getStorage();
+        $storage = $this->getStorageDisk();
 
         return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type));
     }
@@ -330,7 +326,7 @@ class ImageService
     protected function destroyImagesFromPath(string $path, string $imageType): bool
     {
         $path = $this->adjustPathForStorageDisk($path, $imageType);
-        $storage = $this->getStorage($imageType);
+        $storage = $this->getStorageDisk($imageType);
 
         $imageFolder = dirname($path);
         $imageFileName = basename($path);
@@ -417,7 +413,7 @@ class ImageService
         }
 
         $storagePath = $this->adjustPathForStorageDisk($storagePath);
-        $storage = $this->getStorage();
+        $storage = $this->getStorageDisk();
         $imageData = null;
         if ($storage->exists($storagePath)) {
             $imageData = $storage->get($storagePath);
@@ -435,6 +431,42 @@ class ImageService
         return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);
     }
 
+    /**
+     * Check if the given path exists in the local secure image system.
+     * Returns false if local_secure is not in use.
+     */
+    public function pathExistsInLocalSecure(string $imagePath): bool
+    {
+        $disk = $this->getStorageDisk('gallery');
+
+        // Check local_secure is active
+        return $this->usingSecureImages()
+            // Check the image file exists
+            && $disk->exists($imagePath)
+            // Check the file is likely an image file
+            && strpos($disk->getMimetype($imagePath), 'image/') === 0;
+    }
+
+    /**
+     * For the given path, if existing, provide a response that will stream the image contents.
+     */
+    public function streamImageFromStorageResponse(string $imageType, string $path): StreamedResponse
+    {
+        $disk = $this->getStorageDisk($imageType);
+
+        return $disk->response($path);
+    }
+
+    /**
+     * Check if the given image extension is supported by BookStack.
+     * The extension must not be altered in this function. This check should provide a guarantee
+     * that the provided extension is safe to use for the image to be saved.
+     */
+    public static function isExtensionSupported(string $extension): bool
+    {
+        return in_array($extension, static::$supportedExtensions);
+    }
+
     /**
      * Get a storage path for the given image URL.
      * Ensures the path will start with "uploads/images".
@@ -476,7 +508,7 @@ class ImageService
      */
     private function getPublicUrl(string $filePath): string
     {
-        if ($this->storageUrl === null) {
+        if (is_null($this->storageUrl)) {
             $storageUrl = config('filesystems.url');
 
             // Get the standard public s3 url if s3 is set as storage type
@@ -490,6 +522,7 @@ class ImageService
                     $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
                 }
             }
+
             $this->storageUrl = $storageUrl;
         }
 
diff --git a/app/Util/WebSafeMimeSniffer.php b/app/Util/WebSafeMimeSniffer.php
new file mode 100644 (file)
index 0000000..4012004
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+namespace BookStack\Util;
+
+use finfo;
+
+/**
+ * Helper class to sniff out the mime-type of content resulting in
+ * a mime-type that's relatively safe to serve to a browser.
+ */
+class WebSafeMimeSniffer
+{
+    /**
+     * @var string[]
+     */
+    protected $safeMimes = [
+        'application/json',
+        'application/octet-stream',
+        'application/pdf',
+        'image/bmp',
+        'image/jpeg',
+        'image/png',
+        'image/gif',
+        'image/webp',
+        'image/avif',
+        'image/heic',
+        'text/css',
+        'text/csv',
+        'text/javascript',
+        'text/json',
+        'text/plain',
+        'video/x-msvideo',
+        'video/mp4',
+        'video/mpeg',
+        'video/ogg',
+        'video/webm',
+        'video/vp9',
+        'video/h264',
+        'video/av1',
+    ];
+
+    /**
+     * Sniff the mime-type from the given file content while running the result
+     * through an allow-list to ensure a web-safe result.
+     * Takes the content as a reference since the value may be quite large.
+     */
+    public function sniff(string &$content): string
+    {
+        $fInfo = new finfo(FILEINFO_MIME_TYPE);
+        $mime = $fInfo->buffer($content) ?: 'application/octet-stream';
+
+        if (in_array($mime, $this->safeMimes)) {
+            return $mime;
+        }
+
+        [$category] = explode('/', $mime, 2);
+        if ($category === 'text') {
+            return 'text/plain';
+        }
+
+        return 'application/octet-stream';
+    }
+}
index 23ce97cd485cbe02f88268f50fba943a72f8b8f2..07111d985a63439633ce28f5b1f4e1db64b03e34 100644 (file)
@@ -37,7 +37,7 @@
         "predis/predis": "^1.1",
         "socialiteproviders/discord": "^4.1",
         "socialiteproviders/gitlab": "^4.1",
-        "socialiteproviders/microsoft-azure": "^4.1",
+        "socialiteproviders/microsoft-azure": "^5.0.1",
         "socialiteproviders/okta": "^4.1",
         "socialiteproviders/slack": "^4.1",
         "socialiteproviders/twitch": "^5.3",
index 005f30fa383d2c470198256d4bba7f05e49e30e6..4323d89d5d80b2fb343679139cd84f55b1d86c0f 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "48be80b0326176faa9775dca18a00fa2",
+    "content-hash": "d7893ec647ed43e272a21c9c01a9b2cb",
     "packages": [
         {
             "name": "aws/aws-crt-php",
         },
         {
             "name": "socialiteproviders/microsoft-azure",
-            "version": "4.2.1",
+            "version": "5.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Microsoft-Azure.git",
-                "reference": "64779ec21db0bee3111039a67c0fa0ab550a3462"
+                "reference": "9b23e02ff711de42e513aa55f768a4f1c67c0e41"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/64779ec21db0bee3111039a67c0fa0ab550a3462",
-                "reference": "64779ec21db0bee3111039a67c0fa0ab550a3462",
+                "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/9b23e02ff711de42e513aa55f768a4f1c67c0e41",
+                "reference": "9b23e02ff711de42e513aa55f768a4f1c67c0e41",
                 "shasum": ""
             },
             "require": {
                 }
             ],
             "description": "Microsoft Azure OAuth2 Provider for Laravel Socialite",
+            "keywords": [
+                "azure",
+                "laravel",
+                "microsoft",
+                "oauth",
+                "provider",
+                "socialite"
+            ],
             "support": {
-                "source": "https://github.com/SocialiteProviders/Microsoft-Azure/tree/4.2.1"
+                "docs": "https://socialiteproviders.com/microsoft-azure",
+                "issues": "https://github.com/socialiteproviders/providers/issues",
+                "source": "https://github.com/socialiteproviders/providers"
             },
-            "time": "2021-06-14T22:51:38+00:00"
+            "time": "2021-10-07T22:21:59+00:00"
         },
         {
             "name": "socialiteproviders/okta",
index 0c9cf66e98ccc1a6ba864acbd140505b82eadd5c..92bbe2c5cd1273cb74b1ba9e9e72d3b85bbb5496 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -27,6 +27,25 @@ BookStack is not designed as an extensible platform to be used for purposes that
 
 In regard to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
 
+## 🌟 Project Sponsors
+
+Shown below are our bronze, silver and gold project sponsors.
+Big thanks to these companies for supporting the project.
+Note: Listed services are not tested, vetted nor supported by the official BookStack project in any manner.
+[View all sponsors](https://github.com/sponsors/ssddanbrown).
+
+#### Bronze Sponsors
+
+<table><tbody><tr>
+<td><a href="https://www.diagrams.net/" target="_blank">
+    <img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/master/static/images/sponsors/diagramsnet.png" alt="Diagrams.net logo">
+</a></td>
+
+<td><a href="https://www.stellarhosted.com/bookstack/" target="_blank">
+    <img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/master/static/images/sponsors/stellarhosted.png" alt="Stellar Hosted Logo">
+</a></td>
+</tr></tbody></table>
+
 ## 🛣️ Road Map
 
 Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
index f3ebc303927dd96a503a0fda833b5d88af868005..7456fbc996e72665d911b33b004403b3faeb70b2 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 2151e2e201f43f69a8a56018cf74eb4221d3d24b..76b925f5844c7f55c6d01647cc0ca3275b30ebaa 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 335a1b436428d296d0f303aa4e308880b33a301b..688b0aad80b1c443739b22632096e1ce518b06ac 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 4a1e6685514b9b9ce7f00808daa4ada888b3b871..30d937b605140fd1c6b75d49c1683da135c008e2 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index b4a72683057533d0a6ecb79041ca32eb7d95278c..149a0475351218b4d2e386506186b8b87cb07578 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 83b27aaab18d7167d1a5574379000537122c904e..d963a7ad47582f06823e6b4a540222eab240ae4d 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'Hebraisk',
         'hr' => 'Hrvatski',
index 1d1c47060801fc2ca04a1275b4357d0545130d01..1dd84a9a85150939909016130894ad63a2bcb649 100644 (file)
@@ -251,7 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Estnisch',
         'fr' => 'Français',
         'he' => 'Hebräisch',
         'hr' => 'Hrvatski',
index d22f73e8998509c73d594e7e3be448401f8d4581..a5cd94ab0518de65f4c152fbb36af36e17c6f1b0 100644 (file)
@@ -251,7 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Estnisch',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 8c3370c810ed7b094f2e679d058b334f2ad5b1b4..079202bfa61cb1f4a85c1ebb09657693c6828137 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 922944674e6ed3484e30f195f7d631cf9889cce0..ab8a873b349761e297848042303081b3e91f96e4 100644 (file)
@@ -249,7 +249,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index e685e2bc63251206ba230ca78758e849172bab1e..e8a6a29746d5d3f6f1e66ab2a82cae8dcda3a895 100644 (file)
@@ -40,8 +40,8 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'Õigused',
-    'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
-    'permissions_enable' => 'Enable Custom Permissions',
+    'permissions_intro' => 'Kui kohandatud õigused on lubatud, rakendatakse neid eelisjärjekorras, enne rolli õiguseid.',
+    'permissions_enable' => 'Luba kohandatud õigused',
     'permissions_save' => 'Salvesta õigused',
     'permissions_owner' => 'Omanik',
 
@@ -240,11 +240,11 @@ return [
         'start_b' => ':userName alustas selle lehe muutmist',
         'time_a' => 'lehe viimasest muutmisest alates',
         'time_b' => 'viimase :minCount minuti jooksul',
-        'message' => ':start :time. Take care not to overwrite each other\'s updates!',
+        'message' => ':start :time. Ärge teineteise muudatusi üle kirjutage!',
     ],
-    'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
-    'pages_specific' => 'Specific Page',
-    'pages_is_template' => 'Page Template',
+    'pages_draft_discarded' => 'Mustand ära visatud, redaktorisse laeti lehe värske sisu',
+    'pages_specific' => 'Spetsiifiline leht',
+    'pages_is_template' => 'Lehe mall',
 
     // Editor Sidebar
     'page_tags' => 'Lehe sildid',
@@ -264,7 +264,7 @@ return [
     'attachments_items' => 'Lisatud objektid',
     'attachments_upload' => 'Laadi fail üles',
     'attachments_link' => 'Lisa link',
-    'attachments_set_link' => 'Set Link',
+    'attachments_set_link' => 'Määra link',
     'attachments_delete' => 'Kas oled kindel, et soovid selle manuse kustutada?',
     'attachments_dropzone' => 'Manuse lisamiseks lohista failid või klõpsa siin',
     'attachments_no_files' => 'Üleslaaditud faile ei ole',
@@ -274,10 +274,10 @@ return [
     'attachments_link_url' => 'Link failile',
     'attachments_link_url_hint' => 'Lehekülje või faili URL',
     'attach' => 'Lisa',
-    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_insert_link' => 'Lisa manuse link lehele',
     'attachments_edit_file' => 'Muuda faili',
     'attachments_edit_file_name' => 'Faili nimi',
-    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
+    'attachments_edit_drop_upload' => 'Manuse üle kirjutamiseks lohista failid või klõpsa siin',
     'attachments_order_updated' => 'Manuste järjekord muudetud',
     'attachments_updated_success' => 'Manuse andmed muudetud',
     'attachments_deleted' => 'Manus kustutatud',
@@ -292,7 +292,7 @@ return [
     'templates_prepend_content' => 'Lisa lehe sisu ette',
 
     // Profile View
-    'profile_user_for_x' => 'User for :time',
+    'profile_user_for_x' => 'Kasutaja olnud :time',
     'profile_created_content' => 'Lisatud sisu',
     'profile_not_created_pages' => ':userName ei ole ühtegi lehte lisanud',
     'profile_not_created_chapters' => ':userName ei ole ühtegi peatükki lisanud',
index c6ca6425bf1322de00521123bf54dbf690977179..6203d9f0b4e68b97806b154010dc6ee2c3d85b36 100644 (file)
@@ -66,7 +66,7 @@ return [
     'reg_email_confirmation_toggle' => 'Nõua e-posti aadressi kinnitamist',
     'reg_confirm_email_desc' => 'Kui domeeni piirang on kasutusel, siis on e-posti aadressi kinnitamine nõutud ja seda seadet ignoreeritakse.',
     'reg_confirm_restrict_domain' => 'Domeeni piirang',
-    'reg_confirm_restrict_domain_desc' => 'Sisesta komaga eraldatud nimekiri e-posti domeenidest, millega soovitud registreerumist piirata. Kasutajale saadetakse aadressi kinnitamiseks e-kiri, enne kui neil lubatakse rakendust kasutada.<br>Pane tähele, et kasutajad saavad pärast edukat registreerumist oma e-posti aadressi muuta.',
+    'reg_confirm_restrict_domain_desc' => 'Sisesta komaga eraldatud nimekiri e-posti domeenidest, millega soovid registreerumist piirata. Kasutajale saadetakse aadressi kinnitamiseks e-kiri, enne kui neil lubatakse rakendust kasutada.<br>Pane tähele, et kasutajad saavad pärast edukat registreerumist oma e-posti aadressi muuta.',
     'reg_confirm_restrict_domain_placeholder' => 'Piirangut ei ole',
 
     // Maintenance settings
@@ -126,7 +126,7 @@ return [
 
     // Role Settings
     'roles' => 'Rollid',
-    'role_user_roles' => 'Kasutajate rollid',
+    'role_user_roles' => 'Kasutaja rollid',
     'role_create' => 'Lisa uus roll',
     'role_create_success' => 'Roll on lisatud',
     'role_delete' => 'Kustuta roll',
@@ -209,8 +209,8 @@ return [
     'users_api_tokens_docs' => 'API dokumentatsioon',
     'users_mfa' => 'Mitmeastmeline autentimine',
     'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',
-    'users_mfa_x_methods' => ':count method configured|:count methods configured',
-    'users_mfa_configure' => 'Configure Methods',
+    'users_mfa_x_methods' => ':count meetod seadistatud|:count meetodit seadistatud',
+    'users_mfa_configure' => 'Seadista meetodid',
 
     // API Tokens
     'user_api_token_create' => 'Lisa API tunnus',
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 7fde26c475fc453f86ea1686688e62c28eabbfbf..61e13a9f8b620664a7a0d8c9c83fae0105d4cfb4 100644 (file)
@@ -79,16 +79,16 @@ return [
         'string'  => ':attribute peab sisaldama vähemalt :min tähemärki.',
         'array'   => ':attribute peab sisaldama vähemalt :min elementi.',
     ],
-    'not_in'               => 'The selected :attribute is invalid.',
-    'not_regex'            => 'The :attribute format is invalid.',
-    'numeric'              => 'The :attribute must be a number.',
-    'regex'                => 'The :attribute format is invalid.',
+    'not_in'               => 'Valitud :attribute on vigane.',
+    'not_regex'            => ':attribute on vigases formaadis.',
+    'numeric'              => ':attribute peab olema arv.',
+    'regex'                => ':attribute on vigases formaadis.',
     'required'             => ':attribute on kohustuslik.',
     'required_if'          => ':attribute on kohustuslik, kui :other on :value.',
     'required_with'        => ':attribute on kohustuslik, kui :values on olemas.',
     'required_with_all'    => ':attribute on kohustuslik, kui :values on olemas.',
     'required_without'     => ':attribute on kohustuslik, kui :values ei ole olemas.',
-    'required_without_all' => 'The :attribute field is required when none of :values are present.',
+    'required_without_all' => ':attribute on kohustuslik, kui :values on valimata.',
     'same'                 => ':attribute ja :other peavad klappima.',
     'safe_url'             => 'Link ei pruugi olla turvaline.',
     'size'                 => [
@@ -97,8 +97,8 @@ return [
         'string'  => ':attribute peab sisaldama :size tähemärki.',
         'array'   => ':attribute peab sisaldama :size elemente.',
     ],
-    'string'               => 'The :attribute must be a string.',
-    'timezone'             => 'The :attribute must be a valid zone.',
+    'string'               => ':attribute peab olema string.',
+    'timezone'             => ':attribute peab olema kehtiv ajavöönd.',
     'totp'                 => 'Kood ei ole korrektne või on aegunud.',
     'unique'               => ':attribute on juba võetud.',
     'url'                  => ':attribute on vigases formaadis.',
index 335a1b436428d296d0f303aa4e308880b33a301b..688b0aad80b1c443739b22632096e1ce518b06ac 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 56c9adc7be64a7776776e6c9a7bd8225cd3f88e2..5df79561f1c08b2ee87a0cb043edcf1bee927c1a 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Estonien',
         'fr' => 'Français',
         'he' => 'Hébreu',
         'hr' => 'Hrvatski',
index e8f25fe6ccdf0ba9a6d41fa0490a7686cbd47ef3..36e36c94da0bb78d2a5aa1764867e1ee0f3137bb 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 38a2878f65c69212acb9d70ba23b4e394874cbd9..616062eeb2de21b5c9ab4f05469d640f0223e5d5 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 57b640a995176cd8ed9d167835e6ca000a4604fd..b0ad98786923750bf6db9a2f45823f5fef3896e1 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 134b6215046a212be00999d61c58b902b023922c..2ef4b6b08ae28b3555ae50a5bb6e46cfe1726d08 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 6227dbf175eaeb1f4f4894e475bc5e5cfb175183..d7573de887732fc1248820e700043344f32a147c 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 76da40deb1af7fe1f58983c55f7b3b4a6e34f310..c606cfe1b743bd2f41a0acff0f7425b71439a038 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index de5b59d50b41895add58632dbfd6927e36f0711f..7a660bc2d534cf96123d94634ba596b76f59c9f3 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => '히브리어',
         'hr' => 'Hrvatski',
index 8d55646db6224d28b8f6b8d624592dee3b242284..035b61e217d2c2ee935e31ea895c9fd42ded5659 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index e228bed0032e089432f1ac8c82c9fff668d61813..60dc3fda30885e07cb329e0fc28f2f71782df1ae 100644 (file)
@@ -79,32 +79,32 @@ return [
     'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',
     'mfa_setup_reconfigure' => 'Mainīt 2FA konfigurāciju',
     'mfa_setup_remove_confirmation' => 'Vai esi drošs, ka vēlies noņemt divfaktoru autentifikāciju?',
-    '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_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_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_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',
-    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
-    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
-    'mfa_verify_access' => 'Verify Access',
-    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
-    'mfa_verify_no_methods' => 'No Methods Configured',
-    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
-    'mfa_verify_use_totp' => 'Verify using a mobile app',
-    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
-    'mfa_verify_backup_code' => 'Backup Code',
-    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
-    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
-    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
-    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
+    'mfa_setup_action' => 'Iestatījumi',
+    'mfa_backup_codes_usage_limit_warning' => 'Jums atlikuši mazāk kā 5 rezerves kodi. Lūdzu izveidojiet jaunu kodu komplektu pirms tie visi izlietoti, lai izvairītos no izslēgšanas no jūsu konta.',
+    'mfa_option_totp_title' => 'Mobilā aplikācija',
+    'mfa_option_totp_desc' => 'Lai lietotu vairākfaktoru autentifikāciju, jums būs nepieciešama mobilā aplikācija, kas atbalsta TOTP, piemēram, Google Authenticator, Authy vai Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Rezerves kodi',
+    'mfa_option_backup_codes_desc' => 'Droši noglabājiet vienreizlietojamu rezerves kodu komplektu, ko varēsiet izmantot, lai verificētu savu identitāti.',
+    'mfa_gen_confirm_and_enable' => 'Apstiprināt un ieslēgt',
+    'mfa_gen_backup_codes_title' => 'Rezerves kodu iestatījumi',
+    'mfa_gen_backup_codes_desc' => 'Noglabājiet zemāk esošo kodu sarakstu drošā vietā. Kad piekļūsiet sistēmai, jūs varēsiet izmantot vienu no kodiem kā papildus autentifikācijas mehānismu.',
+    'mfa_gen_backup_codes_download' => 'Lejupielādēt kodus',
+    'mfa_gen_backup_codes_usage_warning' => 'Katru kodu var izmantot tikai vienreiz',
+    'mfa_gen_totp_title' => 'Mobilās aplikācijas iestatījumi',
+    'mfa_gen_totp_desc' => 'Lai lietotu vairākfaktoru autentifikāciju, jums būs nepieciešama mobilā aplikācija, kas atbalsta TOTP, piemēram, Google Authenticator, Authy vai Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Skenējiet zemāk esošo kvadrātkodu (QR) izmantojot savu autentifikācijas aplikāciju.',
+    'mfa_gen_totp_verify_setup' => 'Verificēt iestatījumus',
+    'mfa_gen_totp_verify_setup_desc' => 'Pārbaudiet, ka viss darbojas, zemāk esošajā laukā ievadot kodu, ko izveidojusi jūsu autentifikācijas aplikācijā:',
+    'mfa_gen_totp_provide_code_here' => 'Norādīet jūsu aplikācijā izveidoto kodu šeit',
+    'mfa_verify_access' => 'Verificēt piekļuvi',
+    'mfa_verify_access_desc' => 'Jūsu lietotāja kontam nepieciešams verificēt jūsu identitāti ar papildus pārbaudes līmeni pirms piešķirta piekļuve. Verificējiet, izmantojot vienu no uzstādītajām metodēm, lai turpinātu.',
+    'mfa_verify_no_methods' => 'Nav iestatīta neviena metode',
+    'mfa_verify_no_methods_desc' => 'Jūsu kontam nav iestatīta neviena vairākfaktoru autentifikācijas metode. Jums būs nepieciešams iestatīt vismaz vienu metodi, lai iegūtu piekļuvi.',
+    'mfa_verify_use_totp' => 'Verificēt, izmantojot mobilo aplikāciju',
+    'mfa_verify_use_backup_codes' => 'Verificēt, izmantojot rezerves kodu',
+    'mfa_verify_backup_code' => 'Rezerves kods',
+    'mfa_verify_backup_code_desc' => 'Zemāk ievadiet vienu no jūsu atlikušajiem rezerves kodiem:',
+    'mfa_verify_backup_code_enter_here' => 'Ievadiet rezerves kodu šeit',
+    'mfa_verify_totp_desc' => 'Zemāk ievadiet kodu, kas izveidots mobilajā aplikācijā:',
+    'mfa_setup_login_notification' => 'Vairākfaktoru metode iestatīta, lūdzu pieslēdzieties atkal izmantojot iestatīto metodi.',
 ];
index f5ef3b06c535bd05570478088ff3b7e44266303e..21f1cbca5c81e3116b2140b6665038f386cbcc83 100644 (file)
@@ -234,7 +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_page_changed_since_creation' => 'Šī lapa ir izmainīta kopš šī uzmetuma izveidošanas. Ieteicams šo uzmetumu dzēst, lai netiktu pazaudētas veiktās izmaiņas.',
     'pages_draft_edit_active' => [
         'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',
         'start_b' => ':userName veic izmaiņas šajā lapā',
index 8c72da976d1f47687c65f3c59ef9f1de22d08c6a..9ce8201919b78bb8951ac1a9566a7a6bd39e8bd6 100644 (file)
@@ -23,10 +23,10 @@ return [
     'saml_no_email_address' => 'Ārējās autentifikācijas sistēmas sniegtajos datos nevarēja atrast šī lietotāja e-pasta adresi',
     'saml_invalid_response_id' => 'Ārējās autentifikācijas sistēmas pieprasījums neatpazīst procesu, kuru sākusi šī lietojumprogramma. Pārvietojoties atpakaļ pēc pieteikšanās var rasties šāda problēma.',
     'saml_fail_authed' => 'Piekļuve ar :system neizdevās, sistēma nepieļāva veiksmīgu autorizāciju',
-    'oidc_already_logged_in' => 'Already logged in',
-    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_already_logged_in' => 'Jau esat ielogojies',
+    'oidc_user_not_registered' => 'Lietotājs :name nav reģistrēts un automātiska reģistrācija ir izslēgta',
+    'oidc_no_email_address' => 'Ārējās autentifikācijas sistēmas sniegtajos datos nevarēja atrast šī lietotāja e-pasta adresi',
+    'oidc_fail_authed' => 'Piekļuve ar :system neizdevās, sistēma nepieļāva veiksmīgu autorizāciju',
     'social_no_action_defined' => 'Darbības nav definētas',
     'social_login_bad_response' => "Saņemta kļūda izmantojot :socialAccount piekļuvi:\n:error",
     'social_account_in_use' => 'Šis :socialAccount konts jau tiek izmantots, mēģiniet ieiet ar :socialAccount piekļuves iespēju.',
index 1ecc00524ac6ae55dbb3fdcfcae8a942f5abe38b..3d06aca13723264365712524ad08503426991417 100644 (file)
@@ -92,7 +92,7 @@ return [
     'recycle_bin' => 'Miskaste',
     'recycle_bin_desc' => 'Te jūs varat atjaunot dzēstās vienības vai arī izdzēst tās no sistēmas pilnībā. Šis saraksts nav filtrēts atšķirībā no līdzīgiem darbību sarakstiem sistēmā, kur ir piemēroti piekļuves tiesību filtri.',
     'recycle_bin_deleted_item' => 'Dzēsta vienība',
-    'recycle_bin_deleted_parent' => 'Parent',
+    'recycle_bin_deleted_parent' => 'Augstāks līmenis',
     'recycle_bin_deleted_by' => 'Izdzēsa',
     'recycle_bin_deleted_at' => 'Dzēšanas laiks',
     'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst',
@@ -105,7 +105,7 @@ return [
     'recycle_bin_restore_list' => 'Atjaunojamās vienības',
     'recycle_bin_restore_confirm' => 'Šī darbība atjaunos dzēsto vienību, tai skaitā visus tai pakārtotos elementus, uz tās sākotnējo atrašanās vietu. Ja sākotnējā atrašanās vieta ir izdzēsta un atrodas miskastē, būs nepieciešams atjaunot arī to.',
     'recycle_bin_restore_deleted_parent' => 'Šo elementu saturošā vienība arī ir dzēsta. Tas paliks dzēsts līdz šī saturošā vienība arī ir atjaunota.',
-    'recycle_bin_restore_parent' => 'Restore Parent',
+    'recycle_bin_restore_parent' => 'Atjaunot augstāku līmeni',
     'recycle_bin_destroy_notification' => 'Dzēstas kopā :count vienības no miskastes.',
     'recycle_bin_restore_notification' => 'Atjaunotas kopā :count vienības no miskastes.',
 
@@ -119,7 +119,7 @@ return [
     'audit_table_user' => 'Lietotājs',
     'audit_table_event' => 'Notikums',
     'audit_table_related' => 'Saistīta vienība vai detaļa',
-    'audit_table_ip' => 'IP Address',
+    'audit_table_ip' => 'IP adrese',
     'audit_table_date' => 'Notikuma datums',
     'audit_date_from' => 'Datums no',
     'audit_date_to' => 'Datums līdz',
@@ -139,7 +139,7 @@ return [
     'role_details' => 'Informācija par grupu',
     'role_name' => 'Grupas nosaukums',
     'role_desc' => 'Īss grupas apaksts',
-    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
+    'role_mfa_enforced' => 'Nepieciešama vairākfaktoru autentifikācija',
     'role_external_auth_id' => 'Ārējais autentifikācijas ID',
     'role_system' => 'Sistēmas atļaujas',
     'role_manage_users' => 'Pārvaldīt lietotājus',
@@ -149,7 +149,7 @@ return [
     'role_manage_page_templates' => 'Pārvaldīt lapas veidnes',
     'role_access_api' => 'Piekļūt sistēmas API',
     'role_manage_settings' => 'Pārvaldīt iestatījumus',
-    'role_export_content' => 'Export content',
+    'role_export_content' => 'Eksportēt saturu',
     'role_asset' => 'Resursa piekļuves tiesības',
     'roles_system_warning' => 'Jebkuras no trīs augstāk redzamajām atļaujām dod iespēju lietotājam mainīt savas un citu lietotāju sistēmas atļaujas. Pievieno šīs grupu atļaujas tikai tiem lietotājiem, kuriem uzticies.',
     'role_asset_desc' => 'Šīs piekļuves tiesības kontrolē noklusēto piekļuvi sistēmas resursiem. Grāmatām, nodaļām un lapām norādītās tiesības būs pārākas par šīm.',
@@ -207,10 +207,10 @@ return [
     'users_api_tokens_create' => 'Izveidot žetonu',
     'users_api_tokens_expires' => 'Derīguma termiņš',
     'users_api_tokens_docs' => 'API dokumentācija',
-    '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' => 'Vairākfaktoru autentifikācija',
+    'users_mfa_desc' => 'Iestati vairākfaktoru autentifikāciju kā papildus drošības līmeni tavam lietotāja kontam.',
+    'users_mfa_x_methods' => ':count metode iestatīta|:count metodes iestatītas',
+    'users_mfa_configure' => 'Iestatīt metodes',
 
     // API Tokens
     'user_api_token_create' => 'Izveidot API žetonu',
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Igauņu',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 5a6a0fb06dc8018dde2ff77eb1b8fb9cdab86331..6dfa8bad6687e09f4eac392f037cfa8d6ad91694 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index f45ea074ad77d4ff5d59b1eb56c67ec32af1e065..3a3b36a87c54ffbdf5ade77e86e8a5edb8644b66 100644 (file)
@@ -48,8 +48,8 @@ return [
     'favourite_remove_notification' => '":name" is verwijderd uit je favorieten',
 
     // MFA
-    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
-    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+    'mfa_setup_method_notification' => 'Multi-factor methode succesvol geconfigureerd',
+    'mfa_remove_method_notification' => 'Multi-factor methode succesvol verwijderd',
 
     // Other
     'commented_on'                => 'reageerde op',
index 3cec0a94d525a5f7a392d40148b2eedc55dc8ab3..36d9005d814b55f637f00d6140e380ee5cc92ca0 100644 (file)
@@ -74,27 +74,27 @@ return [
     'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :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_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.',
-    '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_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_setup' => 'Multi-factor authenticatie instellen',
+    'mfa_setup_desc' => 'Stel multi-factor authenticatie in als een extra beveiligingslaag voor uw gebruikersaccount.',
+    'mfa_setup_configured' => 'Reeds geconfigureerd',
+    'mfa_setup_reconfigure' => 'Herconfigureren1',
+    'mfa_setup_remove_confirmation' => 'Weet je zeker dat je deze multi-factor authenticatie methode wilt verwijderen?',
+    'mfa_setup_action' => 'Instellen',
+    'mfa_backup_codes_usage_limit_warning' => 'U heeft minder dan 5 back-upcodes resterend. Genereer en sla een nieuwe set op voordat je geen codes meer hebt om te voorkomen dat je buiten je account wordt gesloten.',
+    'mfa_option_totp_title' => 'Mobiele app',
+    'mfa_option_totp_desc' => 'Om multi-factor authenticatie te gebruiken heeft u een mobiele applicatie nodig die TOTP ondersteunt, zoals Google Authenticator, Authy of Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Back-up Codes',
+    'mfa_option_backup_codes_desc' => 'Bewaar veilig een set eenmalige back-upcodes die u kunt invoeren om uw identiteit te verifiëren.',
+    'mfa_gen_confirm_and_enable' => 'Bevestigen en inschakelen',
+    'mfa_gen_backup_codes_title' => 'Reservekopiecodes instellen',
+    'mfa_gen_backup_codes_desc' => 'De onderstaande lijst met codes opslaan op een veilige plaats. Bij de toegang tot het systeem kun je een van de codes gebruiken als tweede verificatiemechanisme.',
     '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_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',
-    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_backup_codes_usage_warning' => 'Elke code kan slechts eenmaal gebruikt worden',
+    'mfa_gen_totp_title' => 'Mobiele app installatie',
+    'mfa_gen_totp_desc' => 'Om multi-factor authenticatie te gebruiken heeft u een mobiele applicatie nodig die TOTP ondersteunt, zoals Google Authenticator, Authy of Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan de onderstaande QR-code door gebruik te maken van uw favoriete authenticatie app om aan de slag te gaan.',
+    'mfa_gen_totp_verify_setup' => 'Installatie verifiëren',
+    'mfa_gen_totp_verify_setup_desc' => 'Controleer of alles werkt door het invoeren van een code, die wordt gegenereerd binnen uw authenticatie-app, in het onderstaande invoerveld:',
     'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
     'mfa_verify_access' => 'Verify Access',
     'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
index 793e87b119153b835216a987a7347565f6f90a52..887f3ebc263befe7fce5903f499862908a643f98 100644 (file)
@@ -39,7 +39,7 @@ return [
     'reset' => 'Resetten',
     'remove' => 'Verwijderen',
     'add' => 'Toevoegen',
-    'configure' => 'Configure',
+    'configure' => 'Configureer',
     'fullscreen' => 'Volledig scherm',
     'favourite' => 'Favoriet',
     'unfavourite' => 'Verwijderen uit favoriet',
index 3c3148a8b9dff4260306e3b087826d3edab0f102..831cb8008ccc9b66b8a00dffa481be93eede6e6c 100644 (file)
@@ -99,7 +99,7 @@ return [
     'shelves_permissions' => 'Boekenplank permissies',
     'shelves_permissions_updated' => 'Boekenplank permissies opgeslagen',
     'shelves_permissions_active' => 'Boekenplank permissies actief',
-    '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' => 'Machtigingen op boekenplanken zijn niet automatisch een cascade om boeken te bevatten. Dit komt omdat een boek in meerdere schappen kan bestaan. Machtigingen kunnen echter worden gekopieerd naar subboeken door gebruik te maken van onderstaande optie.',
     'shelves_copy_permissions_to_books' => 'Kopieer permissies naar boeken',
     'shelves_copy_permissions' => 'Kopieer permissies',
     'shelves_copy_permissions_explain' => 'Met deze actie worden de permissies van deze boekenplank gekopieërd naar alle boeken op de plank. Voordat deze actie wordt uitgevoerd, zorg dat de wijzigingen in de permissies van deze boekenplank zijn opgeslagen.',
@@ -234,7 +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_page_changed_since_creation' => 'Deze pagina is bijgewerkt sinds het aanmaken van dit concept. Het wordt aanbevolen dat u dit ontwerp verwijdert of ervoor zorgt dat u wijzigingen op de pagina niet overschrijft.',
     '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',
index ad94d0f2c39c3d8f7e08d70d13ee763c06a76c7c..379c3a1f6a9c27718c92a2dc1d9b793a14f362b2 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 8d3ce30ed444a9fbdf3821a94a9e76fbe38729f7..e493705b51ecb72cbeaf51517ca5c6c03e905fd5 100644 (file)
@@ -24,9 +24,9 @@ return [
     'saml_invalid_response_id' => 'Żądanie z zewnętrznego systemu uwierzytelniania nie zostało rozpoznane przez proces rozpoczęty przez tę aplikację. Cofnięcie po zalogowaniu mogło spowodować ten problem.',
     'saml_fail_authed' => 'Logowanie przy użyciu :system nie powiodło się, system nie mógł pomyślnie ukończyć uwierzytelniania',
     'oidc_already_logged_in' => 'Już zalogowany',
-    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_user_not_registered' => 'Użytkownik :name nie jest zarejestrowany, a automatyczna rejestracja jest wyłączona',
+    'oidc_no_email_address' => 'Nie można odnaleźć adresu email dla tego użytkownika w danych dostarczonych przez zewnętrzny system uwierzytelniania',
+    'oidc_fail_authed' => 'Logowanie przy użyciu :system nie powiodło się, system nie mógł pomyślnie ukończyć uwierzytelniania',
     'social_no_action_defined' => 'Brak zdefiniowanej akcji',
     'social_login_bad_response' => "Podczas próby logowania :socialAccount wystąpił błąd: \n:error",
     'social_account_in_use' => 'To konto :socialAccount jest już w użyciu. Spróbuj zalogować się za pomocą opcji :socialAccount.',
index fde8cc2096fc554de275a5715b6c47eee857fc34..23defdad817f0abd165824e5dcf8b9a576fac825 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Estoński',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 31e5e87ab409a0c42daf938bea6e7640e89f8ebb..c87bba984ce5a80d5cc2d1a310eb6027c04a283d 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index b73a006e824bd9a1a2d48ac3d843773769ce917a..170dd0a1a3b4e60c83a74ad9f8eb18db3c89cb15 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 79c5d90c675ca990223fb0d082f68b04d5f72c32..c4591dc06ed96c44a91da8d73bb75ffe0b422cfd 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index b738c94659ff5a9f5692afdc5699c015019403eb..4aa813d804ae6a541b251082d3275d431bd01f2c 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index c0f63645691dd0b2fab4c479cc88846fa39d6da5..e18dc11c2ad4ab868cc45e966c6f2c8ad4da839b 100644 (file)
@@ -249,7 +249,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 61326e496b00ae0d3cf7b16f440365cbb3cebd48..76ceede74b00ed82dd4515408b707554666ac65e 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 766e01fad32f32bcb97f8d13ec1f18ee72c88549..95de460b78714031a1c40023adf72349d28758b2 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'İbranice',
         'hr' => 'Hrvatski',
index afdbd657d85a53ec6c5c30bd3ab935b83e98c21d..1b48adfece35b8fd3dffc235b3cb092f45541999 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index d4d5322f194dd61d0976a2136654c0aee0105702..39358a13a7319fefc7739e626aec585986e5646d 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 3f4d7bead92255b09b259d1e30c7fb79ea9d63b4..1686406debbc3f5f7f48c658572dc8c42667897d 100755 (executable)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => 'עברית',
         'hr' => 'Hrvatski',
index 9d0439a37d5d2a849c4ed3fb671e070af0002547..ca49aeaad83c5e09c5ff70e2a3fbf6983e89b784 100644 (file)
@@ -248,7 +248,7 @@ return [
         'de_informal' => 'Deutsch (Du)',
         'es' => 'Español',
         'es_AR' => 'Español Argentina',
-        'et' => 'Eesti Keel',
+        'et' => 'Eesti keel',
         'fr' => 'Français',
         'he' => '希伯來語',
         'hr' => 'Hrvatski',
index ee6f3ecc83143ac52b0780ec2a2bbf76a0a2a3df..49ca6663d79c07870d0136440e2998365f755d25 100644 (file)
@@ -242,7 +242,7 @@ class MfaVerificationTest extends TestCase
     }
 
     /**
-     * @return Array<User, string, TestResponse>
+     * @return array<User, string, TestResponse>
      */
     protected function startTotpLogin(): array
     {
@@ -260,7 +260,7 @@ class MfaVerificationTest extends TestCase
     }
 
     /**
-     * @return Array<User, string, TestResponse>
+     * @return array<User, string, TestResponse>
      */
     protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array
     {
index dd996b92633a2984e33b65e56212feed9c37475f..0b99c63c3971ab92f393e90123c04053bba922a4 100644 (file)
@@ -596,16 +596,24 @@ class PageContentTest extends TestCase
 
     public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
     {
-        $this->asEditor();
-        $page = Page::query()->first();
+        // Relevant to https://github.com/BookStackApp/BookStack/issues/3010 and other cases
+        $extensions = [
+            'jiff', 'pngr', 'png ', ' png', '.png', 'png.', 'p.ng', ',png',
+            'data:image/png', ',data:image/png',
+        ];
 
-        $this->put($page->getUrl(), [
-            'name' => $page->name, 'summary' => '',
-            'html' => '<p>test<img src="data:image/jiff;base64,' . $this->base64Jpeg . '"/></p>',
-        ]);
+        foreach ($extensions as $extension) {
+            $this->asEditor();
+            $page = Page::query()->first();
 
-        $page->refresh();
-        $this->assertStringContainsString('<img src=""', $page->html);
+            $this->put($page->getUrl(), [
+                'name' => $page->name, 'summary' => '',
+                'html' => '<p>test<img src="data:image/' . $extension . ';base64,' . $this->base64Jpeg . '"/></p>',
+            ]);
+
+            $page->refresh();
+            $this->assertStringContainsString('<img src=""', $page->html);
+        }
     }
 
     public function test_base64_images_get_extracted_from_markdown_page_content()
index 588b7054ac23be83fd78b4e0e8ae7d18749c8f08..5545edf13255d1bf1df24e8c7e4e370b0f21f545 100644 (file)
@@ -44,6 +44,21 @@ class AttachmentTest extends TestCase
         return Attachment::query()->latest()->first();
     }
 
+    /**
+     * Create a new upload attachment from the given data.
+     */
+    protected function createUploadAttachment(Page $page, string $filename, string $content, string $mimeType): Attachment
+    {
+        $file = tmpfile();
+        $filePath = stream_get_meta_data($file)['uri'];
+        file_put_contents($filePath, $content);
+        $upload = new UploadedFile($filePath, $filename, $mimeType, null, true);
+
+        $this->call('POST', '/attachments/upload', ['uploaded_to' => $page->id], [], ['file' => $upload], []);
+
+        return $page->attachments()->latest()->firstOrFail();
+    }
+
     /**
      * Delete all uploaded files.
      * To assist with cleanup.
@@ -94,7 +109,8 @@ class AttachmentTest extends TestCase
 
         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
         $this->assertStringNotContainsString($fileName, $attachment->path);
-        $this->assertStringEndsWith('.txt', $attachment->path);
+        $this->assertStringEndsWith('-txt', $attachment->path);
+        $this->deleteUploads();
     }
 
     public function test_file_display_and_access()
@@ -305,6 +321,22 @@ class AttachmentTest extends TestCase
         // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
         $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
         $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
+        $attachmentGet->assertHeader('X-Content-Type-Options', 'nosniff');
+
+        $this->deleteUploads();
+    }
+
+    public function test_html_file_access_with_open_forces_plain_content_type()
+    {
+        $page = Page::query()->first();
+        $this->asAdmin();
+
+        $attachment = $this->createUploadAttachment($page, 'test_file.html', '<html></html><p>testing</p>', 'text/html');
+
+        $attachmentGet = $this->get($attachment->getUrl(true));
+        // http-foundation/Response does some 'fixing' of responses to add charsets to text responses.
+        $attachmentGet->assertHeader('Content-Type', 'text/plain; charset=UTF-8');
+        $attachmentGet->assertHeader('Content-Disposition', 'inline; filename="test_file.html"');
 
         $this->deleteUploads();
     }
index 69b6dc90e96218296e84d51db5d788a7af5a7aa5..296e4d1878ae85680e8553e5f0152ea214273dea 100644 (file)
@@ -241,6 +241,36 @@ class ImageTest extends TestCase
         }
     }
 
+    public function test_secure_image_paths_traversal_causes_500()
+    {
+        config()->set('filesystems.images', 'local_secure');
+        $this->asEditor();
+
+        $resp = $this->get('/uploads/images/../../logs/laravel.log');
+        $resp->assertStatus(500);
+    }
+
+    public function test_secure_image_paths_traversal_on_non_secure_images_causes_404()
+    {
+        config()->set('filesystems.images', 'local');
+        $this->asEditor();
+
+        $resp = $this->get('/uploads/images/../../logs/laravel.log');
+        $resp->assertStatus(404);
+    }
+
+    public function test_secure_image_paths_dont_serve_non_images()
+    {
+        config()->set('filesystems.images', 'local_secure');
+        $this->asEditor();
+
+        $testFilePath = storage_path('/uploads/images/testing.txt');
+        file_put_contents($testFilePath, 'hello from test_secure_image_paths_dont_serve_non_images');
+
+        $resp = $this->get('/uploads/images/testing.txt');
+        $resp->assertStatus(404);
+    }
+
     public function test_secure_images_included_in_exports()
     {
         config()->set('filesystems.images', 'local_secure');