mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-20 19:07:02 +03:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68d437d05b | ||
|
|
1e56aaea04 | ||
|
|
5ba964b677 | ||
|
|
5647a8a091 | ||
|
|
f3c147d33b | ||
|
|
747f81d5d8 | ||
|
|
c9c0e5e16f | ||
|
|
d21b60079c | ||
|
|
ffa4377e65 | ||
|
|
9b8bb49a33 | ||
|
|
69301f7575 | ||
|
|
b043257d9a | ||
|
|
ca764caf2d | ||
|
|
dab170a6fe | ||
|
|
a8de717d9b | ||
|
|
543ea6ef71 | ||
|
|
a9b3df537f | ||
|
|
c2339ac9db | ||
|
|
41541df6ec | ||
|
|
7224fbcc89 | ||
|
|
81d6b1b016 | ||
|
|
41ac69adb1 | ||
|
|
55be75dee2 | ||
|
|
644bbebb6e | ||
|
|
f99af807d0 | ||
|
|
756b55bbff | ||
|
|
3c4415f3ff | ||
|
|
c2e031ae3e | ||
|
|
537b1614c4 |
2
.github/translators.txt
vendored
2
.github/translators.txt
vendored
@@ -190,3 +190,5 @@ Hl2run :: Slovak
|
||||
Ngo Tri Hoai (trihoai) :: Vietnamese
|
||||
Atalonica :: Catalan
|
||||
慕容潭谈 (591442386) :: Chinese Simplified
|
||||
Radim Pesek (ramess18) :: Czech
|
||||
anastasiia.motylko :: Ukrainian
|
||||
|
||||
@@ -8,6 +8,7 @@ use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use BookStack\Auth\User;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use PragmaRX\Google2FA\Support\Constants;
|
||||
|
||||
@@ -36,11 +37,11 @@ class TotpService
|
||||
/**
|
||||
* Generate a TOTP URL from secret key.
|
||||
*/
|
||||
public function generateUrl(string $secret): string
|
||||
public function generateUrl(string $secret, User $user): string
|
||||
{
|
||||
return $this->google2fa->getQRCodeUrl(
|
||||
setting('app-name'),
|
||||
user()->email,
|
||||
$user->email,
|
||||
$secret
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ return [
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ return [
|
||||
* direct class use like:
|
||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||
*/
|
||||
'chroot' => realpath(base_path()),
|
||||
'chroot' => realpath(public_path()),
|
||||
|
||||
/**
|
||||
* Whether to use Unicode fonts or not.
|
||||
|
||||
@@ -37,9 +37,14 @@ return [
|
||||
'root' => public_path(),
|
||||
],
|
||||
|
||||
'local_secure' => [
|
||||
'local_secure_attachments' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path(),
|
||||
'root' => storage_path('uploads/files/'),
|
||||
],
|
||||
|
||||
'local_secure_images' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('uploads/images/'),
|
||||
],
|
||||
|
||||
's3' => [
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Models;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Class PageRevision.
|
||||
@@ -14,11 +15,13 @@ use Carbon\Carbon;
|
||||
* @property string $book_slug
|
||||
* @property int $created_by
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property string $type
|
||||
* @property string $summary
|
||||
* @property string $markdown
|
||||
* @property string $html
|
||||
* @property int $revision_number
|
||||
* @property Page $page
|
||||
*/
|
||||
class PageRevision extends Model
|
||||
{
|
||||
@@ -26,20 +29,16 @@ class PageRevision extends Model
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function createdBy()
|
||||
public function createdBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this revision originates from.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
public function page(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ class PageEditActivity
|
||||
|
||||
/**
|
||||
* Check if there's active editing being performed on this page.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasActiveEditing(): bool
|
||||
{
|
||||
@@ -43,12 +41,38 @@ class PageEditActivity
|
||||
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any editor clash warning messages to show for the given draft revision.
|
||||
*
|
||||
* @param PageRevision|Page $draft
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getWarningMessagesForDraft($draft): array
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
if ($this->hasActiveEditing()) {
|
||||
$warnings[] = $this->activeEditingMessage();
|
||||
}
|
||||
|
||||
if ($draft instanceof PageRevision && $this->hasPageBeenUpdatedSinceDraftCreated($draft)) {
|
||||
$warnings[] = trans('entities.pages_draft_page_changed_since_creation');
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the page has been updated since the draft has been saved.
|
||||
*/
|
||||
protected function hasPageBeenUpdatedSinceDraftCreated(PageRevision $draft): bool
|
||||
{
|
||||
return $draft->page->updated_at->timestamp > $draft->created_at->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to show when the user will be editing one of their drafts.
|
||||
*
|
||||
* @param PageRevision $draft
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEditingActiveDraftMessage(PageRevision $draft): string
|
||||
{
|
||||
|
||||
@@ -156,7 +156,9 @@ class SearchRunner
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable() . '.*, s.score')->orderBy('score', 'desc');
|
||||
})->addSelect($entity->getTable() . '.*')
|
||||
->selectRaw('s.score')
|
||||
->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
}
|
||||
|
||||
|
||||
49
app/Exceptions/WhoopsBookStackPrettyHandler.php
Normal file
49
app/Exceptions/WhoopsBookStackPrettyHandler.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Whoops\Handler\Handler;
|
||||
|
||||
class WhoopsBookStackPrettyHandler extends Handler
|
||||
{
|
||||
/**
|
||||
* @return int|null A handler may return nothing, or a Handler::HANDLE_* constant
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$exception = $this->getException();
|
||||
|
||||
echo view('errors.debug', [
|
||||
'error' => $exception->getMessage(),
|
||||
'errorClass' => get_class($exception),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'environment' => $this->getEnvironment(),
|
||||
])->render();
|
||||
|
||||
return Handler::QUIT;
|
||||
}
|
||||
|
||||
protected function safeReturn(callable $callback, $default = null)
|
||||
{
|
||||
try {
|
||||
return $callback();
|
||||
} catch (\Exception $e) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getEnvironment(): array
|
||||
{
|
||||
return [
|
||||
'PHP Version' => phpversion(),
|
||||
'BookStack Version' => $this->safeReturn(function () {
|
||||
$versionFile = base_path('version');
|
||||
|
||||
return trim(file_get_contents($versionFile));
|
||||
}, 'unknown'),
|
||||
'Theme Configured' => $this->safeReturn(function () {
|
||||
return config('view.theme');
|
||||
}) ?? 'None',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class ForgotPasswordController extends Controller
|
||||
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
|
||||
}
|
||||
|
||||
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
|
||||
if (in_array($response, [Password::RESET_LINK_SENT, Password::INVALID_USER, Password::RESET_THROTTLED])) {
|
||||
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
||||
$this->showSuccessNotification($message);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class MfaTotpController extends Controller
|
||||
session()->put(static::SETUP_SECRET_SESSION_KEY, encrypt($totpSecret));
|
||||
}
|
||||
|
||||
$qrCodeUrl = $totp->generateUrl($totpSecret);
|
||||
$qrCodeUrl = $totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());
|
||||
$svg = $totp->generateQrCodeSvg($qrCodeUrl);
|
||||
|
||||
return view('mfa.totp-generate', [
|
||||
|
||||
@@ -259,13 +259,13 @@ class PageController extends Controller
|
||||
}
|
||||
|
||||
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $updateTime,
|
||||
'warning' => implode("\n", $warnings),
|
||||
'timestamp' => $draft->updated_at->timestamp,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class Kernel extends HttpKernel
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\BookStack\Http\Middleware\PreventAuthenticatedResponseCaching::class,
|
||||
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
|
||||
\BookStack\Http\Middleware\RunThemeActions::class,
|
||||
\BookStack\Http\Middleware\Localization::class,
|
||||
@@ -39,6 +40,7 @@ class Kernel extends HttpKernel
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\BookStack\Http\Middleware\StartSessionIfCookieExists::class,
|
||||
\BookStack\Http\Middleware\ApiAuthenticate::class,
|
||||
\BookStack\Http\Middleware\PreventAuthenticatedResponseCaching::class,
|
||||
\BookStack\Http\Middleware\CheckEmailConfirmed::class,
|
||||
],
|
||||
];
|
||||
|
||||
31
app/Http/Middleware/PreventAuthenticatedResponseCaching.php
Normal file
31
app/Http/Middleware/PreventAuthenticatedResponseCaching.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PreventAuthenticatedResponseCaching
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
/** @var Response $response */
|
||||
$response = $next($request);
|
||||
|
||||
if (signedInUser()) {
|
||||
$response->headers->set('Cache-Control', 'max-age=0, no-store, private');
|
||||
$response->headers->set('Pragma', 'no-cache');
|
||||
$response->headers->set('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\WhoopsBookStackPrettyHandler;
|
||||
use BookStack\Settings\Setting;
|
||||
use BookStack\Settings\SettingService;
|
||||
use BookStack\Util\CspService;
|
||||
@@ -20,6 +21,7 @@ use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
|
||||
use Whoops\Handler\HandlerInterface;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -65,6 +67,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind(HandlerInterface::class, function ($app) {
|
||||
return $app->make(WhoopsBookStackPrettyHandler::class);
|
||||
});
|
||||
|
||||
$this->app->singleton(SettingService::class, function ($app) {
|
||||
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use League\Flysystem\Util;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService
|
||||
@@ -27,15 +28,39 @@ class AttachmentService
|
||||
* Get the storage that will be used for storing files.
|
||||
*/
|
||||
protected function getStorage(): FileSystemInstance
|
||||
{
|
||||
return $this->fileSystem->disk($this->getStorageDiskName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the storage disk to use.
|
||||
*/
|
||||
protected function getStorageDiskName(): string
|
||||
{
|
||||
$storageType = config('filesystems.attachments');
|
||||
|
||||
// Override default location if set to local public to ensure not visible.
|
||||
if ($storageType === 'local') {
|
||||
$storageType = 'local_secure';
|
||||
// Change to our secure-attachment disk if any of the local options
|
||||
// are used to prevent escaping that location.
|
||||
if ($storageType === 'local' || $storageType === 'local_secure') {
|
||||
$storageType = 'local_secure_attachments';
|
||||
}
|
||||
|
||||
return $this->fileSystem->disk($storageType);
|
||||
return $storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the originally provided path to fit any disk-specific requirements.
|
||||
* This also ensures the path is kept to the expected root folders.
|
||||
*/
|
||||
protected function adjustPathForStorageDisk(string $path): string
|
||||
{
|
||||
$path = Util::normalizePath(str_replace('uploads/files/', '', $path));
|
||||
|
||||
if ($this->getStorageDiskName() === 'local_secure_attachments') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return 'uploads/files/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,26 +70,22 @@ class AttachmentService
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment): string
|
||||
{
|
||||
return $this->getStorage()->get($attachment->path);
|
||||
return $this->getStorage()->get($this->adjustPathForStorageDisk($attachment->path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new attachment upon user upload.
|
||||
*
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param int $page_id
|
||||
*
|
||||
* @throws FileUploadException
|
||||
*
|
||||
* @return Attachment
|
||||
*/
|
||||
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
|
||||
public function saveNewUpload(UploadedFile $uploadedFile, int $page_id): Attachment
|
||||
{
|
||||
$attachmentName = $uploadedFile->getClientOriginalName();
|
||||
$attachmentPath = $this->putFileInStorage($uploadedFile);
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
$largestExistingOrder = Attachment::query()->where('uploaded_to', '=', $page_id)->max('order');
|
||||
|
||||
$attachment = Attachment::forceCreate([
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->forceCreate([
|
||||
'name' => $attachmentName,
|
||||
'path' => $attachmentPath,
|
||||
'extension' => $uploadedFile->getClientOriginalExtension(),
|
||||
@@ -78,17 +99,12 @@ class AttachmentService
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a upload, saving to a file and deleting any existing uploads
|
||||
* Store an upload, saving to a file and deleting any existing uploads
|
||||
* attached to that file.
|
||||
*
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param Attachment $attachment
|
||||
*
|
||||
* @throws FileUploadException
|
||||
*
|
||||
* @return Attachment
|
||||
*/
|
||||
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
|
||||
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment): Attachment
|
||||
{
|
||||
if (!$attachment->external) {
|
||||
$this->deleteFileInStorage($attachment);
|
||||
@@ -160,8 +176,6 @@ class AttachmentService
|
||||
/**
|
||||
* Delete a File from the database and storage.
|
||||
*
|
||||
* @param Attachment $attachment
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteFile(Attachment $attachment)
|
||||
@@ -179,15 +193,13 @@ class AttachmentService
|
||||
/**
|
||||
* Delete a file from the filesystem it sits on.
|
||||
* Cleans any empty leftover folders.
|
||||
*
|
||||
* @param Attachment $attachment
|
||||
*/
|
||||
protected function deleteFileInStorage(Attachment $attachment)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$dirPath = dirname($attachment->path);
|
||||
$dirPath = $this->adjustPathForStorageDisk(dirname($attachment->path));
|
||||
|
||||
$storage->delete($attachment->path);
|
||||
$storage->delete($this->adjustPathForStorageDisk($attachment->path));
|
||||
if (count($storage->allFiles($dirPath)) === 0) {
|
||||
$storage->deleteDirectory($dirPath);
|
||||
}
|
||||
@@ -196,13 +208,9 @@ class AttachmentService
|
||||
/**
|
||||
* Store a file in storage with the given filename.
|
||||
*
|
||||
* @param UploadedFile $uploadedFile
|
||||
*
|
||||
* @throws FileUploadException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function putFileInStorage(UploadedFile $uploadedFile)
|
||||
protected function putFileInStorage(UploadedFile $uploadedFile): string
|
||||
{
|
||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
@@ -210,14 +218,14 @@ class AttachmentService
|
||||
$basePath = 'uploads/files/' . date('Y-m-M') . '/';
|
||||
|
||||
$uploadFileName = Str::random(16) . '.' . $uploadedFile->getClientOriginalExtension();
|
||||
while ($storage->exists($basePath . $uploadFileName)) {
|
||||
while ($storage->exists($this->adjustPathForStorageDisk($basePath . $uploadFileName))) {
|
||||
$uploadFileName = Str::random(3) . $uploadFileName;
|
||||
}
|
||||
|
||||
$attachmentPath = $basePath . $uploadFileName;
|
||||
|
||||
try {
|
||||
$storage->put($attachmentPath, $attachmentData);
|
||||
$storage->put($this->adjustPathForStorageDisk($attachmentPath), $attachmentData);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error when attempting file upload:' . $e->getMessage());
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use League\Flysystem\Util;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService
|
||||
@@ -38,16 +39,43 @@ class ImageService
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
*/
|
||||
protected function getStorage(string $type = ''): FileSystemInstance
|
||||
protected function getStorage(string $imageType = ''): FileSystemInstance
|
||||
{
|
||||
return $this->fileSystem->disk($this->getStorageDiskName($imageType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the originally provided path to fit any disk-specific requirements.
|
||||
* This also ensures the path is kept to the expected root folders.
|
||||
*/
|
||||
protected function adjustPathForStorageDisk(string $path, string $imageType = ''): string
|
||||
{
|
||||
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
|
||||
|
||||
if ($this->getStorageDiskName($imageType) === 'local_secure_images') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return 'uploads/images/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the storage disk to use.
|
||||
*/
|
||||
protected function getStorageDiskName(string $imageType): string
|
||||
{
|
||||
$storageType = config('filesystems.images');
|
||||
|
||||
// Ensure system images (App logo) are uploaded to a public space
|
||||
if ($type === 'system' && $storageType === 'local_secure') {
|
||||
if ($imageType === 'system' && $storageType === 'local_secure') {
|
||||
$storageType = 'local';
|
||||
}
|
||||
|
||||
return $this->fileSystem->disk($storageType);
|
||||
if ($storageType === 'local_secure') {
|
||||
$storageType = 'local_secure_images';
|
||||
}
|
||||
|
||||
return $storageType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +132,7 @@ class ImageService
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . date('Y-m') . '/';
|
||||
|
||||
while ($storage->exists($imagePath . $fileName)) {
|
||||
while ($storage->exists($this->adjustPathForStorageDisk($imagePath . $fileName, $type))) {
|
||||
$fileName = Str::random(3) . $fileName;
|
||||
}
|
||||
|
||||
@@ -114,7 +142,7 @@ class ImageService
|
||||
}
|
||||
|
||||
try {
|
||||
$this->saveImageDataInPublicSpace($storage, $fullPath, $imageData);
|
||||
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($fullPath, $type), $imageData);
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Error when attempting image upload:' . $e->getMessage());
|
||||
|
||||
@@ -216,13 +244,13 @@ class ImageService
|
||||
}
|
||||
|
||||
$storage = $this->getStorage($image->type);
|
||||
if ($storage->exists($thumbFilePath)) {
|
||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
|
||||
$thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio);
|
||||
|
||||
$this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData);
|
||||
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData);
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
@@ -279,10 +307,9 @@ class ImageService
|
||||
*/
|
||||
public function getImageData(Image $image): string
|
||||
{
|
||||
$imagePath = $image->path;
|
||||
$storage = $this->getStorage();
|
||||
|
||||
return $storage->get($imagePath);
|
||||
return $storage->get($this->adjustPathForStorageDisk($image->path, $image->type));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +319,7 @@ class ImageService
|
||||
*/
|
||||
public function destroy(Image $image)
|
||||
{
|
||||
$this->destroyImagesFromPath($image->path);
|
||||
$this->destroyImagesFromPath($image->path, $image->type);
|
||||
$image->delete();
|
||||
}
|
||||
|
||||
@@ -300,9 +327,10 @@ class ImageService
|
||||
* Destroys an image at the given path.
|
||||
* Searches for image thumbnails in addition to main provided path.
|
||||
*/
|
||||
protected function destroyImagesFromPath(string $path): bool
|
||||
protected function destroyImagesFromPath(string $path, string $imageType): bool
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$path = $this->adjustPathForStorageDisk($path, $imageType);
|
||||
$storage = $this->getStorage($imageType);
|
||||
|
||||
$imageFolder = dirname($path);
|
||||
$imageFileName = basename($path);
|
||||
@@ -326,7 +354,7 @@ class ImageService
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* Check whether a folder is empty.
|
||||
*/
|
||||
protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
|
||||
{
|
||||
@@ -374,7 +402,7 @@ class ImageService
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a image URI to a Base64 encoded string.
|
||||
* Convert an image URI to a Base64 encoded string.
|
||||
* Attempts to convert the URL to a system storage url then
|
||||
* fetch the data from the disk or storage location.
|
||||
* Returns null if the image data cannot be fetched from storage.
|
||||
@@ -388,6 +416,7 @@ class ImageService
|
||||
return null;
|
||||
}
|
||||
|
||||
$storagePath = $this->adjustPathForStorageDisk($storagePath);
|
||||
$storage = $this->getStorage();
|
||||
$imageData = null;
|
||||
if ($storage->exists($storagePath)) {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
"barryvdh/laravel-dompdf": "^0.9.0",
|
||||
"barryvdh/laravel-snappy": "^0.4.8",
|
||||
"doctrine/dbal": "^2.12.1",
|
||||
"facade/ignition": "^1.16.4",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"filp/whoops": "^2.14",
|
||||
"intervention/image": "^2.5.1",
|
||||
"laravel/framework": "^6.20.33",
|
||||
"laravel/socialite": "^5.1",
|
||||
|
||||
624
composer.lock
generated
624
composer.lock
generated
File diff suppressed because it is too large
Load Diff
2
public/dist/app.js
vendored
2
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
17
readme.md
17
readme.md
@@ -14,17 +14,18 @@ A platform for storing and organising information and documentation. Details for
|
||||
* [Documentation](https://www.bookstackapp.com/docs)
|
||||
* [Demo Instance](https://demo.bookstackapp.com)
|
||||
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
|
||||
* [Screenshots](https://www.bookstackapp.com/#screenshots)
|
||||
* [BookStack Blog](https://www.bookstackapp.com/blog)
|
||||
* [Issue List](https://github.com/BookStackApp/BookStack/issues)
|
||||
* [Discord Chat](https://discord.gg/ztkBqR2)
|
||||
|
||||
## 📚 Project Definition
|
||||
|
||||
BookStack is an opinionated wiki system that provides a pleasant and simple out of the box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
|
||||
BookStack is an opinionated wiki system that provides a pleasant and simple out-of-the-box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it but they should not interfere with the core simple user experience.
|
||||
|
||||
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
|
||||
|
||||
In regards 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.
|
||||
|
||||
## 🛣️ Road Map
|
||||
|
||||
@@ -41,17 +42,23 @@ Below is a high-level road map view for BookStack to provide a sense of directio
|
||||
|
||||
## 🚀 Release Versioning & Process
|
||||
|
||||
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
|
||||
BookStack releases are each assigned a date-based version number in the format `v<year>.<month>[.<optional_patch_number>]`. For example:
|
||||
|
||||
- `v20.12` - New feature released launched during December 2020.
|
||||
- `v21.06.2` - Second patch release upon the June 2021 feature release.
|
||||
|
||||
Patch releases are generally fairly minor, primarily intended for fixes and therefore is fairly unlikely to cause breakages upon update.
|
||||
Feature releases are generally larger, bringing new features in addition to fixes and enhancements. These releases have a greater chance of introducing breaking changes upon update, so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/).
|
||||
|
||||
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
|
||||
|
||||
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
|
||||
Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates).
|
||||
|
||||
## 🛠️ Development & Testing
|
||||
|
||||
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
|
||||
|
||||
* [Node.js](https://nodejs.org/en/) v12.0+
|
||||
* [Node.js](https://nodejs.org/en/) v14.0+
|
||||
|
||||
This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks:
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class PageEditor {
|
||||
frequency: 30000,
|
||||
last: 0,
|
||||
};
|
||||
this.shownWarningsCache = new Set();
|
||||
|
||||
if (this.pageId !== 0 && this.draftsEnabled) {
|
||||
window.setTimeout(() => {
|
||||
@@ -119,6 +120,10 @@ class PageEditor {
|
||||
}
|
||||
this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
|
||||
this.autoSave.last = Date.now();
|
||||
if (resp.data.warning && !this.shownWarningsCache.has(resp.data.warning)) {
|
||||
window.$events.emit('warning', resp.data.warning);
|
||||
this.shownWarningsCache.add(resp.data.warning);
|
||||
}
|
||||
} catch (err) {
|
||||
// Save the editor content in LocalStorage as a last resort, just in case.
|
||||
try {
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'صفحة جديدة',
|
||||
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
|
||||
'start_b' => ':userName بدأ بتعديل هذه الصفحة',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Нова страница',
|
||||
'pages_editing_draft_notification' => 'В момента редактирате чернова, която беше последно обновена :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Тази страница беше актуализирана от тогава. Препоръчително е да изтриете настоящата чернова.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count потребителя започнаха да редактират настоящата страница',
|
||||
'start_b' => ':userName в момента редактира тази страница',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nova stranica',
|
||||
'pages_editing_draft_notification' => 'Trenutno uređujete skicu koja je posljednji put snimljena :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Ova stranica je ažurirana nakon tog vremena. Preporučujemo da odbacite ovu skicu.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count korisnika je počelo sa uređivanjem ove stranice',
|
||||
'start_b' => ':userName je počeo/la sa uređivanjem ove stranice',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Pàgina nova',
|
||||
'pages_editing_draft_notification' => 'Esteu editant un esborrany que es va desar per darrer cop :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Aquesta pàgina s\'ha actualitzat d\'ençà d\'aleshores. Us recomanem que descarteu aquest esborrany.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count usuaris han començat a editar aquesta pàgina',
|
||||
'start_b' => ':userName ha començat a editar aquesta pàgina',
|
||||
|
||||
@@ -48,8 +48,8 @@ return [
|
||||
'favourite_remove_notification' => '":name" byla odstraněna z Vašich oblíbených',
|
||||
|
||||
// MFA
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
'mfa_setup_method_notification' => 'Vícefaktorová metoda byla úspěšně nakonfigurována',
|
||||
'mfa_remove_method_notification' => 'Vícefaktorová metoda byla úspěšně odstraněna',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'okomentoval/a',
|
||||
|
||||
@@ -83,16 +83,16 @@ return [
|
||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
||||
'mfa_setup_action' => 'Setup',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||
'mfa_option_totp_title' => 'Mobile App',
|
||||
'mfa_option_totp_title' => 'Mobilní aplikace',
|
||||
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
'mfa_option_backup_codes_title' => 'Backup Codes',
|
||||
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
||||
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
|
||||
'mfa_gen_confirm_and_enable' => 'Potvrdit a povolit',
|
||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
||||
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
|
||||
'mfa_gen_backup_codes_download' => 'Download Codes',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
||||
'mfa_gen_totp_title' => 'Mobile App Setup',
|
||||
'mfa_gen_totp_title' => 'Nastavení mobilní aplikace',
|
||||
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
|
||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
||||
|
||||
@@ -39,7 +39,7 @@ return [
|
||||
'reset' => 'Obnovit',
|
||||
'remove' => 'Odebrat',
|
||||
'add' => 'Přidat',
|
||||
'configure' => 'Configure',
|
||||
'configure' => 'Nastavit',
|
||||
'fullscreen' => 'Celá obrazovka',
|
||||
'favourite' => 'Přidat do oblíbených',
|
||||
'unfavourite' => 'Odebrat z oblíbených',
|
||||
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
'shelves_permissions' => 'Oprávnění knihovny',
|
||||
'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována',
|
||||
'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_permissions_cascade_warning' => 'Oprávnění v Knihovnách nejsou automaticky kaskádována do obsažených knih. To proto, že kniha může existovat ve více Knihovnách. Oprávnění však lze zkopírovat do podřízených knih pomocí níže uvedené možnosti.',
|
||||
'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',
|
||||
'shelves_copy_permissions' => 'Kopírovat oprávnění',
|
||||
'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nová stránka',
|
||||
'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen před :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Tato stránka se od té doby změnila. Je doporučeno aktuální koncept zahodit.',
|
||||
'pages_draft_page_changed_since_creation' => 'Tato stránka byla aktualizována od vytvoření tohoto konceptu. Doporučuje se zrušit tento koncept nebo se postarat o to, abyste si nepřepsali žádné již zadané změny.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => 'Uživatelé začali upravovat tuto stránku (celkem :count)',
|
||||
'start_b' => ':userName začal/a upravovat tuto stránku',
|
||||
|
||||
@@ -119,7 +119,7 @@ return [
|
||||
'audit_table_user' => 'Uživatel',
|
||||
'audit_table_event' => 'Událost',
|
||||
'audit_table_related' => 'Související položka nebo detail',
|
||||
'audit_table_ip' => 'IP Address',
|
||||
'audit_table_ip' => 'IP adresa',
|
||||
'audit_table_date' => 'Datum aktivity',
|
||||
'audit_date_from' => 'Časový rozsah od',
|
||||
'audit_date_to' => 'Časový rozsah do',
|
||||
@@ -139,7 +139,7 @@ return [
|
||||
'role_details' => 'Detaily role',
|
||||
'role_name' => 'Název role',
|
||||
'role_desc' => 'Stručný popis role',
|
||||
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
|
||||
'role_mfa_enforced' => 'Vyžaduje Vícefaktorové ověření',
|
||||
'role_external_auth_id' => 'Přihlašovací identifikátory třetích stran',
|
||||
'role_system' => 'Systémová oprávnění',
|
||||
'role_manage_users' => 'Správa uživatelů',
|
||||
@@ -149,7 +149,7 @@ return [
|
||||
'role_manage_page_templates' => 'Správa šablon stránek',
|
||||
'role_access_api' => 'Přístup k systémovému API',
|
||||
'role_manage_settings' => 'Správa nastavení aplikace',
|
||||
'role_export_content' => 'Export content',
|
||||
'role_export_content' => 'Exportovat obsah',
|
||||
'role_asset' => 'Obsahová oprávnění',
|
||||
'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',
|
||||
'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',
|
||||
@@ -207,10 +207,10 @@ return [
|
||||
'users_api_tokens_create' => 'Vytvořit Token',
|
||||
'users_api_tokens_expires' => 'Vyprší',
|
||||
'users_api_tokens_docs' => 'API Dokumentace',
|
||||
'users_mfa' => 'Multi-Factor Authentication',
|
||||
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
||||
'users_mfa' => 'Vícefázové ověření',
|
||||
'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',
|
||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
||||
'users_mfa_configure' => 'Configure Methods',
|
||||
'users_mfa_configure' => 'Konfigurovat metody',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Vytvořit API Token',
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'alpha_dash' => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.',
|
||||
'alpha_num' => ':attribute může obsahovat pouze písmena a číslice.',
|
||||
'array' => ':attribute musí být pole.',
|
||||
'backup_codes' => 'The provided code is not valid or has already been used.',
|
||||
'backup_codes' => 'Zadaný kód není platný nebo již byl použit.',
|
||||
'before' => ':attribute musí být datum před :date.',
|
||||
'between' => [
|
||||
'numeric' => ':attribute musí být hodnota mezi :min a :max.',
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
],
|
||||
'string' => ':attribute musí být řetězec znaků.',
|
||||
'timezone' => ':attribute musí být platná časová zóna.',
|
||||
'totp' => 'The provided code is not valid or has expired.',
|
||||
'totp' => 'Zadaný kód je neplatný nebo vypršel.',
|
||||
'unique' => ':attribute musí být unikátní.',
|
||||
'url' => 'Formát :attribute je neplatný.',
|
||||
'uploaded' => 'Nahrávání :attribute se nezdařilo.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Ny side',
|
||||
'pages_editing_draft_notification' => 'Du redigerer en kladde der sidst var gemt :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Siden har været opdateret siden da. Det er anbefalet at du kasserer denne kladde.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count brugerer har begyndt at redigere denne side',
|
||||
'start_b' => ':userName er begyndt at redigere denne side',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Neue Seite',
|
||||
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
|
||||
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
|
||||
'start_b' => ':userName bearbeitet jetzt diese Seite.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Neue Seite',
|
||||
'pages_editing_draft_notification' => 'Du bearbeitest momenten einen Entwurf, der zuletzt :timeDiff gespeichert wurde.',
|
||||
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt verändert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count Benutzer bearbeiten derzeit diese Seite.',
|
||||
'start_b' => ':userName bearbeitet jetzt diese Seite.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'New Page',
|
||||
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count users have started editing this page',
|
||||
'start_b' => ':userName has started editing this page',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Página nueva',
|
||||
'pages_editing_draft_notification' => 'Está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde ese momento. Se recomienda que cancele este borrador.',
|
||||
'pages_draft_page_changed_since_creation' => 'Esta página ha sido actualizada desde que se creó este borrador. Se recomienda descartar este borrador o tener cuidado de no sobrescribir ningún cambio en la página.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count usuarios han comenzado a editar esta página',
|
||||
'start_b' => ':userName ha comenzado a editar esta página',
|
||||
|
||||
@@ -48,8 +48,8 @@ return [
|
||||
'favourite_remove_notification' => '".name" se eliminó de sus favoritos',
|
||||
|
||||
// MFA
|
||||
'mfa_setup_method_notification' => 'Método de Autenticación en Dos Pasos configurado correctamente',
|
||||
'mfa_remove_method_notification' => 'Método de Autenticación en Dos Pasos eliminado correctamente',
|
||||
'mfa_setup_method_notification' => 'Método de autenticación de múltiples factores configurado satisfactoriamente',
|
||||
'mfa_remove_method_notification' => 'Método de autenticación de múltiples factores eliminado satisfactoriamente',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'comentado',
|
||||
|
||||
@@ -76,37 +76,37 @@ return [
|
||||
'user_invite_success' => 'Contraseña establecida, ahora tiene acceso a :appName!',
|
||||
|
||||
// Multi-factor Authentication
|
||||
'mfa_setup' => 'Configurar Autenticación en Dos Pasos',
|
||||
'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.',
|
||||
'mfa_setup' => 'Configurar autenticación de múltiples factores',
|
||||
'mfa_setup_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.',
|
||||
'mfa_setup_configured' => 'Ya está configurado',
|
||||
'mfa_setup_reconfigure' => 'Reconfigurar',
|
||||
'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación de dos pasos?',
|
||||
'mfa_setup_remove_confirmation' => '¿Está seguro que desea eliminar este método de autenticación de múltiples factores?',
|
||||
'mfa_setup_action' => 'Configuración',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.',
|
||||
'mfa_option_totp_title' => 'Aplicación para móviles',
|
||||
'mfa_option_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genere y guarde un nuevo conjunto antes de que se quede sin códigos para evitar que se bloquee su cuenta.',
|
||||
'mfa_option_totp_title' => 'Aplicación móvil',
|
||||
'mfa_option_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitará una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
|
||||
'mfa_option_backup_codes_title' => 'Códigos de Respaldo',
|
||||
'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.',
|
||||
'mfa_option_backup_codes_desc' => 'Almacene de forma segura un conjunto de códigos de respaldo de un solo uso que pueda introducir para verificar su identidad.',
|
||||
'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',
|
||||
'mfa_gen_backup_codes_title' => 'Configuración de Códigos de Respaldo',
|
||||
'mfa_gen_backup_codes_desc' => 'Guarda la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrás usar uno de los códigos como un segundo mecanismo de autenticación.',
|
||||
'mfa_gen_backup_codes_desc' => 'Guarde la siguiente lista de códigos en un lugar seguro. Al acceder al sistema podrá usar uno de los códigos como un segundo mecanismo de autenticación.',
|
||||
'mfa_gen_backup_codes_download' => 'Descargar Códigos',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Cada código puede utilizarse sólo una vez',
|
||||
'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',
|
||||
'mfa_gen_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
|
||||
'mfa_gen_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitará una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
|
||||
'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',
|
||||
'mfa_gen_totp_verify_setup' => 'Verificar Configuración',
|
||||
'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',
|
||||
'mfa_gen_totp_provide_code_here' => 'Introduce aquí tu código generado por la aplicación',
|
||||
'mfa_verify_access' => 'Verificar Acceso',
|
||||
'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.',
|
||||
'mfa_verify_access_desc' => 'Su cuenta de usuario requiere que confirme su identidad a través de un nivel adicional de verificación antes de que se le conceda el acceso. Verifique su identidad usando uno de los métodos configurados para continuar.',
|
||||
'mfa_verify_no_methods' => 'No hay Métodos Configurados',
|
||||
'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',
|
||||
'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de múltiples factores para su cuenta. Tendrá que configurar al menos un método antes de obtener acceso.',
|
||||
'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',
|
||||
'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',
|
||||
'mfa_verify_backup_code' => 'Códigos de Respaldo',
|
||||
'mfa_verify_backup_code' => 'Código de Respaldo',
|
||||
'mfa_verify_backup_code_desc' => 'Introduzca uno de sus códigos de respaldo restantes a continuación:',
|
||||
'mfa_verify_backup_code_enter_here' => 'Introduce el código de respaldo aquí',
|
||||
'mfa_verify_totp_desc' => 'Introduzca el código, generado con tu aplicación móvil, a continuación:',
|
||||
'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicia sesión de nuevo utilizando el método configurado.',
|
||||
'mfa_verify_backup_code_enter_here' => 'Introduzca el código de respaldo aquí',
|
||||
'mfa_verify_totp_desc' => 'A continuación, introduzca el código generado con su aplicación móvil:',
|
||||
'mfa_setup_login_notification' => 'Método de dos factores configurado. Por favor, inicie sesión nuevamente utilizando el método configurado.',
|
||||
];
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
'shelves_permissions' => 'Permisos del Estante',
|
||||
'shelves_permissions_updated' => 'Permisos del Estante actualizados',
|
||||
'shelves_permissions_active' => 'Permisos Activos del Estante',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros contenidos. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.',
|
||||
'shelves_copy_permissions_to_books' => 'Copiar Permisos a los Libros',
|
||||
'shelves_copy_permissions' => 'Copiar Permisos',
|
||||
'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos de este estante a todos los libros contenidos en él. Antes de activarlos, asegúrese que los cambios a los permisos de este estante estén guardados.',
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Página nueva',
|
||||
'pages_editing_draft_notification' => 'Usted está actualmente editando un borrador que fue guardado por última vez el :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
|
||||
'pages_draft_page_changed_since_creation' => 'Esta página fue actualizada desde que se creó este borrador. Se recomienda descartar este borrador o tener cuidado de no sobrescribir ningún cambio en la página.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count usuarios han comenzado a editar esta página',
|
||||
'start_b' => ':userName ha comenzado a editar esta página',
|
||||
|
||||
@@ -92,7 +92,7 @@ return [
|
||||
'recycle_bin' => 'Papelera de Reciclaje',
|
||||
'recycle_bin_desc' => 'Aquí puede restaurar elementos que hayan sido eliminados o elegir eliminarlos permanentemente del sistema. Esta lista no está filtrada a diferencia de las listas de actividad similares en el sistema donde se aplican los filtros de permisos.',
|
||||
'recycle_bin_deleted_item' => 'Elemento Eliminado',
|
||||
'recycle_bin_deleted_parent' => 'Superior',
|
||||
'recycle_bin_deleted_parent' => 'Padre',
|
||||
'recycle_bin_deleted_by' => 'Eliminado por',
|
||||
'recycle_bin_deleted_at' => 'Fecha de eliminación',
|
||||
'recycle_bin_permanently_delete' => 'Eliminar permanentemente',
|
||||
@@ -105,7 +105,7 @@ return [
|
||||
'recycle_bin_restore_list' => 'Elementos a restaurar',
|
||||
'recycle_bin_restore_confirm' => 'Esta acción restaurará el elemento eliminado, incluyendo cualquier elemento secundario, a su ubicación original. Si la ubicación original ha sido eliminada, y ahora está en la papelera de reciclaje, el elemento padre también tendrá que ser restaurado.',
|
||||
'recycle_bin_restore_deleted_parent' => 'El padre de este elemento también ha sido eliminado. Estos permanecerán eliminados hasta que el padre también sea restaurado.',
|
||||
'recycle_bin_restore_parent' => 'Restaurar Superior',
|
||||
'recycle_bin_restore_parent' => 'Restaurar Padre',
|
||||
'recycle_bin_destroy_notification' => 'Eliminados :count elementos de la papelera de reciclaje.',
|
||||
'recycle_bin_restore_notification' => 'Restaurados :count elementos desde la papelera de reciclaje.',
|
||||
|
||||
@@ -119,7 +119,7 @@ return [
|
||||
'audit_table_user' => 'Usuario',
|
||||
'audit_table_event' => 'Evento',
|
||||
'audit_table_related' => 'Elemento o detalle relacionados',
|
||||
'audit_table_ip' => 'IP Address',
|
||||
'audit_table_ip' => 'Dirección IP',
|
||||
'audit_table_date' => 'Fecha de la Actividad',
|
||||
'audit_date_from' => 'Inicio del Rango de Fecha',
|
||||
'audit_date_to' => 'Final del Rango de Fecha',
|
||||
@@ -139,7 +139,7 @@ return [
|
||||
'role_details' => 'Detalles de rol',
|
||||
'role_name' => 'Nombre de rol',
|
||||
'role_desc' => 'Descripción corta de rol',
|
||||
'role_mfa_enforced' => 'Requiere Autenticación en Dos Pasos',
|
||||
'role_mfa_enforced' => 'Requiere autenticación de múltiples factores',
|
||||
'role_external_auth_id' => 'IDs de Autenticación Externa',
|
||||
'role_system' => 'Permisos de sistema',
|
||||
'role_manage_users' => 'Gestionar usuarios',
|
||||
@@ -208,8 +208,8 @@ return [
|
||||
'users_api_tokens_create' => 'Crear token',
|
||||
'users_api_tokens_expires' => 'Expira',
|
||||
'users_api_tokens_docs' => 'Documentación API',
|
||||
'users_mfa' => 'Autenticación en Dos Pasos',
|
||||
'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.',
|
||||
'users_mfa' => 'Autenticación de múltiples factores',
|
||||
'users_mfa_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.',
|
||||
'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',
|
||||
'users_mfa_configure' => 'Configurar Métodos',
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
|
||||
'alpha_num' => 'El :attribute solo puede contener letras y número.',
|
||||
'array' => 'El :attribute debe de ser un array.',
|
||||
'backup_codes' => 'El código suministrado no es válido o ya ha sido utilizado.',
|
||||
'backup_codes' => 'El código suministrado no es válido o ya fue utilizado.',
|
||||
'before' => 'El :attribute debe ser una fecha anterior a :date.',
|
||||
'between' => [
|
||||
'numeric' => 'El :attribute debe estar entre :min y :max.',
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
],
|
||||
'string' => 'El atributo :attribute debe ser una cadena.',
|
||||
'timezone' => 'El atributo :attribute debe ser una zona válida.',
|
||||
'totp' => 'El código suministrado no es válido o ya ha expirado.',
|
||||
'totp' => 'El código suministrado no es válido o ya expiró.',
|
||||
'unique' => 'El atributo :attribute ya ha sido tomado.',
|
||||
'url' => 'El atributo :attribute tiene un formato inválido.',
|
||||
'uploaded' => 'El archivo no se pudo subir. Puede ser que el servidor no acepte archivos de este tamaño.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'New Page',
|
||||
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count users have started editing this page',
|
||||
'start_b' => ':userName has started editing this page',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nouvelle page',
|
||||
'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été enregistré :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez jeter ce brouillon.',
|
||||
'pages_draft_page_changed_since_creation' => 'Cette page a été mise à jour depuis que ce brouillon a été créé. Il est recommandé de supprimer ce brouillon ou de veiller à ne pas écraser toute modification de page.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count utilisateurs ont commencé à éditer cette page',
|
||||
'start_b' => ':userName a commencé à éditer cette page',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'דף חדש',
|
||||
'pages_editing_draft_notification' => 'הינך עורך טיוטה אשר נשמרה לאחרונה ב :timeDiff',
|
||||
'pages_draft_edited_notification' => 'דף זה עודכן מאז, מומלץ להתעלם מהטיוטה הזו.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count משתמשים החלו לערוך דף זה',
|
||||
'start_b' => ':userName החל לערוך דף זה',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nova stranica',
|
||||
'pages_editing_draft_notification' => 'Uređujete nacrt stranice posljednji put spremljen :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Ova je stranica u međuvremenu ažurirana. Preporučujemo da odbacite ovaj nacrt.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count korisnika koji uređuju ovu stranicu',
|
||||
'start_b' => ':userName je počeo uređivati ovu stranicu',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Új oldal',
|
||||
'pages_editing_draft_notification' => 'A jelenleg szerkesztett vázlat legutóbb ekkor volt elmentve: :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Ezt az oldalt azóta már frissítették. Javasolt ennek a vázlatnak az elvetése.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count felhasználók kezdte el szerkeszteni ezt az oldalt',
|
||||
'start_b' => ':userName elkezdte szerkeszteni ezt az oldalt',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Halaman Baru',
|
||||
'pages_editing_draft_notification' => 'Anda sedang menyunting konsep yang terakhir disimpan :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Halaman ini telah diperbarui sejak saat itu. Anda disarankan untuk membuang draf ini.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count pengguna sudah mulai mengedit halaman ini',
|
||||
'start_b' => ':userName telah memulai menyunting halaman ini',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nuova Pagina',
|
||||
'pages_editing_draft_notification' => 'Stai modificando una bozza che è stata salvata il :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Questa pagina è stata aggiornata. È consigliabile scartare questa bozza.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count hanno iniziato a modificare questa pagina',
|
||||
'start_b' => ':userName ha iniziato a modificare questa pagina',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => '新規ページ',
|
||||
'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',
|
||||
'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count人のユーザがページの編集を開始しました',
|
||||
'start_b' => ':userNameがページの編集を開始しました',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => '제목 없음',
|
||||
'pages_editing_draft_notification' => ':timeDiff에 초안 문서입니다.',
|
||||
'pages_draft_edited_notification' => '최근에 수정한 문서이기 때문에 초안 문서를 폐기하는 편이 좋습니다.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count명이 이 문서를 수정하고 있습니다.',
|
||||
'start_b' => ':userName이 이 문서를 수정하고 있습니다.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Naujas puslapis',
|
||||
'pages_editing_draft_notification' => 'Dabar jūs redaguojate juodraštį, kuris paskutinį kartą buvo išsaugotas :timeDiff',
|
||||
'pages_draft_edited_notification' => 'Šis puslapis buvo redaguotas iki to laiko. Rekomenduojame jums išmesti šį juodraštį.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count naudotojai pradėjo redaguoti šį puslapį',
|
||||
'start_b' => ':userName pradėjo redaguoti šį puslapį',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Jauna lapa',
|
||||
'pages_editing_draft_notification' => 'Jūs pašlaik veicat izmaiņas melnrakstā, kurš pēdējo reizi ir saglabāts :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Šī lapa ir tikusi atjaunināta. Šo melnrakstu ieteicams atmest.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count lietotāji pašlaik veic izmaiņas šajā lapā',
|
||||
'start_b' => ':userName veic izmaiņas šajā lapā',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Ny side',
|
||||
'pages_editing_draft_notification' => 'Du skriver på et utkast som sist ble lagret :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Siden har blitt endret siden du startet. Det anbefales at du forkaster dine endringer.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count forfattere har begynt å endre denne siden.',
|
||||
'start_b' => ':userName skriver på siden for øyeblikket',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nieuwe pagina',
|
||||
'pages_editing_draft_notification' => 'U bewerkt momenteel een concept dat voor het laatst is opgeslagen op :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Deze pagina is sindsdien bijgewerkt. Het wordt aanbevolen dat u dit concept verwijderd.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count gebruikers zijn begonnen deze pagina te bewerken',
|
||||
'start_b' => ':userName is begonnen met het bewerken van deze pagina',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nowa strona',
|
||||
'pages_editing_draft_notification' => 'Edytujesz obecnie wersje roboczą, która była ostatnio zapisana :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tej wersji roboczej.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony',
|
||||
'start_b' => ':userName edytuje stronę',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'página criada',
|
||||
'page_create' => 'criou a página',
|
||||
'page_create_notification' => 'Página criada com sucesso',
|
||||
'page_update' => 'página atualizada',
|
||||
'page_update_notification' => 'Página atualizada com sucesso',
|
||||
@@ -48,8 +48,8 @@ return [
|
||||
'favourite_remove_notification' => '":name" foi removido dos seus favoritos',
|
||||
|
||||
// MFA
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
'mfa_setup_method_notification' => 'Método de múltiplos-fatores configurado com sucesso',
|
||||
'mfa_remove_method_notification' => 'Método de múltiplos-fatores removido com sucesso',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'comentado a',
|
||||
|
||||
@@ -76,12 +76,12 @@ return [
|
||||
'user_invite_success' => 'Palavra-passe definida, tem agora acesso a :appName!',
|
||||
|
||||
// Multi-factor Authentication
|
||||
'mfa_setup' => 'Setup Multi-Factor Authentication',
|
||||
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
||||
'mfa_setup_configured' => 'Already configured',
|
||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
||||
'mfa_setup_action' => 'Setup',
|
||||
'mfa_setup' => 'Configurar autenticação de múltiplos fatores',
|
||||
'mfa_setup_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',
|
||||
'mfa_setup_configured' => 'Já configurado',
|
||||
'mfa_setup_reconfigure' => 'Reconfigurar',
|
||||
'mfa_setup_remove_confirmation' => 'Tem a certeza que deseja remover este método de autenticação de múltiplos fatores?',
|
||||
'mfa_setup_action' => 'Configuração',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||
'mfa_option_totp_title' => 'Mobile App',
|
||||
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
|
||||
@@ -39,7 +39,7 @@ return [
|
||||
'reset' => 'Redefinir',
|
||||
'remove' => 'Remover',
|
||||
'add' => 'Adicionar',
|
||||
'configure' => 'Configure',
|
||||
'configure' => 'Configurar',
|
||||
'fullscreen' => 'Ecrã completo',
|
||||
'favourite' => 'Favorito',
|
||||
'unfavourite' => 'Retirar Favorito',
|
||||
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
'shelves_permissions' => 'Permissões da Estante',
|
||||
'shelves_permissions_updated' => 'Permissões da Estante de Livros Atualizada',
|
||||
'shelves_permissions_active' => 'Permissões da Estante de Livros Ativas',
|
||||
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
|
||||
'shelves_permissions_cascade_warning' => 'As permissões nas estantes não são passadas automaticamente em efeito dominó para os livros contidos. Isto acontece porque um livro pode existir em várias prateleiras. As permissões podem, no entanto, ser copiadas para livros filhos usando a opção encontrada abaixo.',
|
||||
'shelves_copy_permissions_to_books' => 'Copiar Permissões para Livros',
|
||||
'shelves_copy_permissions' => 'Copiar Permissões',
|
||||
'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.',
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nova Página',
|
||||
'pages_editing_draft_notification' => 'Você está atualmente a editar um rascunho que foi guardado pela última vez a :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Esta página entretanto já foi atualizada. É recomendado que você descarte este rascunho.',
|
||||
'pages_draft_page_changed_since_creation' => 'Esta página foi atualizada desde que este rascunho foi criado. É recomendável que descarte este rascunho ou tenha cuidado para não sobrescrever nenhuma alteração de página.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count usuários iniciaram a edição dessa página',
|
||||
'start_b' => ':userName iniciou a edição desta página',
|
||||
|
||||
@@ -119,7 +119,7 @@ return [
|
||||
'audit_table_user' => 'Utilizador',
|
||||
'audit_table_event' => 'Evento',
|
||||
'audit_table_related' => 'Item ou Detalhe Relacionado',
|
||||
'audit_table_ip' => 'IP Address',
|
||||
'audit_table_ip' => 'Endereço de IP',
|
||||
'audit_table_date' => 'Data da Atividade',
|
||||
'audit_date_from' => 'Intervalo De',
|
||||
'audit_date_to' => 'Intervalo Até',
|
||||
@@ -139,7 +139,7 @@ return [
|
||||
'role_details' => 'Detalhes do Cargo',
|
||||
'role_name' => 'Nome do Cargo',
|
||||
'role_desc' => 'Breve Descrição do Cargo',
|
||||
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
|
||||
'role_mfa_enforced' => 'Exige autenticação de múltiplos fatores',
|
||||
'role_external_auth_id' => 'IDs de Autenticação Externa',
|
||||
'role_system' => 'Permissões do Sistema',
|
||||
'role_manage_users' => 'Gerir utilizadores',
|
||||
@@ -149,7 +149,7 @@ return [
|
||||
'role_manage_page_templates' => 'Gerir modelos de página',
|
||||
'role_access_api' => 'Aceder à API do sistema',
|
||||
'role_manage_settings' => 'Gerir as configurações da aplicação',
|
||||
'role_export_content' => 'Export content',
|
||||
'role_export_content' => 'Exportar conteúdo',
|
||||
'role_asset' => 'Permissões de Ativos',
|
||||
'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.',
|
||||
'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.',
|
||||
@@ -207,10 +207,10 @@ return [
|
||||
'users_api_tokens_create' => 'Criar Token',
|
||||
'users_api_tokens_expires' => 'Expira',
|
||||
'users_api_tokens_docs' => 'Documentação da API',
|
||||
'users_mfa' => 'Multi-Factor Authentication',
|
||||
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
||||
'users_mfa_configure' => 'Configure Methods',
|
||||
'users_mfa' => 'Autenticação Multi-fator',
|
||||
'users_mfa_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.',
|
||||
'users_mfa_x_methods' => ':count método configurado|:count métodos configurados',
|
||||
'users_mfa_configure' => 'Configurar Métodos',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Criar Token de API',
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'alpha_dash' => 'O campo :attribute deve conter apenas letras, números, traços e sublinhado.',
|
||||
'alpha_num' => 'O campo :attribute deve conter apenas letras e números.',
|
||||
'array' => 'O campo :attribute deve ser uma lista(array).',
|
||||
'backup_codes' => 'The provided code is not valid or has already been used.',
|
||||
'backup_codes' => 'O código fornecido não é válido ou já foi utilizado.',
|
||||
'before' => 'O campo :attribute deve ser uma data anterior à data :date.',
|
||||
'between' => [
|
||||
'numeric' => 'O campo :attribute deve estar entre :min e :max.',
|
||||
@@ -99,7 +99,7 @@ return [
|
||||
],
|
||||
'string' => 'O campo :attribute deve ser uma string.',
|
||||
'timezone' => 'O campo :attribute deve conter uma timezone válida.',
|
||||
'totp' => 'The provided code is not valid or has expired.',
|
||||
'totp' => 'O código fornecido não é válido ou já expirou.',
|
||||
'unique' => 'Já existe um campo/dado de nome :attribute.',
|
||||
'url' => 'O formato da URL :attribute é inválido.',
|
||||
'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nova Página',
|
||||
'pages_editing_draft_notification' => 'Você está atualmente editando um rascunho que foi salvo da última vez em :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Essa página foi atualizada desde então. É recomendado que você descarte esse rascunho.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count usuários iniciaram a edição dessa página',
|
||||
'start_b' => ':userName iniciou a edição dessa página',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Новая страница',
|
||||
'pages_editing_draft_notification' => 'В настоящее время вы редактируете черновик, который был сохранён :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Эта страница была обновлена до этого момента. Рекомендуется отменить этот черновик.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count пользователей начали редактирование этой страницы',
|
||||
'start_b' => ':userName начал редактирование этой страницы',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nová stránka',
|
||||
'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count používateľov začalo upravovať túto stránku',
|
||||
'start_b' => ':userName začal upravovať túto stránku',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Nova stran',
|
||||
'pages_editing_draft_notification' => 'Trenutno urejate osnutek, ki je bil nazadnje shranjen :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Ta stran je odtlej posodobljena. Priporočamo, da zavržete ta osnutek.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count uporabnikov je začelo urejati to stran',
|
||||
'start_b' => ':userName je začel urejati to stran',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Ny sida',
|
||||
'pages_editing_draft_notification' => 'Du redigerar just nu ett utkast som senast sparades :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Denna sida har uppdaterats sen dess. Vi rekommenderar att du förkastar dina ändringar.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count har börjat redigera den här sidan',
|
||||
'start_b' => ':userName har börjat redigera den här sidan',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Yeni Sayfa',
|
||||
'pages_editing_draft_notification' => 'Şu anda en son :timeDiff tarihinde kaydedilmiş olan taslağı düzenliyorsunuz.',
|
||||
'pages_draft_edited_notification' => 'Bu sayfa o zamandan bu zamana güncellenmiş, bu nedenle bu taslağı yok saymanız önerilir.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count kullanıcı, bu sayfayı düzenlemeye başladı',
|
||||
'start_b' => ':userName, bu sayfayı düzenlemeye başladı',
|
||||
|
||||
@@ -44,12 +44,12 @@ return [
|
||||
'bookshelf_delete_notification' => 'Книжкову полицю успішно видалено',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
'favourite_add_notification' => '":ім\'я" було додане до ваших улюлених',
|
||||
'favourite_remove_notification' => '":ім\'я" було видалено з ваших улюблених',
|
||||
|
||||
// MFA
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
'mfa_setup_method_notification' => 'Багатофакторний метод успішно налаштований',
|
||||
'mfa_remove_method_notification' => 'Багатофакторний метод успішно видалений',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'прокоментував',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Нова сторінка',
|
||||
'pages_editing_draft_notification' => 'Ви наразі редагуєте чернетку, що була збережена останньою :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'З того часу ця сторінка була оновлена. Рекомендуємо відмовитися від цього проекту.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count користувачі(в) почали редагувати цю сторінку',
|
||||
'start_b' => ':userName розпочав редагування цієї сторінки',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => 'Trang mới',
|
||||
'pages_editing_draft_notification' => 'Bạn hiện đang chỉnh sửa một bản nháp được lưu cách đây :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'Trang này đã được cập nhật từ lúc đó. Bạn nên loại bỏ bản nháp này.',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count người dùng đang bắt đầu chỉnh sửa trang này',
|
||||
'start_b' => ':userName đang bắt đầu chỉnh sửa trang này',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => '新页面',
|
||||
'pages_editing_draft_notification' => '您正在编辑在 :timeDiff 内保存的草稿.',
|
||||
'pages_draft_edited_notification' => '此后,此页面已经被更新,建议您放弃此草稿。',
|
||||
'pages_draft_page_changed_since_creation' => '这个页面在您的草稿创建后被其他用户更新了,您目前的草稿不包含新的内容。建议您放弃此草稿,或是注意不要覆盖新的页面更改。',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count位用户正在编辑此页面',
|
||||
'start_b' => '用户“:userName”已经开始编辑此页面',
|
||||
|
||||
@@ -209,8 +209,8 @@ return [
|
||||
'users_api_tokens_docs' => 'API文档',
|
||||
'users_mfa' => '多重身份认证',
|
||||
'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。',
|
||||
'users_mfa_x_methods' => ':count 方法已配置|:count 方法已配置',
|
||||
'users_mfa_configure' => '配置方法',
|
||||
'users_mfa_x_methods' => ':count 个措施已配置|:count 个措施已配置',
|
||||
'users_mfa_configure' => '配置安全措施',
|
||||
|
||||
// API Tokens
|
||||
'user_api_token_create' => '创建 API 令牌',
|
||||
|
||||
@@ -234,6 +234,7 @@ return [
|
||||
'pages_initial_name' => '新頁面',
|
||||
'pages_editing_draft_notification' => '您正在編輯最後儲存為 :timeDiff 的草稿。',
|
||||
'pages_draft_edited_notification' => '此頁面已經被更新過。建議您放棄此草稿。',
|
||||
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count 位使用者已經開始編輯此頁面',
|
||||
'start_b' => '使用者 :userName 已經開始編輯此頁面',
|
||||
|
||||
146
resources/views/errors/debug.blade.php
Normal file
146
resources/views/errors/debug.blade.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Error: {{ $error }}</title>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
background-color: #F2F2F2;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
border-top: 6px solid #206ea7;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: #FFF;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem 2rem;
|
||||
margin: 2rem 1rem;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
color: #FFF;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
background-color: #206ea7;
|
||||
padding: 0.25rem .5rem;
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: scroll;
|
||||
background-color: #EEE;
|
||||
border: 1px solid #DDD;
|
||||
padding: .25rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #206ea7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
text-decoration: underline;
|
||||
color: #105282;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 2rem;
|
||||
padding: 0 2rem;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<p class="notice">
|
||||
WARNING: Application is in debug mode. This mode has the potential to leak confidential
|
||||
information and therefore should not be used in production or publicly
|
||||
accessible environments.
|
||||
</p>
|
||||
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">Error</h4>
|
||||
<h2>{{ $errorClass }}</h2>
|
||||
<h1>{{ $error }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">Help Resources</h4>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.bookstackapp.com/docs/admin/debugging/" target="_blank">Review BookStack debugging documentation »</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/BookStackApp/BookStack/releases" target="_blank">Ensure your instance is up-to-date »</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue+{{ urlencode($error) }}" target="_blank">Search for the issue on GitHub »</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/ztkBqR2" target="_blank">Ask for help via Discord »</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://duckduckgo.com/?q={{urlencode("BookStack {$error}")}}" target="_blank">Search the error message »</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">Environment</h4>
|
||||
<ul>
|
||||
@foreach($environment as $label => $text)
|
||||
<li><strong>{{ $label }}:</strong> {{ $text }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">Stack Trace</h4>
|
||||
<pre>{{ $trace }}</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -282,6 +282,22 @@ class AuthTest extends TestCase
|
||||
->assertElementContains('a', 'Sign up');
|
||||
}
|
||||
|
||||
public function test_reset_password_request_is_throttled()
|
||||
{
|
||||
$editor = $this->getEditor();
|
||||
Notification::fake();
|
||||
$this->get('/password/email');
|
||||
$this->followingRedirects()->post('/password/email', [
|
||||
'email' => $editor->email,
|
||||
]);
|
||||
|
||||
$resp = $this->followingRedirects()->post('/password/email', [
|
||||
'email' => $editor->email,
|
||||
]);
|
||||
Notification::assertTimesSent(1, ResetPassword::class);
|
||||
$resp->assertSee('A password reset link will be sent to ' . $editor->email . ' if that email address is found in the system.');
|
||||
}
|
||||
|
||||
public function test_login_redirects_to_initially_requested_url_correctly()
|
||||
{
|
||||
config()->set('app.url', 'http://localhost');
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Auth;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\Mfa\MfaValue;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\TestCase;
|
||||
@@ -164,4 +165,22 @@ class MfaConfigurationTest extends TestCase
|
||||
$this->assertActivityExists(ActivityType::MFA_REMOVE_METHOD);
|
||||
$this->assertEquals(0, $admin->mfaValues()->count());
|
||||
}
|
||||
|
||||
public function test_totp_setup_url_shows_correct_user_when_setup_forced_upon_login()
|
||||
{
|
||||
$admin = $this->getAdmin();
|
||||
/** @var Role $role */
|
||||
$role = $admin->roles()->first();
|
||||
$role->mfa_enforced = true;
|
||||
$role->save();
|
||||
|
||||
$resp = $this->post('/login', ['email' => $admin->email, 'password' => 'password']);
|
||||
$this->assertFalse(auth()->check());
|
||||
$resp->assertRedirect('/mfa/verify');
|
||||
|
||||
$resp = $this->get('/mfa/totp/generate');
|
||||
$resp->assertSeeText('Mobile App Setup');
|
||||
$resp->assertDontSee('otpauth://totp/BookStack:guest%40example.com');
|
||||
$resp->assertSee('otpauth://totp/BookStack:admin%40admin.com');
|
||||
}
|
||||
}
|
||||
|
||||
53
tests/DebugViewTest.php
Normal file
53
tests/DebugViewTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
|
||||
class DebugViewTest extends TestCase
|
||||
{
|
||||
public function test_debug_view_shows_expected_details()
|
||||
{
|
||||
config()->set('app.debug', true);
|
||||
$resp = $this->getDebugViewForException(new \InvalidArgumentException('An error occurred during testing'));
|
||||
|
||||
// Error message
|
||||
$resp->assertSeeText('An error occurred during testing');
|
||||
// Exception Class
|
||||
$resp->assertSeeText('InvalidArgumentException');
|
||||
// Stack trace
|
||||
$resp->assertSeeText('#0');
|
||||
$resp->assertSeeText('#1');
|
||||
// Warning message
|
||||
$resp->assertSeeText('WARNING: Application is in debug mode. This mode has the potential to leak');
|
||||
// PHP version
|
||||
$resp->assertSeeText('PHP Version: ' . phpversion());
|
||||
// BookStack version
|
||||
$resp->assertSeeText('BookStack Version: ' . trim(file_get_contents(base_path('version'))));
|
||||
// Dynamic help links
|
||||
$resp->assertElementExists('a[href*="q=' . urlencode('BookStack An error occurred during testing') . '"]');
|
||||
$resp->assertElementExists('a[href*="?q=is%3Aissue+' . urlencode('An error occurred during testing') . '"]');
|
||||
}
|
||||
|
||||
public function test_debug_view_only_shows_when_debug_mode_is_enabled()
|
||||
{
|
||||
config()->set('app.debug', true);
|
||||
$resp = $this->getDebugViewForException(new \InvalidArgumentException('An error occurred during testing'));
|
||||
$resp->assertSeeText('Stack Trace');
|
||||
$resp->assertDontSeeText('An unknown error occurred');
|
||||
|
||||
config()->set('app.debug', false);
|
||||
$resp = $this->getDebugViewForException(new \InvalidArgumentException('An error occurred during testing'));
|
||||
$resp->assertDontSeeText('Stack Trace');
|
||||
$resp->assertSeeText('An unknown error occurred');
|
||||
}
|
||||
|
||||
protected function getDebugViewForException(\Exception $exception): TestResponse
|
||||
{
|
||||
// Fake an error via social auth service used on login page
|
||||
$mockService = $this->mock(SocialAuthService::class);
|
||||
$mockService->shouldReceive('getActiveDrivers')->andThrow($exception);
|
||||
|
||||
return $this->get('/login');
|
||||
}
|
||||
}
|
||||
@@ -229,6 +229,34 @@ class ExportTest extends TestCase
|
||||
$resp->assertSee('src="/uploads/svg_test.svg"');
|
||||
}
|
||||
|
||||
public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local()
|
||||
{
|
||||
$contents = file_get_contents(public_path('.htaccess'));
|
||||
config()->set('filesystems.images', 'local');
|
||||
|
||||
$page = Page::query()->first();
|
||||
$page->html = '<img src="http://localhost/uploads/images/../../.htaccess"/>';
|
||||
$page->save();
|
||||
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
|
||||
$resp->assertDontSee(base64_encode($contents));
|
||||
}
|
||||
|
||||
public function test_page_export_contained_html_does_not_allow_upward_traversal_with_local_secure()
|
||||
{
|
||||
$testFilePath = storage_path('logs/test.txt');
|
||||
config()->set('filesystems.images', 'local_secure');
|
||||
file_put_contents($testFilePath, 'I am a cat');
|
||||
|
||||
$page = Page::query()->first();
|
||||
$page->html = '<img src="http://localhost/uploads/images/../../logs/test.txt"/>';
|
||||
$page->save();
|
||||
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
|
||||
$resp->assertDontSee(base64_encode('I am a cat'));
|
||||
unlink($testFilePath);
|
||||
}
|
||||
|
||||
public function test_exports_removes_scripts_from_custom_head()
|
||||
{
|
||||
$entities = [
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Entity;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -80,6 +81,63 @@ class PageDraftTest extends TestCase
|
||||
->assertElementNotContains('.notification', 'Admin has started editing this page');
|
||||
}
|
||||
|
||||
public function test_draft_save_shows_alert_if_draft_older_than_last_page_update()
|
||||
{
|
||||
$admin = $this->getAdmin();
|
||||
$editor = $this->getEditor();
|
||||
/** @var Page $page */
|
||||
$page = Page::query()->first();
|
||||
|
||||
$this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>updated draft</p>',
|
||||
]);
|
||||
|
||||
/** @var PageRevision $draft */
|
||||
$draft = $page->allRevisions()
|
||||
->where('type', '=', 'update_draft')
|
||||
->where('created_by', '=', $editor->id)
|
||||
->first();
|
||||
$draft->created_at = now()->subMinute(1);
|
||||
$draft->save();
|
||||
|
||||
$this->actingAs($admin)->put($page->refresh()->getUrl(), [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>admin update</p>',
|
||||
]);
|
||||
|
||||
$resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>updated draft again</p>',
|
||||
]);
|
||||
|
||||
$resp->assertJson([
|
||||
'warning' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_draft_save_shows_alert_if_draft_edit_started_by_someone_else()
|
||||
{
|
||||
$admin = $this->getAdmin();
|
||||
$editor = $this->getEditor();
|
||||
/** @var Page $page */
|
||||
$page = Page::query()->first();
|
||||
|
||||
$this->actingAs($admin)->put('/ajax/page/' . $page->id . '/save-draft', [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>updated draft</p>',
|
||||
]);
|
||||
|
||||
$resp = $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
|
||||
'name' => $page->name,
|
||||
'html' => '<p>updated draft again</p>',
|
||||
]);
|
||||
|
||||
$resp->assertJson([
|
||||
'warning' => 'Admin has started editing this page in the last 60 minutes. Take care not to overwrite each other\'s updates!',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_draft_pages_show_on_homepage()
|
||||
{
|
||||
/** @var Book $book */
|
||||
|
||||
@@ -119,6 +119,15 @@ class SecurityHeaderTest extends TestCase
|
||||
$this->assertEquals('base-uri \'self\'', $scriptHeader);
|
||||
}
|
||||
|
||||
public function test_cache_control_headers_are_strict_on_responses_when_logged_in()
|
||||
{
|
||||
$this->asEditor();
|
||||
$resp = $this->get('/');
|
||||
$resp->assertHeader('Cache-Control', 'max-age=0, no-store, private');
|
||||
$resp->assertHeader('Pragma', 'no-cache');
|
||||
$resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the first CSP header of the given type.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user