mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-25 11:19:39 +03:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01cdbdb7ae | ||
|
|
fc8bbf3eab | ||
|
|
a17be959d8 | ||
|
|
ce3f489188 | ||
|
|
f4201e5740 | ||
|
|
bfbccbede1 | ||
|
|
4360da03d4 | ||
|
|
c7fea8fe08 | ||
|
|
43830a372f | ||
|
|
ae155d6745 | ||
|
|
5c834f24a6 | ||
|
|
85dc8d9791 | ||
|
|
5fd10e695a | ||
|
|
3cdab19319 | ||
|
|
5661d20e87 | ||
|
|
e7bec79f25 | ||
|
|
4f55fe2f8e | ||
|
|
91f80123e8 | ||
|
|
7a0636d0f8 | ||
|
|
3166541002 | ||
|
|
b31fbf5ba8 | ||
|
|
624d55a773 | ||
|
|
42f0ba1875 | ||
|
|
0d312e5348 | ||
|
|
7b244ea012 | ||
|
|
538b5ef4eb | ||
|
|
64937ab826 |
32
.github/SECURITY.md
vendored
Normal file
32
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Only the [latest version](https://github.com/BookStackApp/BookStack/releases) of BookStack is supported.
|
||||||
|
We generally don't support older versions of BookStack due to maintenance effort and
|
||||||
|
since we aim to provide a fairly stable upgrade path for new versions.
|
||||||
|
|
||||||
|
## Security Notifications
|
||||||
|
|
||||||
|
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you've found an issue that likely has no impact to existing users (For example, in a development-only branch)
|
||||||
|
feel free to raise it via a standard GitHub bug report issue.
|
||||||
|
|
||||||
|
If the issue could have a security impact to BookStack instances, please use one of the below
|
||||||
|
methods to report the vulnerability:
|
||||||
|
|
||||||
|
- Directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown).
|
||||||
|
- You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown).
|
||||||
|
- Alternatively you can send a DM via Twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
|
||||||
|
- [Disclose via huntr.dev](https://huntr.dev/bounties/disclose)
|
||||||
|
- Bounties may be available to you through this platform.
|
||||||
|
- Be sure to use `https://github.com/BookStackApp/BookStack` as the repository URL.
|
||||||
|
|
||||||
|
Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability
|
||||||
|
can often take a little time due to the amount of preparation required, to ensure the vulnerability has
|
||||||
|
been covered, and to create the content required to adequately notify the user-base.
|
||||||
|
|
||||||
|
Thank you for keeping BookStack instances safe!
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
https://github.com/BookStackApp/BookStack/graphs/contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ class CommentRepo
|
|||||||
/**
|
/**
|
||||||
* Delete a comment from the system.
|
* Delete a comment from the system.
|
||||||
*/
|
*/
|
||||||
public function delete(Comment $comment)
|
public function delete(Comment $comment): void
|
||||||
{
|
{
|
||||||
$comment->delete();
|
$comment->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the given comment markdown text to HTML.
|
* Convert the given comment Markdown to HTML.
|
||||||
*/
|
*/
|
||||||
public function commentToHtml(string $commentText): string
|
public function commentToHtml(string $commentText): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -281,9 +281,6 @@ class SocialAuthService
|
|||||||
if ($driverName === 'google' && config('services.google.select_account')) {
|
if ($driverName === 'google' && config('services.google.select_account')) {
|
||||||
$driver->with(['prompt' => 'select_account']);
|
$driver->with(['prompt' => 'select_account']);
|
||||||
}
|
}
|
||||||
if ($driverName === 'azure') {
|
|
||||||
$driver->with(['resource' => 'https://graph.windows.net']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
||||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
$this->configureForRedirectCallbacks[$driverName]($driver);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use Illuminate\Support\Collection;
|
|||||||
/**
|
/**
|
||||||
* Class User.
|
* Class User.
|
||||||
*
|
*
|
||||||
* @property string $id
|
* @property int $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $slug
|
* @property string $slug
|
||||||
* @property string $email
|
* @property string $email
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use BookStack\Exceptions\ImageUploadException;
|
|||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
|
use BookStack\Uploads\ImageService;
|
||||||
use BookStack\Util\HtmlContentFilter;
|
use BookStack\Util\HtmlContentFilter;
|
||||||
use DOMDocument;
|
use DOMDocument;
|
||||||
use DOMNodeList;
|
use DOMNodeList;
|
||||||
@@ -86,30 +87,13 @@ class PageContent
|
|||||||
$body = $container->childNodes->item(0);
|
$body = $container->childNodes->item(0);
|
||||||
$childNodes = $body->childNodes;
|
$childNodes = $body->childNodes;
|
||||||
$xPath = new DOMXPath($doc);
|
$xPath = new DOMXPath($doc);
|
||||||
$imageRepo = app()->make(ImageRepo::class);
|
|
||||||
|
|
||||||
// Get all img elements with image data blobs
|
// Get all img elements with image data blobs
|
||||||
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
|
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
|
||||||
foreach ($imageNodes as $imageNode) {
|
foreach ($imageNodes as $imageNode) {
|
||||||
$imageSrc = $imageNode->getAttribute('src');
|
$imageSrc = $imageNode->getAttribute('src');
|
||||||
[$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
|
$newUrl = $this->base64ImageUriToUploadedImageUrl($imageSrc);
|
||||||
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
|
$imageNode->setAttribute('src', $newUrl);
|
||||||
|
|
||||||
// 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', '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate inner html as a string
|
// Generate inner html as a string
|
||||||
@@ -126,34 +110,59 @@ class PageContent
|
|||||||
*/
|
*/
|
||||||
protected function extractBase64ImagesFromMarkdown(string $markdown)
|
protected function extractBase64ImagesFromMarkdown(string $markdown)
|
||||||
{
|
{
|
||||||
$imageRepo = app()->make(ImageRepo::class);
|
|
||||||
$matches = [];
|
$matches = [];
|
||||||
preg_match_all('/!\[.*?]\(.*?(data:image\/.*?)[)"\s]/', $markdown, $matches);
|
preg_match_all('/!\[.*?]\(.*?(data:image\/.*?)[)"\s]/', $markdown, $matches);
|
||||||
|
|
||||||
foreach ($matches[1] as $base64Match) {
|
foreach ($matches[1] as $base64Match) {
|
||||||
[$dataDefinition, $base64ImageData] = explode(',', $base64Match, 2);
|
$newUrl = $this->base64ImageUriToUploadedImageUrl($base64Match);
|
||||||
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
|
$markdown = str_replace($base64Match, $newUrl, $markdown);
|
||||||
|
|
||||||
// Validate extension
|
|
||||||
if (!$imageRepo->imageExtensionSupported($extension)) {
|
|
||||||
$markdown = str_replace($base64Match, '', $markdown);
|
|
||||||
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);
|
|
||||||
$markdown = str_replace($base64Match, $image->url, $markdown);
|
|
||||||
} catch (ImageUploadException $exception) {
|
|
||||||
$markdown = str_replace($base64Match, '', $markdown);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $markdown;
|
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) . '.' . $imageInfo['extension'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$image = $imageRepo->saveNewFromData($imageName, $imageInfo['data'], 'gallery', $this->page->id);
|
||||||
|
} catch (ImageUploadException $exception) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ?: '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats a page's html to be tagged correctly within the system.
|
* Formats a page's html to be tagged correctly within the system.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class AttachmentController extends Controller
|
|||||||
'file' => 'required|file',
|
'file' => 'required|file',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** @var Attachment $attachment */
|
||||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||||
$this->checkOwnablePermission('view', $attachment->page);
|
$this->checkOwnablePermission('view', $attachment->page);
|
||||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||||
@@ -86,11 +87,10 @@ class AttachmentController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the update form for an attachment.
|
* Get the update form for an attachment.
|
||||||
*
|
|
||||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
*/
|
*/
|
||||||
public function getUpdateForm(string $attachmentId)
|
public function getUpdateForm(string $attachmentId)
|
||||||
{
|
{
|
||||||
|
/** @var Attachment $attachment */
|
||||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||||
@@ -173,6 +173,8 @@ class AttachmentController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attachments for a specific page.
|
* Get the attachments for a specific page.
|
||||||
|
*
|
||||||
|
* @throws NotFoundException
|
||||||
*/
|
*/
|
||||||
public function listForPage(int $pageId)
|
public function listForPage(int $pageId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers;
|
|||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Interfaces\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\Model;
|
||||||
use finfo;
|
use BookStack\Util\WebSafeMimeSniffer;
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||||
@@ -117,8 +117,9 @@ abstract class Controller extends BaseController
|
|||||||
protected function downloadResponse(string $content, string $fileName): Response
|
protected function downloadResponse(string $content, string $fileName): Response
|
||||||
{
|
{
|
||||||
return response()->make($content, 200, [
|
return response()->make($content, 200, [
|
||||||
'Content-Type' => 'application/octet-stream',
|
'Content-Type' => 'application/octet-stream',
|
||||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
|
'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
|
protected function inlineDownloadResponse(string $content, string $fileName): Response
|
||||||
{
|
{
|
||||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
$mime = (new WebSafeMimeSniffer())->sniff($content);
|
||||||
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
|
|
||||||
|
|
||||||
return response()->make($content, 200, [
|
return response()->make($content, 200, [
|
||||||
'Content-Type' => $mime,
|
'Content-Type' => $mime,
|
||||||
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
|
'Content-Disposition' => 'inline; filename="' . $fileName . '"',
|
||||||
|
'X-Content-Type-Options' => 'nosniff',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,12 @@ class DrawioImageController extends Controller
|
|||||||
public function getAsBase64($id)
|
public function getAsBase64($id)
|
||||||
{
|
{
|
||||||
$image = $this->imageRepo->getById($id);
|
$image = $this->imageRepo->getById($id);
|
||||||
$page = $image->getPage();
|
if (is_null($image) || $image->type !== 'drawio' || !userCan('page-view', $image->getPage())) {
|
||||||
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
|
|
||||||
return $this->jsonError('Image data could not be found');
|
return $this->jsonError('Image data could not be found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$imageData = $this->imageRepo->getImageData($image);
|
$imageData = $this->imageRepo->getImageData($image);
|
||||||
if ($imageData === null) {
|
if (is_null($imageData)) {
|
||||||
return $this->jsonError('Image data could not be found');
|
return $this->jsonError('Image data could not be found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,25 +7,23 @@ use BookStack\Exceptions\NotFoundException;
|
|||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use BookStack\Uploads\Image;
|
use BookStack\Uploads\Image;
|
||||||
use BookStack\Uploads\ImageRepo;
|
use BookStack\Uploads\ImageRepo;
|
||||||
|
use BookStack\Uploads\ImageService;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Filesystem\Filesystem as File;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class ImageController extends Controller
|
class ImageController extends Controller
|
||||||
{
|
{
|
||||||
protected $image;
|
|
||||||
protected $file;
|
|
||||||
protected $imageRepo;
|
protected $imageRepo;
|
||||||
|
protected $imageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageController constructor.
|
* 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->imageRepo = $imageRepo;
|
||||||
|
$this->imageService = $imageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,14 +33,13 @@ class ImageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showImage(string $path)
|
public function showImage(string $path)
|
||||||
{
|
{
|
||||||
$path = storage_path('uploads/images/' . $path);
|
if (!$this->imageService->pathExistsInLocalSecure($path)) {
|
||||||
if (!file_exists($path)) {
|
|
||||||
throw (new NotFoundException(trans('errors.image_not_found')))
|
throw (new NotFoundException(trans('errors.image_not_found')))
|
||||||
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
||||||
->setDetails(trans('errors.image_not_found_details'));
|
->setDetails(trans('errors.image_not_found_details'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->file($path);
|
return $this->imageService->streamImageFromStorageResponse('gallery', $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Providers;
|
namespace BookStack\Providers;
|
||||||
|
|
||||||
|
use BookStack\Uploads\ImageService;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ class CustomValidationServiceProvider extends ServiceProvider
|
|||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
|
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) {
|
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AttachmentService
|
|||||||
/**
|
/**
|
||||||
* Get the storage that will be used for storing files.
|
* Get the storage that will be used for storing files.
|
||||||
*/
|
*/
|
||||||
protected function getStorage(): FileSystemInstance
|
protected function getStorageDisk(): FileSystemInstance
|
||||||
{
|
{
|
||||||
return $this->fileSystem->disk($this->getStorageDiskName());
|
return $this->fileSystem->disk($this->getStorageDiskName());
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ class AttachmentService
|
|||||||
*/
|
*/
|
||||||
public function getAttachmentFromStorage(Attachment $attachment): string
|
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)
|
protected function deleteFileInStorage(Attachment $attachment)
|
||||||
{
|
{
|
||||||
$storage = $this->getStorage();
|
$storage = $this->getStorageDisk();
|
||||||
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
|
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
|
||||||
|
|
||||||
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
|
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
|
||||||
@@ -213,10 +213,10 @@ class AttachmentService
|
|||||||
{
|
{
|
||||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||||
|
|
||||||
$storage = $this->getStorage();
|
$storage = $this->getStorageDisk();
|
||||||
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
|
$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))) {
|
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
|
||||||
$uploadFileName = Str::random(3) . $uploadFileName;
|
$uploadFileName = Str::random(3) . $uploadFileName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,34 +11,16 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
|||||||
|
|
||||||
class ImageRepo
|
class ImageRepo
|
||||||
{
|
{
|
||||||
protected $image;
|
|
||||||
protected $imageService;
|
protected $imageService;
|
||||||
protected $restrictionService;
|
protected $restrictionService;
|
||||||
protected $page;
|
|
||||||
|
|
||||||
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageRepo constructor.
|
* ImageRepo constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(ImageService $imageService, PermissionService $permissionService)
|
||||||
Image $image,
|
{
|
||||||
ImageService $imageService,
|
|
||||||
PermissionService $permissionService,
|
|
||||||
Page $page
|
|
||||||
) {
|
|
||||||
$this->image = $image;
|
|
||||||
$this->imageService = $imageService;
|
$this->imageService = $imageService;
|
||||||
$this->restrictionService = $permissionService;
|
$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
|
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;
|
$hasMore = count($images) > $pageSize;
|
||||||
|
|
||||||
$returnImages = $images->take($pageSize);
|
$returnImages = $images->take($pageSize);
|
||||||
$returnImages->each(function ($image) {
|
$returnImages->each(function (Image $image) {
|
||||||
$this->loadThumbs($image);
|
$this->loadThumbs($image);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -81,7 +63,7 @@ class ImageRepo
|
|||||||
string $search = null,
|
string $search = null,
|
||||||
callable $whereClause = null
|
callable $whereClause = null
|
||||||
): array {
|
): array {
|
||||||
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
|
$imageQuery = Image::query()->where('type', '=', strtolower($type));
|
||||||
|
|
||||||
if ($uploadedTo !== null) {
|
if ($uploadedTo !== null) {
|
||||||
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
|
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
|
||||||
@@ -112,7 +94,8 @@ class ImageRepo
|
|||||||
int $uploadedTo = null,
|
int $uploadedTo = null,
|
||||||
string $search = null
|
string $search = null
|
||||||
): array {
|
): array {
|
||||||
$contextPage = $this->page->findOrFail($uploadedTo);
|
/** @var Page $contextPage */
|
||||||
|
$contextPage = Page::visible()->findOrFail($uploadedTo);
|
||||||
$parentFilter = null;
|
$parentFilter = null;
|
||||||
|
|
||||||
if ($filterType === 'book' || $filterType === 'page') {
|
if ($filterType === 'book' || $filterType === 'page') {
|
||||||
@@ -147,7 +130,7 @@ class ImageRepo
|
|||||||
*
|
*
|
||||||
* @throws ImageUploadException
|
* @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);
|
$image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||||
$this->loadThumbs($image);
|
$this->loadThumbs($image);
|
||||||
@@ -156,13 +139,13 @@ class ImageRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a drawing the the database.
|
* Save a drawing in the database.
|
||||||
*
|
*
|
||||||
* @throws ImageUploadException
|
* @throws ImageUploadException
|
||||||
*/
|
*/
|
||||||
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
|
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);
|
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.
|
* Update the details of an image via an array of properties.
|
||||||
*
|
*
|
||||||
* @throws ImageUploadException
|
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function updateImageDetails(Image $image, $updateDetails): Image
|
public function updateImageDetails(Image $image, $updateDetails): Image
|
||||||
@@ -187,13 +169,11 @@ class ImageRepo
|
|||||||
*
|
*
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function destroyImage(Image $image = null): bool
|
public function destroyImage(Image $image = null): void
|
||||||
{
|
{
|
||||||
if ($image) {
|
if ($image) {
|
||||||
$this->imageService->destroy($image);
|
$this->imageService->destroy($image);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,9 +181,9 @@ class ImageRepo
|
|||||||
*
|
*
|
||||||
* @throws Exception
|
* @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) {
|
foreach ($images as $image) {
|
||||||
$this->destroyImage($image);
|
$this->destroyImage($image);
|
||||||
}
|
}
|
||||||
@@ -211,25 +191,21 @@ class ImageRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load thumbnails onto an image object.
|
* 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),
|
'gallery' => $this->getThumbnail($image, 150, 150, false),
|
||||||
'display' => $this->getThumbnail($image, 1680, null, true),
|
'display' => $this->getThumbnail($image, 1680, null, true),
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the thumbnail for an image.
|
* Get the thumbnail for an image.
|
||||||
* If $keepRatio is true only the width will be used.
|
* If $keepRatio is true only the width will be used.
|
||||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
* 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 {
|
try {
|
||||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
|||||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||||
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
|
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Intervention\Image\Exception\NotSupportedException;
|
use Intervention\Image\Exception\NotSupportedException;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
use League\Flysystem\Util;
|
use League\Flysystem\Util;
|
||||||
|
use Psr\SimpleCache\InvalidArgumentException;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
class ImageService
|
class ImageService
|
||||||
{
|
{
|
||||||
@@ -25,6 +28,8 @@ class ImageService
|
|||||||
protected $image;
|
protected $image;
|
||||||
protected $fileSystem;
|
protected $fileSystem;
|
||||||
|
|
||||||
|
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageService constructor.
|
* ImageService constructor.
|
||||||
*/
|
*/
|
||||||
@@ -39,11 +44,20 @@ class ImageService
|
|||||||
/**
|
/**
|
||||||
* Get the storage that will be used for storing images.
|
* 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));
|
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.
|
* Change the originally provided path to fit any disk-specific requirements.
|
||||||
* This also ensures the path is kept to the expected root folders.
|
* 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
|
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');
|
$secureUploads = setting('app-secure-images');
|
||||||
$fileName = $this->cleanImageFileName($imageName);
|
$fileName = $this->cleanImageFileName($imageName);
|
||||||
|
|
||||||
@@ -144,7 +158,7 @@ class ImageService
|
|||||||
try {
|
try {
|
||||||
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData);
|
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData);
|
||||||
} catch (Exception $e) {
|
} 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]));
|
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.
|
* If $keepRatio is true only the width will be used.
|
||||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
* 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 Exception
|
||||||
* @throws ImageUploadException
|
* @throws InvalidArgumentException
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
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)) {
|
if ($keepRatio && $this->isGif($image)) {
|
||||||
return $this->getPublicUrl($image->path);
|
return $this->getPublicUrl($image->path);
|
||||||
@@ -243,7 +250,7 @@ class ImageService
|
|||||||
return $this->getPublicUrl($thumbFilePath);
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$storage = $this->getStorage($image->type);
|
$storage = $this->getStorageDisk($image->type);
|
||||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||||
return $this->getPublicUrl($thumbFilePath);
|
return $this->getPublicUrl($thumbFilePath);
|
||||||
}
|
}
|
||||||
@@ -257,27 +264,16 @@ class ImageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize image data.
|
* Resize the image of given data to the specified size, and return the new image data.
|
||||||
*
|
|
||||||
* @param string $imageData
|
|
||||||
* @param int $width
|
|
||||||
* @param int $height
|
|
||||||
* @param bool $keepRatio
|
|
||||||
*
|
*
|
||||||
* @throws ImageUploadException
|
* @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 {
|
try {
|
||||||
$thumb = $this->imageTool->make($imageData);
|
$thumb = $this->imageTool->make($imageData);
|
||||||
} catch (Exception $e) {
|
} catch (ErrorException|NotSupportedException $e) {
|
||||||
if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
|
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw $e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($keepRatio) {
|
if ($keepRatio) {
|
||||||
@@ -307,7 +303,7 @@ class ImageService
|
|||||||
*/
|
*/
|
||||||
public function getImageData(Image $image): string
|
public function getImageData(Image $image): string
|
||||||
{
|
{
|
||||||
$storage = $this->getStorage();
|
$storage = $this->getStorageDisk();
|
||||||
|
|
||||||
return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type));
|
return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type));
|
||||||
}
|
}
|
||||||
@@ -330,7 +326,7 @@ class ImageService
|
|||||||
protected function destroyImagesFromPath(string $path, string $imageType): bool
|
protected function destroyImagesFromPath(string $path, string $imageType): bool
|
||||||
{
|
{
|
||||||
$path = $this->adjustPathForStorageDisk($path, $imageType);
|
$path = $this->adjustPathForStorageDisk($path, $imageType);
|
||||||
$storage = $this->getStorage($imageType);
|
$storage = $this->getStorageDisk($imageType);
|
||||||
|
|
||||||
$imageFolder = dirname($path);
|
$imageFolder = dirname($path);
|
||||||
$imageFileName = basename($path);
|
$imageFileName = basename($path);
|
||||||
@@ -417,7 +413,7 @@ class ImageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$storagePath = $this->adjustPathForStorageDisk($storagePath);
|
$storagePath = $this->adjustPathForStorageDisk($storagePath);
|
||||||
$storage = $this->getStorage();
|
$storage = $this->getStorageDisk();
|
||||||
$imageData = null;
|
$imageData = null;
|
||||||
if ($storage->exists($storagePath)) {
|
if ($storage->exists($storagePath)) {
|
||||||
$imageData = $storage->get($storagePath);
|
$imageData = $storage->get($storagePath);
|
||||||
@@ -435,6 +431,42 @@ class ImageService
|
|||||||
return 'data:image/' . $extension . ';base64,' . base64_encode($imageData);
|
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.
|
* Get a storage path for the given image URL.
|
||||||
* Ensures the path will start with "uploads/images".
|
* Ensures the path will start with "uploads/images".
|
||||||
@@ -476,7 +508,7 @@ class ImageService
|
|||||||
*/
|
*/
|
||||||
private function getPublicUrl(string $filePath): string
|
private function getPublicUrl(string $filePath): string
|
||||||
{
|
{
|
||||||
if ($this->storageUrl === null) {
|
if (is_null($this->storageUrl)) {
|
||||||
$storageUrl = config('filesystems.url');
|
$storageUrl = config('filesystems.url');
|
||||||
|
|
||||||
// Get the standard public s3 url if s3 is set as storage type
|
// 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'];
|
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->storageUrl = $storageUrl;
|
$this->storageUrl = $storageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
app/Util/WebSafeMimeSniffer.php
Normal file
63
app/Util/WebSafeMimeSniffer.php
Normal file
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"predis/predis": "^1.1.6",
|
"predis/predis": "^1.1.6",
|
||||||
"socialiteproviders/discord": "^4.1",
|
"socialiteproviders/discord": "^4.1",
|
||||||
"socialiteproviders/gitlab": "^4.1",
|
"socialiteproviders/gitlab": "^4.1",
|
||||||
"socialiteproviders/microsoft-azure": "^4.1",
|
"socialiteproviders/microsoft-azure": "^5.0.1",
|
||||||
"socialiteproviders/okta": "^4.1",
|
"socialiteproviders/okta": "^4.1",
|
||||||
"socialiteproviders/slack": "^4.1",
|
"socialiteproviders/slack": "^4.1",
|
||||||
"socialiteproviders/twitch": "^5.3",
|
"socialiteproviders/twitch": "^5.3",
|
||||||
|
|||||||
435
composer.lock
generated
435
composer.lock
generated
File diff suppressed because it is too large
Load Diff
21
readme.md
21
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.
|
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
|
## 🛣️ 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.
|
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.
|
||||||
@@ -157,7 +176,7 @@ Security information for administering a BookStack instance can be found on the
|
|||||||
|
|
||||||
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
|
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates).
|
||||||
|
|
||||||
If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
|
If you would like to report a security concern, details of doing so can [can be found here](https://github.com/BookStackApp/BookStack/blob/master/.github/SECURITY.md).
|
||||||
|
|
||||||
## ♿ Accessibility
|
## ♿ Accessibility
|
||||||
|
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'Hebraisk',
|
'he' => 'Hebraisk',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Estnisch',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'Hebräisch',
|
'he' => 'Hebräisch',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Estnisch',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ return [
|
|||||||
|
|
||||||
// Permissions and restrictions
|
// Permissions and restrictions
|
||||||
'permissions' => 'Õigused',
|
'permissions' => 'Õigused',
|
||||||
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
|
'permissions_intro' => 'Kui kohandatud õigused on lubatud, rakendatakse neid eelisjärjekorras, enne rolli õiguseid.',
|
||||||
'permissions_enable' => 'Enable Custom Permissions',
|
'permissions_enable' => 'Luba kohandatud õigused',
|
||||||
'permissions_save' => 'Salvesta õigused',
|
'permissions_save' => 'Salvesta õigused',
|
||||||
'permissions_owner' => 'Omanik',
|
'permissions_owner' => 'Omanik',
|
||||||
|
|
||||||
@@ -240,11 +240,11 @@ return [
|
|||||||
'start_b' => ':userName alustas selle lehe muutmist',
|
'start_b' => ':userName alustas selle lehe muutmist',
|
||||||
'time_a' => 'lehe viimasest muutmisest alates',
|
'time_a' => 'lehe viimasest muutmisest alates',
|
||||||
'time_b' => 'viimase :minCount minuti jooksul',
|
'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_draft_discarded' => 'Mustand ära visatud, redaktorisse laeti lehe värske sisu',
|
||||||
'pages_specific' => 'Specific Page',
|
'pages_specific' => 'Spetsiifiline leht',
|
||||||
'pages_is_template' => 'Page Template',
|
'pages_is_template' => 'Lehe mall',
|
||||||
|
|
||||||
// Editor Sidebar
|
// Editor Sidebar
|
||||||
'page_tags' => 'Lehe sildid',
|
'page_tags' => 'Lehe sildid',
|
||||||
@@ -264,7 +264,7 @@ return [
|
|||||||
'attachments_items' => 'Lisatud objektid',
|
'attachments_items' => 'Lisatud objektid',
|
||||||
'attachments_upload' => 'Laadi fail üles',
|
'attachments_upload' => 'Laadi fail üles',
|
||||||
'attachments_link' => 'Lisa link',
|
'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_delete' => 'Kas oled kindel, et soovid selle manuse kustutada?',
|
||||||
'attachments_dropzone' => 'Manuse lisamiseks lohista failid või klõpsa siin',
|
'attachments_dropzone' => 'Manuse lisamiseks lohista failid või klõpsa siin',
|
||||||
'attachments_no_files' => 'Üleslaaditud faile ei ole',
|
'attachments_no_files' => 'Üleslaaditud faile ei ole',
|
||||||
@@ -274,10 +274,10 @@ return [
|
|||||||
'attachments_link_url' => 'Link failile',
|
'attachments_link_url' => 'Link failile',
|
||||||
'attachments_link_url_hint' => 'Lehekülje või faili URL',
|
'attachments_link_url_hint' => 'Lehekülje või faili URL',
|
||||||
'attach' => 'Lisa',
|
'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' => 'Muuda faili',
|
||||||
'attachments_edit_file_name' => 'Faili nimi',
|
'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_order_updated' => 'Manuste järjekord muudetud',
|
||||||
'attachments_updated_success' => 'Manuse andmed muudetud',
|
'attachments_updated_success' => 'Manuse andmed muudetud',
|
||||||
'attachments_deleted' => 'Manus kustutatud',
|
'attachments_deleted' => 'Manus kustutatud',
|
||||||
@@ -292,7 +292,7 @@ return [
|
|||||||
'templates_prepend_content' => 'Lisa lehe sisu ette',
|
'templates_prepend_content' => 'Lisa lehe sisu ette',
|
||||||
|
|
||||||
// Profile View
|
// Profile View
|
||||||
'profile_user_for_x' => 'User for :time',
|
'profile_user_for_x' => 'Kasutaja olnud :time',
|
||||||
'profile_created_content' => 'Lisatud sisu',
|
'profile_created_content' => 'Lisatud sisu',
|
||||||
'profile_not_created_pages' => ':userName ei ole ühtegi lehte lisanud',
|
'profile_not_created_pages' => ':userName ei ole ühtegi lehte lisanud',
|
||||||
'profile_not_created_chapters' => ':userName ei ole ühtegi peatükki lisanud',
|
'profile_not_created_chapters' => ':userName ei ole ühtegi peatükki lisanud',
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ return [
|
|||||||
'reg_email_confirmation_toggle' => 'Nõua e-posti aadressi kinnitamist',
|
'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_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' => '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',
|
'reg_confirm_restrict_domain_placeholder' => 'Piirangut ei ole',
|
||||||
|
|
||||||
// Maintenance settings
|
// Maintenance settings
|
||||||
@@ -126,7 +126,7 @@ return [
|
|||||||
|
|
||||||
// Role Settings
|
// Role Settings
|
||||||
'roles' => 'Rollid',
|
'roles' => 'Rollid',
|
||||||
'role_user_roles' => 'Kasutajate rollid',
|
'role_user_roles' => 'Kasutaja rollid',
|
||||||
'role_create' => 'Lisa uus roll',
|
'role_create' => 'Lisa uus roll',
|
||||||
'role_create_success' => 'Roll on lisatud',
|
'role_create_success' => 'Roll on lisatud',
|
||||||
'role_delete' => 'Kustuta roll',
|
'role_delete' => 'Kustuta roll',
|
||||||
@@ -209,8 +209,8 @@ return [
|
|||||||
'users_api_tokens_docs' => 'API dokumentatsioon',
|
'users_api_tokens_docs' => 'API dokumentatsioon',
|
||||||
'users_mfa' => 'Mitmeastmeline autentimine',
|
'users_mfa' => 'Mitmeastmeline autentimine',
|
||||||
'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',
|
'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.',
|
||||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
'users_mfa_x_methods' => ':count meetod seadistatud|:count meetodit seadistatud',
|
||||||
'users_mfa_configure' => 'Configure Methods',
|
'users_mfa_configure' => 'Seadista meetodid',
|
||||||
|
|
||||||
// API Tokens
|
// API Tokens
|
||||||
'user_api_token_create' => 'Lisa API tunnus',
|
'user_api_token_create' => 'Lisa API tunnus',
|
||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ return [
|
|||||||
'string' => ':attribute peab sisaldama vähemalt :min tähemärki.',
|
'string' => ':attribute peab sisaldama vähemalt :min tähemärki.',
|
||||||
'array' => ':attribute peab sisaldama vähemalt :min elementi.',
|
'array' => ':attribute peab sisaldama vähemalt :min elementi.',
|
||||||
],
|
],
|
||||||
'not_in' => 'The selected :attribute is invalid.',
|
'not_in' => 'Valitud :attribute on vigane.',
|
||||||
'not_regex' => 'The :attribute format is invalid.',
|
'not_regex' => ':attribute on vigases formaadis.',
|
||||||
'numeric' => 'The :attribute must be a number.',
|
'numeric' => ':attribute peab olema arv.',
|
||||||
'regex' => 'The :attribute format is invalid.',
|
'regex' => ':attribute on vigases formaadis.',
|
||||||
'required' => ':attribute on kohustuslik.',
|
'required' => ':attribute on kohustuslik.',
|
||||||
'required_if' => ':attribute on kohustuslik, kui :other on :value.',
|
'required_if' => ':attribute on kohustuslik, kui :other on :value.',
|
||||||
'required_with' => ':attribute on kohustuslik, kui :values on olemas.',
|
'required_with' => ':attribute on kohustuslik, kui :values on olemas.',
|
||||||
'required_with_all' => ':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' => ':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.',
|
'same' => ':attribute ja :other peavad klappima.',
|
||||||
'safe_url' => 'Link ei pruugi olla turvaline.',
|
'safe_url' => 'Link ei pruugi olla turvaline.',
|
||||||
'size' => [
|
'size' => [
|
||||||
@@ -97,8 +97,8 @@ return [
|
|||||||
'string' => ':attribute peab sisaldama :size tähemärki.',
|
'string' => ':attribute peab sisaldama :size tähemärki.',
|
||||||
'array' => ':attribute peab sisaldama :size elemente.',
|
'array' => ':attribute peab sisaldama :size elemente.',
|
||||||
],
|
],
|
||||||
'string' => 'The :attribute must be a string.',
|
'string' => ':attribute peab olema string.',
|
||||||
'timezone' => 'The :attribute must be a valid zone.',
|
'timezone' => ':attribute peab olema kehtiv ajavöönd.',
|
||||||
'totp' => 'Kood ei ole korrektne või on aegunud.',
|
'totp' => 'Kood ei ole korrektne või on aegunud.',
|
||||||
'unique' => ':attribute on juba võetud.',
|
'unique' => ':attribute on juba võetud.',
|
||||||
'url' => ':attribute on vigases formaadis.',
|
'url' => ':attribute on vigases formaadis.',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Estonien',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'Hébreu',
|
'he' => 'Hébreu',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => '히브리어',
|
'he' => '히브리어',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -81,32 +81,32 @@ return [
|
|||||||
'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',
|
'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',
|
||||||
'mfa_setup_reconfigure' => 'Mainīt 2FA konfigurāciju',
|
'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_remove_confirmation' => 'Vai esi drošs, ka vēlies noņemt divfaktoru autentifikāciju?',
|
||||||
'mfa_setup_action' => 'Setup',
|
'mfa_setup_action' => 'Iestatījumi',
|
||||||
'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_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' => 'Mobile App',
|
'mfa_option_totp_title' => 'Mobilā aplikācija',
|
||||||
'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_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' => 'Backup Codes',
|
'mfa_option_backup_codes_title' => 'Rezerves kodi',
|
||||||
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
'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' => 'Confirm and Enable',
|
'mfa_gen_confirm_and_enable' => 'Apstiprināt un ieslēgt',
|
||||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
'mfa_gen_backup_codes_title' => 'Rezerves kodu iestatījumi',
|
||||||
'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_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' => 'Download Codes',
|
'mfa_gen_backup_codes_download' => 'Lejupielādēt kodus',
|
||||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
'mfa_gen_backup_codes_usage_warning' => 'Katru kodu var izmantot tikai vienreiz',
|
||||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
'mfa_gen_totp_title' => 'Mobilās aplikācijas iestatījumi',
|
||||||
'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_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' => 'Scan the QR code below using your preferred authentication app to get started.',
|
'mfa_gen_totp_scan' => 'Skenējiet zemāk esošo kvadrātkodu (QR) izmantojot savu autentifikācijas aplikāciju.',
|
||||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
'mfa_gen_totp_verify_setup' => 'Verificēt iestatījumus',
|
||||||
'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_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' => 'Provide your app generated code here',
|
'mfa_gen_totp_provide_code_here' => 'Norādīet jūsu aplikācijā izveidoto kodu šeit',
|
||||||
'mfa_verify_access' => 'Verify Access',
|
'mfa_verify_access' => 'Verificēt piekļuvi',
|
||||||
'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_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' => 'No Methods Configured',
|
'mfa_verify_no_methods' => 'Nav iestatīta neviena metode',
|
||||||
'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_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' => 'Verify using a mobile app',
|
'mfa_verify_use_totp' => 'Verificēt, izmantojot mobilo aplikāciju',
|
||||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
'mfa_verify_use_backup_codes' => 'Verificēt, izmantojot rezerves kodu',
|
||||||
'mfa_verify_backup_code' => 'Backup Code',
|
'mfa_verify_backup_code' => 'Rezerves kods',
|
||||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
'mfa_verify_backup_code_desc' => 'Zemāk ievadiet vienu no jūsu atlikušajiem rezerves kodiem:',
|
||||||
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
|
'mfa_verify_backup_code_enter_here' => 'Ievadiet rezerves kodu šeit',
|
||||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
'mfa_verify_totp_desc' => 'Zemāk ievadiet kodu, kas izveidots mobilajā aplikācijā:',
|
||||||
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
|
'mfa_setup_login_notification' => 'Vairākfaktoru metode iestatīta, lūdzu pieslēdzieties atkal izmantojot iestatīto metodi.',
|
||||||
];
|
];
|
||||||
@@ -234,7 +234,7 @@ return [
|
|||||||
'pages_initial_name' => 'Jauna lapa',
|
'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_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_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' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',
|
'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',
|
||||||
'start_b' => ':userName veic izmaiņas šajā lapā',
|
'start_b' => ':userName veic izmaiņas šajā lapā',
|
||||||
|
|||||||
@@ -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_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_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',
|
'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_already_logged_in' => 'Jau esat ielogojies',
|
||||||
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
|
'oidc_user_not_registered' => 'Lietotājs :name nav reģistrēts un automātiska reģistrācija ir izslēgta',
|
||||||
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
'oidc_no_email_address' => 'Ārējās autentifikācijas sistēmas sniegtajos datos nevarēja atrast šī lietotāja e-pasta adresi',
|
||||||
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
|
'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_no_action_defined' => 'Darbības nav definētas',
|
||||||
'social_login_bad_response' => "Saņemta kļūda izmantojot :socialAccount piekļuvi:\n:error",
|
'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.',
|
'social_account_in_use' => 'Šis :socialAccount konts jau tiek izmantots, mēģiniet ieiet ar :socialAccount piekļuves iespēju.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ return [
|
|||||||
'recycle_bin' => 'Miskaste',
|
'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_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_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_by' => 'Izdzēsa',
|
||||||
'recycle_bin_deleted_at' => 'Dzēšanas laiks',
|
'recycle_bin_deleted_at' => 'Dzēšanas laiks',
|
||||||
'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst',
|
'recycle_bin_permanently_delete' => 'Neatgriezeniski izdzēst',
|
||||||
@@ -105,7 +105,7 @@ return [
|
|||||||
'recycle_bin_restore_list' => 'Atjaunojamās vienības',
|
'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_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_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_destroy_notification' => 'Dzēstas kopā :count vienības no miskastes.',
|
||||||
'recycle_bin_restore_notification' => 'Atjaunotas 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_user' => 'Lietotājs',
|
||||||
'audit_table_event' => 'Notikums',
|
'audit_table_event' => 'Notikums',
|
||||||
'audit_table_related' => 'Saistīta vienība vai detaļa',
|
'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_table_date' => 'Notikuma datums',
|
||||||
'audit_date_from' => 'Datums no',
|
'audit_date_from' => 'Datums no',
|
||||||
'audit_date_to' => 'Datums līdz',
|
'audit_date_to' => 'Datums līdz',
|
||||||
@@ -139,7 +139,7 @@ return [
|
|||||||
'role_details' => 'Informācija par grupu',
|
'role_details' => 'Informācija par grupu',
|
||||||
'role_name' => 'Grupas nosaukums',
|
'role_name' => 'Grupas nosaukums',
|
||||||
'role_desc' => 'Īss grupas apaksts',
|
'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_external_auth_id' => 'Ārējais autentifikācijas ID',
|
||||||
'role_system' => 'Sistēmas atļaujas',
|
'role_system' => 'Sistēmas atļaujas',
|
||||||
'role_manage_users' => 'Pārvaldīt lietotājus',
|
'role_manage_users' => 'Pārvaldīt lietotājus',
|
||||||
@@ -149,7 +149,7 @@ return [
|
|||||||
'role_manage_page_templates' => 'Pārvaldīt lapas veidnes',
|
'role_manage_page_templates' => 'Pārvaldīt lapas veidnes',
|
||||||
'role_access_api' => 'Piekļūt sistēmas API',
|
'role_access_api' => 'Piekļūt sistēmas API',
|
||||||
'role_manage_settings' => 'Pārvaldīt iestatījumus',
|
'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',
|
'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.',
|
'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.',
|
'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_create' => 'Izveidot žetonu',
|
||||||
'users_api_tokens_expires' => 'Derīguma termiņš',
|
'users_api_tokens_expires' => 'Derīguma termiņš',
|
||||||
'users_api_tokens_docs' => 'API dokumentācija',
|
'users_api_tokens_docs' => 'API dokumentācija',
|
||||||
'users_mfa' => 'Multi-Factor Authentication',
|
'users_mfa' => 'Vairākfaktoru autentifikācija',
|
||||||
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
'users_mfa_desc' => 'Iestati vairākfaktoru autentifikāciju kā papildus drošības līmeni tavam lietotāja kontam.',
|
||||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
'users_mfa_x_methods' => ':count metode iestatīta|:count metodes iestatītas',
|
||||||
'users_mfa_configure' => 'Configure Methods',
|
'users_mfa_configure' => 'Iestatīt metodes',
|
||||||
|
|
||||||
// API Tokens
|
// API Tokens
|
||||||
'user_api_token_create' => 'Izveidot API žetonu',
|
'user_api_token_create' => 'Izveidot API žetonu',
|
||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Igauņu',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ return [
|
|||||||
'favourite_remove_notification' => '":name" is verwijderd uit je favorieten',
|
'favourite_remove_notification' => '":name" is verwijderd uit je favorieten',
|
||||||
|
|
||||||
// MFA
|
// MFA
|
||||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
'mfa_setup_method_notification' => 'Multi-factor methode succesvol geconfigureerd',
|
||||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
'mfa_remove_method_notification' => 'Multi-factor methode succesvol verwijderd',
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
'commented_on' => 'reageerde op',
|
'commented_on' => 'reageerde op',
|
||||||
|
|||||||
@@ -76,27 +76,27 @@ return [
|
|||||||
'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!',
|
'user_invite_success' => 'Wachtwoord ingesteld, je hebt nu toegang tot :appName!',
|
||||||
|
|
||||||
// Multi-factor Authentication
|
// Multi-factor Authentication
|
||||||
'mfa_setup' => 'Setup Multi-Factor Authentication',
|
'mfa_setup' => 'Multi-factor authenticatie instellen',
|
||||||
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
'mfa_setup_desc' => 'Stel multi-factor authenticatie in als een extra beveiligingslaag voor uw gebruikersaccount.',
|
||||||
'mfa_setup_configured' => 'Already configured',
|
'mfa_setup_configured' => 'Reeds geconfigureerd',
|
||||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
'mfa_setup_reconfigure' => 'Herconfigureren1',
|
||||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
'mfa_setup_remove_confirmation' => 'Weet je zeker dat je deze multi-factor authenticatie methode wilt verwijderen?',
|
||||||
'mfa_setup_action' => 'Setup',
|
'mfa_setup_action' => 'Instellen',
|
||||||
'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_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' => 'Mobile App',
|
'mfa_option_totp_title' => 'Mobiele 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_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' => 'Backup Codes',
|
'mfa_option_backup_codes_title' => 'Back-up 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_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' => 'Confirm and Enable',
|
'mfa_gen_confirm_and_enable' => 'Bevestigen en inschakelen',
|
||||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
'mfa_gen_backup_codes_title' => 'Reservekopiecodes instellen',
|
||||||
'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_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_download' => 'Download Codes',
|
||||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
'mfa_gen_backup_codes_usage_warning' => 'Elke code kan slechts eenmaal gebruikt worden',
|
||||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
'mfa_gen_totp_title' => 'Mobiele app installatie',
|
||||||
'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_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 the QR code below using your preferred authentication app to get started.',
|
'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' => 'Verify Setup',
|
'mfa_gen_totp_verify_setup' => 'Installatie verifiëren',
|
||||||
'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_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_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
||||||
'mfa_verify_access' => 'Verify Access',
|
'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_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.',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ return [
|
|||||||
'reset' => 'Resetten',
|
'reset' => 'Resetten',
|
||||||
'remove' => 'Verwijderen',
|
'remove' => 'Verwijderen',
|
||||||
'add' => 'Toevoegen',
|
'add' => 'Toevoegen',
|
||||||
'configure' => 'Configure',
|
'configure' => 'Configureer',
|
||||||
'fullscreen' => 'Volledig scherm',
|
'fullscreen' => 'Volledig scherm',
|
||||||
'favourite' => 'Favoriet',
|
'favourite' => 'Favoriet',
|
||||||
'unfavourite' => 'Verwijderen uit favoriet',
|
'unfavourite' => 'Verwijderen uit favoriet',
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ return [
|
|||||||
'shelves_permissions' => 'Boekenplank permissies',
|
'shelves_permissions' => 'Boekenplank permissies',
|
||||||
'shelves_permissions_updated' => 'Boekenplank permissies opgeslagen',
|
'shelves_permissions_updated' => 'Boekenplank permissies opgeslagen',
|
||||||
'shelves_permissions_active' => 'Boekenplank permissies actief',
|
'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_to_books' => 'Kopieer permissies naar boeken',
|
||||||
'shelves_copy_permissions' => 'Kopieer permissies',
|
'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.',
|
'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_initial_name' => 'Nieuwe pagina',
|
||||||
'pages_editing_draft_notification' => 'U bewerkt momenteel een concept dat voor het laatst is opgeslagen op :timeDiff.',
|
'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_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' => [
|
'pages_draft_edit_active' => [
|
||||||
'start_a' => ':count gebruikers zijn begonnen deze pagina te bewerken',
|
'start_a' => ':count gebruikers zijn begonnen deze pagina te bewerken',
|
||||||
'start_b' => ':userName is begonnen met het bewerken van deze pagina',
|
'start_b' => ':userName is begonnen met het bewerken van deze pagina',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -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_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',
|
'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_already_logged_in' => 'Już zalogowany',
|
||||||
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
|
'oidc_user_not_registered' => 'Użytkownik :name nie jest zarejestrowany, a automatyczna rejestracja jest wyłączona',
|
||||||
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
'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' => 'Login using :system failed, system did not provide successful authorization',
|
'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_no_action_defined' => 'Brak zdefiniowanej akcji',
|
||||||
'social_login_bad_response' => "Podczas próby logowania :socialAccount wystąpił błąd: \n:error",
|
'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.',
|
'social_account_in_use' => 'To konto :socialAccount jest już w użyciu. Spróbuj zalogować się za pomocą opcji :socialAccount.',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Estoński',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'İbranice',
|
'he' => 'İbranice',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => 'עברית',
|
'he' => 'עברית',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ return [
|
|||||||
'de_informal' => 'Deutsch (Du)',
|
'de_informal' => 'Deutsch (Du)',
|
||||||
'es' => 'Español',
|
'es' => 'Español',
|
||||||
'es_AR' => 'Español Argentina',
|
'es_AR' => 'Español Argentina',
|
||||||
'et' => 'Eesti Keel',
|
'et' => 'Eesti keel',
|
||||||
'fr' => 'Français',
|
'fr' => 'Français',
|
||||||
'he' => '希伯來語',
|
'he' => '希伯來語',
|
||||||
'hr' => 'Hrvatski',
|
'hr' => 'Hrvatski',
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class MfaVerificationTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Array<User, string, TestResponse>
|
* @return array<User, string, TestResponse>
|
||||||
*/
|
*/
|
||||||
protected function startTotpLogin(): array
|
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
|
protected function startBackupCodeLogin($codes = ['kzzu6-1pgll', 'bzxnf-plygd', 'bwdsp-ysl51', '1vo93-ioy7n', 'lf7nw-wdyka', 'xmtrd-oplac']): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -596,16 +596,24 @@ class PageContentTest extends TestCase
|
|||||||
|
|
||||||
public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
|
public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract()
|
||||||
{
|
{
|
||||||
$this->asEditor();
|
// Relevant to https://github.com/BookStackApp/BookStack/issues/3010 and other cases
|
||||||
$page = Page::query()->first();
|
$extensions = [
|
||||||
|
'jiff', 'pngr', 'png ', ' png', '.png', 'png.', 'p.ng', ',png',
|
||||||
|
'data:image/png', ',data:image/png',
|
||||||
|
];
|
||||||
|
|
||||||
$this->put($page->getUrl(), [
|
foreach ($extensions as $extension) {
|
||||||
'name' => $page->name, 'summary' => '',
|
$this->asEditor();
|
||||||
'html' => '<p>test<img src="data:image/jiff;base64,' . $this->base64Jpeg . '"/></p>',
|
$page = Page::query()->first();
|
||||||
]);
|
|
||||||
|
|
||||||
$page->refresh();
|
$this->put($page->getUrl(), [
|
||||||
$this->assertStringContainsString('<img src=""', $page->html);
|
'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()
|
public function test_base64_images_get_extracted_from_markdown_page_content()
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ class AttachmentTest extends TestCase
|
|||||||
return Attachment::query()->latest()->first();
|
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.
|
* Delete all uploaded files.
|
||||||
* To assist with cleanup.
|
* To assist with cleanup.
|
||||||
@@ -94,7 +109,8 @@ class AttachmentTest extends TestCase
|
|||||||
|
|
||||||
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
|
$attachment = Attachment::query()->orderBy('id', 'desc')->first();
|
||||||
$this->assertStringNotContainsString($fileName, $attachment->path);
|
$this->assertStringNotContainsString($fileName, $attachment->path);
|
||||||
$this->assertStringEndsWith('.txt', $attachment->path);
|
$this->assertStringEndsWith('-txt', $attachment->path);
|
||||||
|
$this->deleteUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_file_display_and_access()
|
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.
|
// 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-Type', 'text/plain; charset=UTF-8');
|
||||||
$attachmentGet->assertHeader('Content-Disposition', 'inline; filename="upload_test_file.txt"');
|
$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();
|
$this->deleteUploads();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
public function test_secure_images_included_in_exports()
|
||||||
{
|
{
|
||||||
config()->set('filesystems.images', 'local_secure');
|
config()->set('filesystems.images', 'local_secure');
|
||||||
|
|||||||
Reference in New Issue
Block a user