mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-11 19:06:22 +03:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a746e297 | ||
|
|
a4d43ee24b | ||
|
|
2acef3c2ec | ||
|
|
f7793a70a9 | ||
|
|
ceba3d31fb | ||
|
|
3f3fad7113 | ||
|
|
eecc08edde | ||
|
|
eb19aadc75 | ||
|
|
884664bfe9 | ||
|
|
8911e3f441 | ||
|
|
7d38c96a23 | ||
|
|
162d893143 | ||
|
|
06c81e69b9 | ||
|
|
3dc3d4a639 | ||
|
|
6d8b0605a0 | ||
|
|
349162ea13 | ||
|
|
bbd1384acb | ||
|
|
6aa2bf9e27 | ||
|
|
94c59c1e3d | ||
|
|
4d2205853a | ||
|
|
18bcafaee4 | ||
|
|
8d07b7cf1c | ||
|
|
080f9c3025 | ||
|
|
617fe6bc8c | ||
|
|
bb1f1a9ecd | ||
|
|
d688e43197 | ||
|
|
c82c3023c5 | ||
|
|
d0d75afc66 | ||
|
|
467176ee78 | ||
|
|
521a002001 | ||
|
|
aca37b8784 | ||
|
|
751772b87a | ||
|
|
76e30869e1 | ||
|
|
f3ee8f2d4c | ||
|
|
ea406690f5 | ||
|
|
465d405926 | ||
|
|
1097c61d6d | ||
|
|
def2d61ad8 | ||
|
|
8b0f5e7000 | ||
|
|
1e88e8086f | ||
|
|
d48ac0a37d | ||
|
|
3edc9fe9eb | ||
|
|
616c62703e | ||
|
|
3eeb1e7d08 | ||
|
|
0d43b50f9d | ||
|
|
6bcfac6751 | ||
|
|
68489e5b44 | ||
|
|
fe0e307313 | ||
|
|
9985046685 | ||
|
|
53ec794e53 | ||
|
|
328d2514c4 | ||
|
|
de2756dd95 | ||
|
|
1f97047799 | ||
|
|
c870c10e38 | ||
|
|
49fa21c1e2 | ||
|
|
9f87423584 | ||
|
|
08fbd39fcb | ||
|
|
5f75a9f32c | ||
|
|
3750922c3e | ||
|
|
4b0d1ddf39 | ||
|
|
2c3f453c1f | ||
|
|
7837b8c4ee |
14
.env.example
14
.env.example
@@ -12,11 +12,13 @@
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# Application URL
|
||||
# Remove the hash below and set a URL if using BookStack behind
|
||||
# a proxy or if using a third-party authentication option.
|
||||
# This must be the root URL that you want to host BookStack on.
|
||||
# All URL's in BookStack will be generated using this value.
|
||||
#APP_URL=https://example.com
|
||||
# All URLs in BookStack will be generated using this value
|
||||
# to ensure URLs generated are consistent and secure.
|
||||
# If you change this in the future you may need to run a command
|
||||
# to update stored URLs in the database. Command example:
|
||||
# php artisan bookstack:update-url https://old.example.com https://new.example.com
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
@@ -28,8 +30,8 @@ DB_PASSWORD=database_user_password
|
||||
# Can be 'smtp' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# Mail sender options
|
||||
MAIL_FROM_NAME=BookStack
|
||||
# Mail sender details
|
||||
MAIL_FROM_NAME="BookStack"
|
||||
MAIL_FROM=bookstack@example.com
|
||||
|
||||
# SMTP mail options
|
||||
|
||||
@@ -238,9 +238,9 @@ DISABLE_EXTERNAL_SERVICES=false
|
||||
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
|
||||
AVATAR_URL=
|
||||
|
||||
# Enable draw.io integration
|
||||
# Enable diagrams.net integration
|
||||
# Can simply be true/false to enable/disable the integration.
|
||||
# Alternatively, It can be URL to the draw.io instance you want to use.
|
||||
# Alternatively, It can be URL to the diagrams.net instance you want to use.
|
||||
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
|
||||
DRAWIO=true
|
||||
|
||||
|
||||
5
.github/translators.txt
vendored
5
.github/translators.txt
vendored
@@ -119,4 +119,7 @@ Simsimpicpic :: French
|
||||
Zenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal
|
||||
tatsuya.info :: Japanese
|
||||
fadiapp :: Arabic
|
||||
Jakub “Jéžiš” Bouček (jakubboucek) :: Czech
|
||||
Jakub Bouček (jakubboucek) :: Czech
|
||||
Marco (cdrfun) :: German
|
||||
10935336 :: Chinese Simplified
|
||||
孟繁阳 (FanyangMeng) :: Chinese Simplified
|
||||
|
||||
58
.github/workflows/test-migrations.yml
vendored
Normal file
58
.github/workflows/test-migrations.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: test-migrations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!l10n_master'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache composer packages
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ matrix.php }}
|
||||
|
||||
- name: Start MySQL
|
||||
run: |
|
||||
sudo /etc/init.d/mysql start
|
||||
|
||||
- name: Create database & user
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
- name: Install composer dependencies
|
||||
run: composer install --prefer-dist --no-interaction --ansi
|
||||
|
||||
- name: Start migration test
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||
|
||||
- name: Start migration:rollback test
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
|
||||
|
||||
- name: Start migration rerun test
|
||||
run: |
|
||||
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
|
||||
@@ -57,7 +57,7 @@ class RegistrationService
|
||||
// Ensure user does not already exist
|
||||
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
|
||||
if ($alreadyUser) {
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
|
||||
}
|
||||
|
||||
// Create the user
|
||||
|
||||
@@ -42,13 +42,6 @@ return [
|
||||
'root' => storage_path(),
|
||||
],
|
||||
|
||||
'ftp' => [
|
||||
'driver' => 'ftp',
|
||||
'host' => 'ftp.example.com',
|
||||
'username' => 'your-username',
|
||||
'password' => 'your-password',
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
||||
@@ -59,16 +52,6 @@ return [
|
||||
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
||||
],
|
||||
|
||||
'rackspace' => [
|
||||
'driver' => 'rackspace',
|
||||
'username' => 'your-username',
|
||||
'key' => 'your-key',
|
||||
'container' => 'your-container',
|
||||
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
|
||||
'region' => 'IAD',
|
||||
'url_type' => 'publicURL',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ use Illuminate\Support\Collection;
|
||||
/**
|
||||
* Class Chapter
|
||||
* @property Collection<Page> $pages
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class Chapter extends BookChild
|
||||
{
|
||||
@@ -52,15 +51,6 @@ class Chapter extends BookChild
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this chapter has any child pages.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren()
|
||||
{
|
||||
return count($this->pages) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible pages in this chapter.
|
||||
*/
|
||||
|
||||
@@ -201,12 +201,10 @@ class Entity extends Ownable
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Cleaner method for is_a.
|
||||
* @param $type
|
||||
* @return bool
|
||||
* Check if this instance or class is a certain type of entity.
|
||||
* Examples of $type are 'page', 'book', 'chapter'
|
||||
*/
|
||||
public static function isA($type)
|
||||
public static function isA(string $type): bool
|
||||
{
|
||||
return static::getType() === strtolower($type);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ class ExportService
|
||||
{
|
||||
$text = $chapter->name . "\n\n";
|
||||
$text .= $chapter->description . "\n\n";
|
||||
foreach ($chapter->pages as $page) {
|
||||
foreach ($chapter->getVisiblePages() as $page) {
|
||||
$text .= $this->pageToPlainText($page);
|
||||
}
|
||||
return $text;
|
||||
@@ -214,7 +214,7 @@ class ExportService
|
||||
*/
|
||||
public function bookToPlainText(Book $book): string
|
||||
{
|
||||
$bookTree = (new BookContents($book))->getTree(false, true);
|
||||
$bookTree = (new BookContents($book))->getTree(false, false);
|
||||
$text = $book->name . "\n\n";
|
||||
foreach ($bookTree as $bookChild) {
|
||||
if ($bookChild->isA('chapter')) {
|
||||
|
||||
@@ -41,7 +41,6 @@ class BookContents
|
||||
|
||||
/**
|
||||
* Get the contents as a sorted collection tree.
|
||||
* TODO - Support $renderPages option
|
||||
*/
|
||||
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
|
||||
{
|
||||
@@ -54,14 +53,22 @@ class BookContents
|
||||
$pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
|
||||
$chapter = $chapterMap->get($chapter_id);
|
||||
if ($chapter) {
|
||||
$chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
|
||||
$chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
|
||||
} else {
|
||||
$lonePages = $lonePages->concat($pages);
|
||||
}
|
||||
});
|
||||
|
||||
$all->each(function (Entity $entity) {
|
||||
$chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
|
||||
$chapter->setAttribute('visible_pages', collect([]));
|
||||
});
|
||||
|
||||
$all->each(function (Entity $entity) use ($renderPages) {
|
||||
$entity->setRelation('book', $this->book);
|
||||
|
||||
if ($renderPages && $entity->isA('page')) {
|
||||
$entity->html = (new PageContent($entity))->render();
|
||||
}
|
||||
});
|
||||
|
||||
return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use BookStack\Entities\Page;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
|
||||
@@ -44,18 +43,24 @@ class PageContent
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
// Set ids on top-level nodes
|
||||
$idMap = [];
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
$this->setUniqueId($childNode, $idMap);
|
||||
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
|
||||
if ($newId && $newId !== $oldId) {
|
||||
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure no duplicate ids within child items
|
||||
$xPath = new DOMXPath($doc);
|
||||
$idElems = $xPath->query('//body//*//*[@id]');
|
||||
foreach ($idElems as $domElem) {
|
||||
$this->setUniqueId($domElem, $idMap);
|
||||
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
|
||||
if ($newId && $newId !== $oldId) {
|
||||
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
@@ -67,23 +72,34 @@ class PageContent
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the all links to the $old location to instead point to $new.
|
||||
*/
|
||||
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
|
||||
{
|
||||
$old = str_replace('"', '', $old);
|
||||
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
|
||||
foreach ($matchingLinks as $domElem) {
|
||||
$domElem->setAttribute('href', $new);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a unique id on the given DOMElement.
|
||||
* A map for existing ID's should be passed in to check for current existence.
|
||||
* @param DOMElement $element
|
||||
* @param array $idMap
|
||||
* Returns a pair of strings in the format [old_id, new_id]
|
||||
*/
|
||||
protected function setUniqueId($element, array &$idMap)
|
||||
protected function setUniqueId(\DOMNode $element, array &$idMap): array
|
||||
{
|
||||
if (get_class($element) !== 'DOMElement') {
|
||||
return;
|
||||
return ['', ''];
|
||||
}
|
||||
|
||||
// Overwrite id if not a BookStack custom id
|
||||
// Stop if there's an existing valid id that has not already been used.
|
||||
$existingId = $element->getAttribute('id');
|
||||
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
||||
$idMap[$existingId] = true;
|
||||
return;
|
||||
return [$existingId, $existingId];
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
@@ -100,6 +116,7 @@ class PageContent
|
||||
|
||||
$element->setAttribute('id', $newId);
|
||||
$idMap[$newId] = true;
|
||||
return [$existingId, $newId];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +296,24 @@ class PageContent
|
||||
$scriptElem->parentNode->removeChild($scriptElem);
|
||||
}
|
||||
|
||||
// Remove clickable links to JavaScript URI
|
||||
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||
foreach ($badLinks as $badLink) {
|
||||
$badLink->parentNode->removeChild($badLink);
|
||||
}
|
||||
|
||||
// Remove forms with calls to JavaScript URI
|
||||
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||
foreach ($badForms as $badForm) {
|
||||
$badForm->parentNode->removeChild($badForm);
|
||||
}
|
||||
|
||||
// Remove meta tag to prevent external redirects
|
||||
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||
foreach ($metaTags as $metaTag) {
|
||||
$metaTag->parentNode->removeChild($metaTag);
|
||||
}
|
||||
|
||||
// Remove data or JavaScript iFrames
|
||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||
foreach ($badIframes as $badIframe) {
|
||||
|
||||
@@ -110,7 +110,7 @@ class AttachmentController extends Controller
|
||||
try {
|
||||
$this->validate($request, [
|
||||
'attachment_edit_name' => 'required|string|min:1|max:255',
|
||||
'attachment_edit_url' => 'string|min:1|max:255'
|
||||
'attachment_edit_url' => 'string|min:1|max:255|safe_url'
|
||||
]);
|
||||
} catch (ValidationException $exception) {
|
||||
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
|
||||
@@ -145,7 +145,7 @@ class AttachmentController extends Controller
|
||||
$this->validate($request, [
|
||||
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'attachment_link_name' => 'required|string|min:1|max:255',
|
||||
'attachment_link_url' => 'required|string|min:1|max:255'
|
||||
'attachment_link_url' => 'required|string|min:1|max:255|safe_url'
|
||||
]);
|
||||
} catch (ValidationException $exception) {
|
||||
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
|
||||
@@ -161,7 +161,7 @@ class AttachmentController extends Controller
|
||||
|
||||
$attachmentName = $request->get('attachment_link_name');
|
||||
$link = $request->get('attachment_link_url');
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
|
||||
return view('attachments.manager-link-form', [
|
||||
'pageId' => $pageId,
|
||||
|
||||
@@ -43,6 +43,13 @@ class AppServiceProvider extends ServiceProvider
|
||||
return substr_count($uploadName, '.') < 2;
|
||||
});
|
||||
|
||||
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
|
||||
$cleanLinkName = strtolower(trim($value));
|
||||
$isJs = strpos($cleanLinkName, 'javascript:') === 0;
|
||||
$isData = strpos($cleanLinkName, 'data:') === 0;
|
||||
return !$isJs && !$isData;
|
||||
});
|
||||
|
||||
// Custom blade view directives
|
||||
Blade::directive('icon', function ($expression) {
|
||||
return "<?php echo icon($expression); ?>";
|
||||
|
||||
@@ -2,17 +2,29 @@
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService extends UploadService
|
||||
class AttachmentService
|
||||
{
|
||||
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* AttachmentService constructor.
|
||||
*/
|
||||
public function __construct(FileSystem $fileSystem)
|
||||
{
|
||||
$this->fileSystem = $fileSystem;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing files.
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
*/
|
||||
protected function getStorage()
|
||||
protected function getStorage(): FileSystemInstance
|
||||
{
|
||||
$storageType = config('filesystems.attachments');
|
||||
|
||||
@@ -88,12 +100,8 @@ class AttachmentService extends UploadService
|
||||
|
||||
/**
|
||||
* Save a new File attachment from a given link and name.
|
||||
* @param string $name
|
||||
* @param string $link
|
||||
* @param int $page_id
|
||||
* @return Attachment
|
||||
*/
|
||||
public function saveNewFromLink($name, $link, $page_id)
|
||||
public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
|
||||
{
|
||||
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
|
||||
return Attachment::forceCreate([
|
||||
@@ -123,13 +131,11 @@ class AttachmentService extends UploadService
|
||||
|
||||
/**
|
||||
* Update the details of a file.
|
||||
* @param Attachment $attachment
|
||||
* @param $requestData
|
||||
* @return Attachment
|
||||
*/
|
||||
public function updateFile(Attachment $attachment, $requestData)
|
||||
public function updateFile(Attachment $attachment, array $requestData): Attachment
|
||||
{
|
||||
$attachment->name = $requestData['name'];
|
||||
|
||||
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
|
||||
$attachment->path = $requestData['link'];
|
||||
if (!$attachment->external) {
|
||||
@@ -137,6 +143,7 @@ class AttachmentService extends UploadService
|
||||
$attachment->external = true;
|
||||
}
|
||||
}
|
||||
|
||||
$attachment->save();
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ class ImageRepo
|
||||
if ($filterType === 'page') {
|
||||
$query->where('uploaded_to', '=', $contextPage->id);
|
||||
} elseif ($filterType === 'book') {
|
||||
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
|
||||
$validPageIds = $contextPage->book->pages()->visible()->get(['id'])->pluck('id')->toArray();
|
||||
$query->whereIn('uploaded_to', $validPageIds);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,16 +4,18 @@ use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use DB;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use phpDocumentor\Reflection\Types\Integer;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService extends UploadService
|
||||
class ImageService
|
||||
{
|
||||
|
||||
protected $imageTool;
|
||||
@@ -21,30 +23,24 @@ class ImageService extends UploadService
|
||||
protected $storageUrl;
|
||||
protected $image;
|
||||
protected $http;
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* ImageService constructor.
|
||||
* @param Image $image
|
||||
* @param ImageManager $imageTool
|
||||
* @param FileSystem $fileSystem
|
||||
* @param Cache $cache
|
||||
* @param HttpFetcher $http
|
||||
*/
|
||||
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageTool = $imageTool;
|
||||
$this->fileSystem = $fileSystem;
|
||||
$this->cache = $cache;
|
||||
$this->http = $http;
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @param string $type
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
*/
|
||||
protected function getStorage($type = '')
|
||||
protected function getStorage(string $type = ''): FileSystemInstance
|
||||
{
|
||||
$storageType = config('filesystems.images');
|
||||
|
||||
@@ -58,12 +54,6 @@ class ImageService extends UploadService
|
||||
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @param int|null $resizeWidth
|
||||
* @param int|null $resizeHeight
|
||||
* @param bool $keepRatio
|
||||
* @return mixed
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
@@ -107,10 +97,10 @@ class ImageService extends UploadService
|
||||
/**
|
||||
* Gets an image from url and saves it to the database.
|
||||
* @param $url
|
||||
* @param string $type
|
||||
* @param string $type
|
||||
* @param bool|string $imageName
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
* @throws Exception
|
||||
*/
|
||||
private function saveNewFromUrl($url, $type, $imageName = false)
|
||||
{
|
||||
@@ -118,7 +108,7 @@ class ImageService extends UploadService
|
||||
try {
|
||||
$imageData = $this->http->fetch($url);
|
||||
} catch (HttpFetchException $exception) {
|
||||
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
throw new Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
}
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
@@ -152,10 +142,10 @@ class ImageService extends UploadService
|
||||
}
|
||||
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type,
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
'url' => $this->getPublicUrl($fullPath),
|
||||
'type' => $type,
|
||||
'uploaded_to' => $uploadedTo
|
||||
];
|
||||
|
||||
@@ -185,15 +175,13 @@ class ImageService extends UploadService
|
||||
$name = Str::random(10);
|
||||
}
|
||||
|
||||
return $name . '.' . $extension;
|
||||
return $name . '.' . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the image is a gif. Returns true if it is, else false.
|
||||
* @param Image $image
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isGif(Image $image)
|
||||
protected function isGif(Image $image): bool
|
||||
{
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
@@ -253,7 +241,7 @@ class ImageService extends UploadService
|
||||
try {
|
||||
$thumb = $this->imageTool->make($imageData);
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
if ($e instanceof ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
}
|
||||
throw $e;
|
||||
@@ -281,11 +269,9 @@ class ImageService extends UploadService
|
||||
|
||||
/**
|
||||
* Get the raw data content from an image.
|
||||
* @param Image $image
|
||||
* @return string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getImageData(Image $image)
|
||||
public function getImageData(Image $image): string
|
||||
{
|
||||
$imagePath = $image->path;
|
||||
$storage = $this->getStorage();
|
||||
@@ -294,7 +280,6 @@ class ImageService extends UploadService
|
||||
|
||||
/**
|
||||
* Destroy an image along with its revisions, thumbnails and remaining folders.
|
||||
* @param Image $image
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(Image $image)
|
||||
@@ -324,7 +309,7 @@ class ImageService extends UploadService
|
||||
// Cleanup of empty folders
|
||||
$foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
|
||||
foreach ($foldersInvolved as $directory) {
|
||||
if ($this->isFolderEmpty($directory)) {
|
||||
if ($this->isFolderEmpty($storage, $directory)) {
|
||||
$storage->deleteDirectory($directory);
|
||||
}
|
||||
}
|
||||
@@ -332,14 +317,21 @@ class ImageService extends UploadService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
*/
|
||||
protected function isFolderEmpty(FileSystemInstance $storage, string $path): bool
|
||||
{
|
||||
$files = $storage->files($path);
|
||||
$folders = $storage->directories($path);
|
||||
return (count($files) === 0 && count($folders) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an avatar image from an external service.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $size
|
||||
* @return Image
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveUserAvatar(User $user, $size = 500)
|
||||
public function saveUserAvatar(User $user, int $size = 500): Image
|
||||
{
|
||||
$avatarUrl = $this->getAvatarUrl();
|
||||
$email = strtolower(trim($user->email));
|
||||
@@ -363,9 +355,8 @@ class ImageService extends UploadService
|
||||
|
||||
/**
|
||||
* Check if fetching external avatars is enabled.
|
||||
* @return bool
|
||||
*/
|
||||
public function avatarFetchEnabled()
|
||||
public function avatarFetchEnabled(): bool
|
||||
{
|
||||
$fetchUrl = $this->getAvatarUrl();
|
||||
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
|
||||
@@ -407,11 +398,11 @@ class ImageService extends UploadService
|
||||
foreach ($images as $image) {
|
||||
$searchQuery = '%' . basename($image->path) . '%';
|
||||
$inPage = DB::table('pages')
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
$inRevision = false;
|
||||
if ($checkRevisions) {
|
||||
$inRevision = DB::table('page_revisions')
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
$inRevision = DB::table('page_revisions')
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
}
|
||||
|
||||
if (!$inPage && !$inRevision) {
|
||||
@@ -427,38 +418,25 @@ class ImageService extends UploadService
|
||||
|
||||
/**
|
||||
* Convert a image URI to a Base64 encoded string.
|
||||
* Attempts to find locally via set storage method first.
|
||||
* @param string $uri
|
||||
* @return null|string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
* 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.
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function imageUriToBase64(string $uri)
|
||||
public function imageUriToBase64(string $uri): ?string
|
||||
{
|
||||
$isLocal = strpos(trim($uri), 'http') !== 0;
|
||||
|
||||
// Attempt to find local files even if url not absolute
|
||||
$base = url('/');
|
||||
if (!$isLocal && strpos($uri, $base) === 0) {
|
||||
$isLocal = true;
|
||||
$uri = str_replace($base, '', $uri);
|
||||
$storagePath = $this->imageUrlToStoragePath($uri);
|
||||
if (empty($uri) || is_null($storagePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$storage = $this->getStorage();
|
||||
$imageData = null;
|
||||
|
||||
if ($isLocal) {
|
||||
$uri = trim($uri, '/');
|
||||
$storage = $this->getStorage();
|
||||
if ($storage->exists($uri)) {
|
||||
$imageData = $storage->get($uri);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$imageData = $this->http->fetch($uri);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
if ($storage->exists($storagePath)) {
|
||||
$imageData = $storage->get($storagePath);
|
||||
}
|
||||
|
||||
if ($imageData === null) {
|
||||
if (is_null($imageData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -471,11 +449,44 @@ class ImageService extends UploadService
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a public facing url for an image by checking relevant environment variables.
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
* Get a storage path for the given image URL.
|
||||
* Ensures the path will start with "uploads/images".
|
||||
* Returns null if the url cannot be resolved to a local URL.
|
||||
*/
|
||||
private function getPublicUrl($filePath)
|
||||
private function imageUrlToStoragePath(string $url): ?string
|
||||
{
|
||||
$url = ltrim(trim($url), '/');
|
||||
|
||||
// Handle potential relative paths
|
||||
$isRelative = strpos($url, 'http') !== 0;
|
||||
if ($isRelative) {
|
||||
if (strpos(strtolower($url), 'uploads/images') === 0) {
|
||||
return trim($url, '/');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle local images based on paths on the same domain
|
||||
$potentialHostPaths = [
|
||||
url('uploads/images/'),
|
||||
$this->getPublicUrl('/uploads/images/'),
|
||||
];
|
||||
|
||||
foreach ($potentialHostPaths as $potentialBasePath) {
|
||||
$potentialBasePath = strtolower($potentialBasePath);
|
||||
if (strpos(strtolower($url), $potentialBasePath) === 0) {
|
||||
return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a public facing url for an image by checking relevant environment variables.
|
||||
* If s3-style store is in use it will default to guessing a public bucket URL.
|
||||
*/
|
||||
private function getPublicUrl(string $filePath): string
|
||||
{
|
||||
if ($this->storageUrl === null) {
|
||||
$storageUrl = config('filesystems.url');
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
|
||||
abstract class UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* @var FileSystem
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
|
||||
/**
|
||||
* FileService constructor.
|
||||
* @param $fileSystem
|
||||
*/
|
||||
public function __construct(FileSystem $fileSystem)
|
||||
{
|
||||
$this->fileSystem = $fileSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @return FileSystemInstance
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
$storageType = config('filesystems.default');
|
||||
return $this->fileSystem->disk($storageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
* @return bool
|
||||
*/
|
||||
protected function isFolderEmpty($path)
|
||||
{
|
||||
$files = $this->getStorage()->files($path);
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return (count($files) === 0 && count($folders) === 0);
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ class AddActivityIndexes extends Migration
|
||||
public function down()
|
||||
{
|
||||
Schema::table('activities', function(Blueprint $table) {
|
||||
$table->dropIndex('key');
|
||||
$table->dropIndex('created_at');
|
||||
$table->dropIndex('activities_key_index');
|
||||
$table->dropIndex('activities_created_at_index');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ set -e
|
||||
npm install
|
||||
npm rebuild node-sass
|
||||
|
||||
exec npm run watch
|
||||
SHELL=/bin/sh exec npm run watch
|
||||
|
||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -139,9 +139,9 @@
|
||||
}
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.57.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz",
|
||||
"integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg=="
|
||||
"version": "5.58.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.1.tgz",
|
||||
"integrity": "sha512-UGb/ueu20U4xqWk8hZB3xIfV2/SFqnSLYONiM3wTMDqko0bsYrsAkGGhqUzbRkYm89aBKPyHtuNEbVWF9FTFzw=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -253,9 +253,9 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.6.30",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.6.30.tgz",
|
||||
"integrity": "sha512-ZSZY461UPzTYYC3rqy1QiMtngk2WyXf+58MgC7tC22jkI90FXNgEl0hN3ipfn/UgZYzTW2GBcHiO7t0rSbHT7g==",
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
|
||||
"integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
@@ -496,9 +496,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.0.tgz",
|
||||
"integrity": "sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
|
||||
"integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
@@ -730,9 +730,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.26.10",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz",
|
||||
"integrity": "sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw==",
|
||||
"version": "1.26.11",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
|
||||
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
@@ -777,9 +777,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
|
||||
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.12.0.tgz",
|
||||
"integrity": "sha512-bPn57rCjBRlt2sC24RBsu40wZsmLkSo2XeqG8k6DC1zru5eObQUIPPZAQG7W2SJ8FZQYq+BEJmvuw1Zxb3chqg=="
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.1.1",
|
||||
|
||||
16
package.json
16
package.json
@@ -4,9 +4,9 @@
|
||||
"build:css:dev": "sass ./resources/sass:./public/dist",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
|
||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2020",
|
||||
"build:js:watch": "chokidar \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --minify",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
@@ -16,18 +16,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^2.1.0",
|
||||
"esbuild": "0.6.30",
|
||||
"esbuild": "0.7.8",
|
||||
"livereload": "^0.9.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.26.10"
|
||||
"sass": "^1.26.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.6",
|
||||
"codemirror": "^5.57.0",
|
||||
"codemirror": "^5.58.1",
|
||||
"dropzone": "^5.7.2",
|
||||
"markdown-it": "^11.0.0",
|
||||
"markdown-it": "^11.0.1",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.10.2"
|
||||
"sortablejs": "^1.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)/$ /$1 [L,R=301]
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Handle Front Controller...
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
|
||||
90
public/dist/app.js
vendored
90
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/export-styles.css
vendored
2
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles.css
vendored
2
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -168,6 +168,6 @@ These are the great open-source projects used to help build BookStack:
|
||||
* [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
|
||||
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
|
||||
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
|
||||
* [Draw.io](https://github.com/jgraph/drawio)
|
||||
* [diagrams.net](https://github.com/jgraph/drawio)
|
||||
* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
|
||||
* [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Sortable, MultiDrag} from "sortablejs";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
// Auto sort control
|
||||
const sortOperations = {
|
||||
@@ -43,7 +43,6 @@ class BookSort {
|
||||
this.input = elem.querySelector('[book-sort-input]');
|
||||
|
||||
const initialSortBox = elem.querySelector('.sort-box');
|
||||
Sortable.mount(new MultiDrag());
|
||||
this.setupBookSortable(initialSortBox);
|
||||
this.setupSortPresets();
|
||||
|
||||
|
||||
@@ -440,10 +440,10 @@ class MarkdownEditor {
|
||||
|
||||
const data = {
|
||||
image: pngData,
|
||||
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
|
||||
uploaded_to: Number(this.pageId),
|
||||
};
|
||||
|
||||
window.$http.post(window.baseUrl('/images/drawio'), data).then(resp => {
|
||||
window.$http.post("/images/drawio", data).then(resp => {
|
||||
this.insertDrawing(resp.data, cursorPos);
|
||||
DrawIO.close();
|
||||
}).catch(err => {
|
||||
@@ -476,10 +476,10 @@ class MarkdownEditor {
|
||||
|
||||
let data = {
|
||||
image: pngData,
|
||||
uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id'))
|
||||
uploaded_to: Number(this.pageId),
|
||||
};
|
||||
|
||||
window.$http.post(window.baseUrl(`/images/drawio`), data).then(resp => {
|
||||
window.$http.post("/images/drawio", data).then(resp => {
|
||||
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
|
||||
let newContent = this.cm.getValue().split('\n').map(line => {
|
||||
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'codemirror/mode/rust/rust';
|
||||
import 'codemirror/mode/shell/shell';
|
||||
import 'codemirror/mode/sql/sql';
|
||||
import 'codemirror/mode/toml/toml';
|
||||
import 'codemirror/mode/vbscript/vbscript';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
|
||||
@@ -84,6 +85,8 @@ const modeMap = {
|
||||
bash: 'shell',
|
||||
toml: 'toml',
|
||||
sql: 'text/x-sql',
|
||||
vbs: 'vbscript',
|
||||
vbscript: 'vbscript',
|
||||
xml: 'xml',
|
||||
yaml: 'yaml',
|
||||
yml: 'yaml',
|
||||
|
||||
@@ -141,10 +141,14 @@ async function request(url, options = {}) {
|
||||
/**
|
||||
* Get the content from a fetch response.
|
||||
* Checks the content-type header to determine the format.
|
||||
* @param response
|
||||
* @param {Response} response
|
||||
* @returns {Promise<Object|String>}
|
||||
*/
|
||||
async function getResponseContent(response) {
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const responseContentType = response.headers.get('Content-Type');
|
||||
const subType = responseContentType.split('/').pop();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ return [
|
||||
'username' => 'Uživatelské jméno',
|
||||
'email' => 'E-mail',
|
||||
'password' => 'Heslo',
|
||||
'password_confirm' => 'Oveření hesla',
|
||||
'password_confirm' => 'Potvrdit heslo',
|
||||
'password_hint' => 'Musí mít více než 7 znaků',
|
||||
'forgot_password' => 'Zapomněli jste heslo?',
|
||||
'remember_me' => 'Zapamatovat si mě',
|
||||
@@ -74,4 +74,4 @@ return [
|
||||
'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při budoucích návštěvách.',
|
||||
'user_invite_page_confirm_button' => 'Potvrdit heslo',
|
||||
'user_invite_success' => 'Heslo nastaveno, nyní máte přístup k :appName!'
|
||||
];
|
||||
];
|
||||
@@ -33,7 +33,7 @@ return [
|
||||
'copy' => 'Kopírovat',
|
||||
'reply' => 'Odpovědět',
|
||||
'delete' => 'Odstranit',
|
||||
'delete_confirm' => 'Confirm Deletion',
|
||||
'delete_confirm' => 'Potvrdit odstranění',
|
||||
'search' => 'Hledat',
|
||||
'search_clear' => 'Vymazat hledání',
|
||||
'reset' => 'Obnovit',
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'image_load_more' => 'Načíst další',
|
||||
'image_image_name' => 'Název obrázku',
|
||||
'image_delete_used' => 'Tento obrázek je použit na níže uvedených stránkách.',
|
||||
'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
|
||||
'image_delete_confirm_text' => 'Opravdu chcete odstranit tento obrázek?',
|
||||
'image_select_image' => 'Vyberte obrázek',
|
||||
'image_dropzone' => 'Přetáhněte obrázky nebo klikněte sem pro nahrání',
|
||||
'images_deleted' => 'Obrázky odstraněny',
|
||||
|
||||
@@ -146,7 +146,7 @@ return [
|
||||
'chapters_create' => 'Vytvořit novou kapitolu',
|
||||
'chapters_delete' => 'Smazat kapitolu',
|
||||
'chapters_delete_named' => 'Smazat kapitolu :chapterName',
|
||||
'chapters_delete_explain' => "Kapitola ':chapterName' bude smazána. Všechny stránky v ní obsažené budou přesunuty přímo pod samotnou knihu.",
|
||||
'chapters_delete_explain' => 'Kapitola \':chapterName\' bude smazána. Všechny stránky v ní obsažené budou přesunuty přímo pod samotnou knihu.',
|
||||
'chapters_delete_confirm' => 'Opravdu chcete tuto kapitolu smazat?',
|
||||
'chapters_edit' => 'Upravit kapitolu',
|
||||
'chapters_edit_named' => 'Upravit kapitolu :chapterName',
|
||||
@@ -233,7 +233,7 @@ return [
|
||||
'time_b' => 'v posledních minutách (:minCount min.)',
|
||||
'message' => ':start :time. Dávejte pozor abyste nepřepsali změny ostatním!',
|
||||
],
|
||||
'pages_draft_discarded' => 'Koncept byl zahozen. Editor nyní obsahuje aktuální verzi stránky.',
|
||||
'pages_draft_discarded' => 'Koncept zahozen. Editor nyní obsahuje aktuální verzi stránky.',
|
||||
'pages_specific' => 'Konkrétní stránka',
|
||||
'pages_is_template' => 'Šablona stránky',
|
||||
|
||||
@@ -256,16 +256,16 @@ return [
|
||||
'attachments_upload' => 'Nahrát soubor',
|
||||
'attachments_link' => 'Připojit odkaz',
|
||||
'attachments_set_link' => 'Nastavit odkaz',
|
||||
'attachments_delete' => 'Are you sure you want to delete this attachment?',
|
||||
'attachments_delete' => 'Jste si jisti, že chcete odstranit tuto přílohu?',
|
||||
'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem kliknětě pro vybrání souboru.',
|
||||
'attachments_no_files' => 'Žádné soubory nebyli nahrány',
|
||||
'attachments_explain_link' => 'Můžete pouze připojit odkaz, pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.',
|
||||
'attachments_explain_link' => 'Můžete pouze připojit odkaz pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.',
|
||||
'attachments_link_name' => 'Název odkazu',
|
||||
'attachment_link' => 'Odkaz na přílohu',
|
||||
'attachments_link_url' => 'Odkaz na soubor',
|
||||
'attachments_link_url_hint' => 'URL stránky nebo souboru',
|
||||
'attach' => 'Připojit',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => 'Přidat odkaz na přílohu do stránky',
|
||||
'attachments_edit_file' => 'Upravit soubor',
|
||||
'attachments_edit_file_name' => 'Název souboru',
|
||||
'attachments_edit_drop_upload' => 'Přetáhněte sem soubor myší nebo klikněte pro nahrání nového a následné přepsání starého.',
|
||||
@@ -294,7 +294,7 @@ return [
|
||||
'comment' => 'Komentář',
|
||||
'comments' => 'Komentáře',
|
||||
'comment_add' => 'Přidat komentář',
|
||||
'comment_placeholder' => 'Zanechte komentář zde',
|
||||
'comment_placeholder' => 'Zanechat komentář zde',
|
||||
'comment_count' => '{0} Bez komentářů|{1} 1 komentář|[2,4] :count komentáře|[5,*] :count komentářů',
|
||||
'comment_save' => 'Uložit komentář',
|
||||
'comment_saving' => 'Ukládání komentáře...',
|
||||
@@ -313,4 +313,4 @@ return [
|
||||
'revision_restore_confirm' => 'Jste si jisti, že chcete obnovit tuto revizi? Aktuální obsah stránky bude nahrazen.',
|
||||
'revision_delete_success' => 'Revize smazána',
|
||||
'revision_cannot_delete_latest' => 'Nelze smazat poslední revizi.'
|
||||
];
|
||||
];
|
||||
@@ -9,8 +9,8 @@ return [
|
||||
'permissionJson' => 'Nemáte povolení k provedení požadované akce.',
|
||||
|
||||
// Auth
|
||||
'error_user_exists_different_creds' => 'Uživatel s e-mailem :email již existuje ale s jinými přihlašovacími údaji.',
|
||||
'email_already_confirmed' => 'E-mailová adresa již byla potvrzena. Zkuste se přihlásit.',
|
||||
'error_user_exists_different_creds' => 'Uživatel s emailem :email již existuje ale s jinými přihlašovacími údaji.',
|
||||
'email_already_confirmed' => 'Emailová adresa již byla potvrzena. Zkuste se přihlásit.',
|
||||
'email_confirmation_invalid' => 'Tento potvrzovací odkaz již neplatí nebo už byl použit. Zkuste prosím registraci znovu.',
|
||||
'email_confirmation_expired' => 'Potvrzovací odkaz už neplatí, email s novým odkazem už byl poslán.',
|
||||
'email_confirmation_awaiting' => 'E-mailová adresa pro používaný účet musí být potvrzena',
|
||||
@@ -20,20 +20,20 @@ return [
|
||||
'ldap_cannot_connect' => 'Nelze se připojit k adresáři LDAP. Prvotní připojení selhalo.',
|
||||
'saml_already_logged_in' => 'Již jste přihlášeni',
|
||||
'saml_user_not_registered' => 'Uživatel :name není registrován a automatická registrace je zakázána',
|
||||
'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
||||
'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',
|
||||
'saml_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',
|
||||
'saml_invalid_response_id' => 'Požadavek z externího ověřovacího systému nebyl rozpoznám procesem, který tato aplikace spustila. Tento problém může způsobit stisknutí tlačítka Zpět po přihlášení.',
|
||||
'saml_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',
|
||||
'social_no_action_defined' => 'Nebyla zvolena žádá akce',
|
||||
'social_login_bad_response' => "Nastala chyba během přihlašování přes :socialAccount \n:error",
|
||||
'social_account_in_use' => 'Tento účet na :socialAccount se již používá. Pokuste se s ním přihlásit volbou Přihlásit přes :socialAccount.',
|
||||
'social_account_email_in_use' => 'E-mailová adresa :email se již používá. Pokud máte již máte náš účet, můžete si jej propojit se svým účtem na :socialAccount v nastavení vašeho profilu.',
|
||||
'social_account_email_in_use' => 'Emailová adresa :email se již používá. Pokud máte již máte náš účet, můžete si jej propojit se svým účtem na :socialAccount v nastavení vašeho profilu.',
|
||||
'social_account_existing' => 'Tento účet na :socialAccount je již propojen s vaším profilem zde.',
|
||||
'social_account_already_used_existing' => 'Tento účet na :socialAccount je již používán jiným uživatelem.',
|
||||
'social_account_not_used' => 'Tento účet na :socialAccount není spřažen s žádným uživatelem. Prosím přiřaďtě si jej v nastavení svého profilu.',
|
||||
'social_account_register_instructions' => 'Pokud ještě nemáte náš účet, můžete se zaregistrovat pomocí vašeho účtu na :socialAccount.',
|
||||
'social_driver_not_found' => 'Doplněk pro tohoto správce identity nebyl nalezen.',
|
||||
'social_driver_not_configured' => 'Nastavení vašeho účtu na :socialAccount není správné. :socialAccount musí mít vaše svolení pro naší aplikaci vás přihlásit.',
|
||||
'invite_token_expired' => 'Odkaz v pozvánce již bohužel expiroval. Namísto toho ale můžete zkusit resetovat heslo do Vašeho účtu.',
|
||||
'invite_token_expired' => 'Odkaz v pozvánce již bohužel vypršel. Namísto toho ale můžete zkusit resetovat heslo do Vašeho účtu.',
|
||||
|
||||
// System
|
||||
'path_not_writable' => 'Nelze zapisovat na cestu k souboru :filePath. Zajistěte aby se dalo nahrávat na server.',
|
||||
@@ -60,11 +60,11 @@ return [
|
||||
'chapter_not_found' => 'Kapitola nenalezena',
|
||||
'selected_book_not_found' => 'Vybraná kniha nebyla nalezena',
|
||||
'selected_book_chapter_not_found' => 'Zvolená kniha nebo kapitola nebyla nalezena',
|
||||
'guests_cannot_save_drafts' => 'Nepřihlášení návštěvníci nemohou ukládat koncepty.',
|
||||
'guests_cannot_save_drafts' => 'Návštěvníci z řad veřejnosti nemohou ukládat koncepty.',
|
||||
|
||||
// Users
|
||||
'users_cannot_delete_only_admin' => 'Nemůžete smazat posledního administrátora',
|
||||
'users_cannot_delete_guest' => 'Uživatele Guest není možno smazat',
|
||||
'users_cannot_delete_guest' => 'Uživatele host není možno smazat',
|
||||
|
||||
// Roles
|
||||
'role_cannot_be_edited' => 'Tuto roli nelze editovat',
|
||||
@@ -73,16 +73,16 @@ return [
|
||||
'role_cannot_remove_only_admin' => 'Tento uživatel má roli administrátora. Přiřaďte roli administrátora někomu jinému než jí odeberete zde.',
|
||||
|
||||
// Comments
|
||||
'comment_list' => 'Při načítání komentářů nastala chyba.',
|
||||
'comment_list' => 'Při dotahování komentářů nastala chyba.',
|
||||
'cannot_add_comment_to_draft' => 'Nemůžete přidávat komentáře ke konceptu.',
|
||||
'comment_add' => 'Při přidávání / úpravě komentáře nastala chyba.',
|
||||
'comment_add' => 'Při přidávání / aktualizaci komentáře nastala chyba.',
|
||||
'comment_delete' => 'Při mazání komentáře nastala chyba.',
|
||||
'empty_comment' => 'Nemůžete přidat prázdný komentář.',
|
||||
|
||||
// Error pages
|
||||
'404_page_not_found' => 'Stránka nenalezena',
|
||||
'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte, nebyla nalezena.',
|
||||
'sorry_page_not_found_permission_warning' => 'Pokud myslíte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.',
|
||||
'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte nebyla nalezena.',
|
||||
'sorry_page_not_found_permission_warning' => 'Pokud očekáváte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.',
|
||||
'return_home' => 'Návrat domů',
|
||||
'error_occurred' => 'Nastala chyba',
|
||||
'app_down' => ':appName je momentálně vypnutá',
|
||||
@@ -91,7 +91,7 @@ return [
|
||||
// API errors
|
||||
'api_no_authorization_found' => 'V požadavku nebyla nalezen žádný autorizační token',
|
||||
'api_bad_authorization_format' => 'V požadavku byl nalezen autorizační token, ale jeho formát se zdá být chybný',
|
||||
'api_user_token_not_found' => 'Pro poskytnutý autorizační token nebyl nalezen žádný odpovídající API token',
|
||||
'api_user_token_not_found' => 'Pro zadaný autorizační token nebyl nalezen žádný odpovídající API token',
|
||||
'api_incorrect_token_secret' => 'Poskytnutý Token Secret neodpovídá použitému API tokenu',
|
||||
'api_user_no_api_permission' => 'Vlastník použitého API tokenu nemá oprávnění provádět API volání',
|
||||
'api_user_token_expired' => 'Platnost autorizačního tokenu vypršela',
|
||||
|
||||
@@ -9,67 +9,67 @@ return [
|
||||
// Common Messages
|
||||
'settings' => 'Nastavení',
|
||||
'settings_save' => 'Uložit nastavení',
|
||||
'settings_save_success' => 'Nastavení bylo uloženo',
|
||||
'settings_save_success' => 'Nastavení uloženo',
|
||||
|
||||
// App Settings
|
||||
'app_customization' => 'Přizpůsobení',
|
||||
'app_features_security' => 'Funkce a zabezpečení',
|
||||
'app_name' => 'Název aplikace',
|
||||
'app_name_desc' => 'Název se bude zobrazovat v záhlaví této aplikace a v odesílaných e-mailech.',
|
||||
'app_name_header' => 'Zobrazovát název aplikace v záhlaví?',
|
||||
'app_name_desc' => 'Název se bude zobrazovat v záhlaví této aplikace a v e-mailech odesílaných systémem.',
|
||||
'app_name_header' => 'Zobrazovat název aplikace v záhlaví',
|
||||
'app_public_access' => 'Veřejný přístup',
|
||||
'app_public_access_desc' => 'Povolení této volby umožní návštěvníkům, kteří nejsou přihlášeni, přístup k obsahu v instanci BookStack.',
|
||||
'app_public_access_desc_guest' => 'Přístup veřejnosti je možné kontrolovat prostřednictvím uživatele "Guest".',
|
||||
'app_public_access_desc' => 'Zapnutím této volby umožníte nepřihlášeným návštěvníkům přístup k Vašemu obsahu v BookStack aplikaci.',
|
||||
'app_public_access_desc_guest' => 'Přístup pro nepřihlášené návštěvníky je možné nastavit přes uživatele "Guest".',
|
||||
'app_public_access_toggle' => 'Povolit veřejný přístup',
|
||||
'app_public_viewing' => 'Povolit prohlížení veřejností?',
|
||||
'app_secure_images' => 'Povolit vyšší zabezpečení obrázků ?',
|
||||
'app_secure_images_toggle' => 'Povolit vyšší zabezpečení obrázků',
|
||||
'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že není povoleno indexování adresářů, abyste zamezili snadnému přístupu.',
|
||||
'app_secure_images' => 'Nahrávat obrázky neveřejně a zabezpečeně',
|
||||
'app_secure_images_toggle' => 'Zapnout bezpečnější nahrávání obrázků',
|
||||
'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k přístup opět otevřelo.',
|
||||
'app_editor' => 'Editor stránek',
|
||||
'app_editor_desc' => 'Zvolte který editor budou užívat všichni uživatelé k úpravě stránek.',
|
||||
'app_custom_html' => 'Vlastní HTML kód pro sekci hlavičky (<head>).',
|
||||
'app_custom_html' => 'Vlastní obsah hlavičky HTML',
|
||||
'app_custom_html_desc' => 'Cokoliv sem napíšete bude přidáno na konec sekce <head> v každém místě této aplikace. To se hodí pro přidávání nebo změnu CSS stylů nebo přidání kódu pro analýzu používání (např.: google analytics.).',
|
||||
'app_custom_html_disabled_notice' => 'Vlastní HTML hlavička je na této stránce nastavení zakázána, aby bylo možné vrátit změny zpět.',
|
||||
'app_custom_html_disabled_notice' => 'Na této stránce nastavení je zakázán vlastní obsah HTML hlavičky, aby bylo zajištěno, že bude možné vrátit případnou problematickou úpravu.',
|
||||
'app_logo' => 'Logo aplikace',
|
||||
'app_logo_desc' => 'Obrázek by měl mít 43 pixelů na výšku. <br>Větší obrázky zmenšíme na tuto velikost.',
|
||||
'app_logo_desc' => 'Tento obrázek by měl mít výšku 43px. <br>Větší obrázky zmenšíme na tuto velikost.',
|
||||
'app_primary_color' => 'Hlavní barva aplikace',
|
||||
'app_primary_color_desc' => 'Zápis by měl být hexa (#aabbcc). <br>Pro základní barvu nechte pole prázdné.',
|
||||
'app_primary_color_desc' => 'Nastaví hlavní barvu aplikace včetně panelů, tlačítek a odkazů.',
|
||||
'app_homepage' => 'Úvodní stránka aplikace',
|
||||
'app_homepage_desc' => 'Zvolte pohled, který se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.',
|
||||
'app_homepage_desc' => 'Vyberte si zobrazení, které se použije jako úvodní stránka. U zvolených stránek bude ignorováno jejich oprávnění.',
|
||||
'app_homepage_select' => 'Zvolte stránku',
|
||||
'app_disable_comments' => 'Zakázání komentářů',
|
||||
'app_disable_comments_toggle' => 'Zakázat komentáře',
|
||||
'app_disable_comments_desc' => 'Zakáže komentáře napříč všemi stránkami. <br> Existující komentáře se přestanou zobrazovat.',
|
||||
'app_disable_comments' => 'Vypnutí komentářů',
|
||||
'app_disable_comments_toggle' => 'Vypnout komentáře',
|
||||
'app_disable_comments_desc' => 'Vypne komentáře napříč všemi stránkami. <br> Existující komentáře se přestanou zobrazovat.',
|
||||
|
||||
// Color settings
|
||||
'content_colors' => 'Barvy obsahu',
|
||||
'content_colors_desc' => 'Nastaví barvy pro všechny prvky v hierarchii organizace stránek. Pro čitelnost je doporučeno zvolit barvy s podobným jasem jako výchozí barvy.',
|
||||
'bookshelf_color' => 'Barva Knihovny',
|
||||
'book_color' => 'Barva Knihy',
|
||||
'chapter_color' => 'Barva Kapitoly',
|
||||
'page_color' => 'Barva Stránky',
|
||||
'page_draft_color' => 'Page Draft Color',
|
||||
'content_colors_desc' => 'Nastaví barvy pro všechny prvky v organizační struktuře stránky. Pro lepší čitelnost doporučujeme zvolit barvy s podobným jasem, jakou mají výchozí barvy.',
|
||||
'bookshelf_color' => 'Barva knihovny',
|
||||
'book_color' => 'Barva knihy',
|
||||
'chapter_color' => 'Barva kapitoly',
|
||||
'page_color' => 'Barva stránky',
|
||||
'page_draft_color' => 'Barva návrhu stránky',
|
||||
|
||||
// Registration Settings
|
||||
'reg_settings' => 'Nastavení registrace',
|
||||
'reg_enable' => 'Povolit Registrace',
|
||||
'reg_enable' => 'Povolení registrace',
|
||||
'reg_enable_toggle' => 'Povolit registrace',
|
||||
'reg_enable_desc' => 'Při povolení registrace se budou moct tito uživatelé přihlásit a obdrží výchozí uživatelskou roli.',
|
||||
'reg_enable_desc' => 'Pokud jsou povoleny registrace, bude se uživatel moci sám registrovat jako uživatel aplikace. Po registraci dostane jednu výchozí uživatelskou roli.',
|
||||
'reg_default_role' => 'Role přiřazená po registraci',
|
||||
'reg_enable_external_warning' => 'Výše uvedená volba je ignorována, pokud je aktivní externí LDAP nebo SAML ověření. Uživatelské účty pro neexistující členy budou automaticky vytvořeny po přihlášení přes externí autentifikační systém.',
|
||||
'reg_email_confirmation' => 'Potvrzení e-mailem',
|
||||
'reg_email_confirmation_toggle' => 'Vyžadovat potvrzení e-mailem',
|
||||
'reg_confirm_email_desc' => 'Pokud zapnete omezení emailové domény, tak bude ověřování emailové adresy vyžadováno vždy.',
|
||||
'reg_enable_external_warning' => 'Pokud je povolené externí ověřování přes LDAP nebo SAML, je výše uvedená možnost ignorována. Uživatelský účet budou automaticky vytvořen i neexistujícímu uživateli, jakmile se úspěšně přihlásí přes použitý externí přihlašovací systém.',
|
||||
'reg_email_confirmation' => 'Ověření e-mailu',
|
||||
'reg_email_confirmation_toggle' => 'Vyžadovat ověření e-mailu',
|
||||
'reg_confirm_email_desc' => 'Pokud je zapnuté Omezení registrace podle domény, bude e-mail ověřován vždy a tato volba bude ignorována.',
|
||||
'reg_confirm_restrict_domain' => 'Omezit registraci podle domény',
|
||||
'reg_confirm_restrict_domain_desc' => 'Zadejte emailové domény, kterým bude povolena registrace uživatelů. Oddělujete čárkou. Uživatelům bude odeslán email s odkazem pro potvrzení vlastnictví emailové adresy. Bez potvrzení nebudou moci aplikaci používat. <br> Pozn.: Uživatelé si mohou emailovou adresu změnit po úspěšné registraci.',
|
||||
'reg_confirm_restrict_domain_desc' => 'Zadejte seznam e-mailových domén oddělených čárkami, na které chcete registraci omezit. Registrujícímu se uživateli bude zaslán e-mail, aby ověřil svoji e-mailovou adresu před tím, než mu bude přístup do aplikace povolen. <br> Upozorňujeme, že po úspěšné registraci může uživatel svoji e-mailovou adresu změnit.',
|
||||
'reg_confirm_restrict_domain_placeholder' => 'Žádná omezení nebyla nastavena',
|
||||
|
||||
// Maintenance settings
|
||||
'maint' => 'Údržba',
|
||||
'maint_image_cleanup' => 'Promazání obrázků',
|
||||
'maint_image_cleanup_desc' => 'Prohledá stránky a jejich revize, aby zjistil, které obrázky a kresby jsou momentálně používány a které jsou zbytečné. Zajistěte plnou zálohu databáze a obrázků než se do toho pustíte.',
|
||||
'maint_image_cleanup' => 'Pročistění obrázků',
|
||||
'maint_image_cleanup_desc' => "Prohledá stránky a jejich revize, aby zjistil, které obrázky a kresby jsou momentálně používány a které jsou zbytečné. Zajistěte plnou zálohu databáze a obrázků než se do toho pustíte.",
|
||||
'maint_image_cleanup_ignore_revisions' => 'Ignorovat obrázky v revizích',
|
||||
'maint_image_cleanup_run' => 'Spustit Promazání',
|
||||
'maint_image_cleanup_run' => 'Spustit pročištění',
|
||||
'maint_image_cleanup_warning' => 'Nalezeno :count potenciálně nepoužitých obrázků. Jste si jistí, že je chcete smazat?',
|
||||
'maint_image_cleanup_success' => 'Potenciálně nepoužité obrázky byly smazány. Celkem :count.',
|
||||
'maint_image_cleanup_nothing_found' => 'Žádné potenciálně nepoužité obrázky nebyly nalezeny. Nic nebylo smazáno.',
|
||||
@@ -77,23 +77,23 @@ return [
|
||||
'maint_send_test_email_desc' => 'Toto pošle zkušební e-mail na vaši e-mailovou adresu uvedenou ve vašem profilu.',
|
||||
'maint_send_test_email_run' => 'Odeslat zkušební e-mail',
|
||||
'maint_send_test_email_success' => 'E-mail odeslán na :address',
|
||||
'maint_send_test_email_mail_subject' => 'Test Email',
|
||||
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
|
||||
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
|
||||
'maint_send_test_email_mail_subject' => 'Testovací e-mail',
|
||||
'maint_send_test_email_mail_greeting' => 'Zdá se, že posílání e-mailů funguje!',
|
||||
'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.',
|
||||
|
||||
// Audit Log
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_table_item' => 'Related Item',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit' => 'Protokol auditu',
|
||||
'audit_desc' => 'Tento protokol auditu zobrazuje seznam činností zaznamenaných v systému. Tento seznam není filtrován na rozdíl od podobných seznamů aktivit v systému, kde jsou použity filtry podle oprávnění.',
|
||||
'audit_event_filter' => 'Filtr událostí',
|
||||
'audit_event_filter_no_filter' => 'Bez filtru',
|
||||
'audit_deleted_item' => 'Odstraněná položka',
|
||||
'audit_deleted_item_name' => 'Jméno: :name',
|
||||
'audit_table_user' => 'Uživatel',
|
||||
'audit_table_event' => 'Událost',
|
||||
'audit_table_item' => 'Související položka',
|
||||
'audit_table_date' => 'Datum aktivity',
|
||||
'audit_date_from' => 'Časový rozsah od',
|
||||
'audit_date_to' => 'Časový rozsah do',
|
||||
|
||||
// Role Settings
|
||||
'roles' => 'Role',
|
||||
@@ -101,9 +101,9 @@ return [
|
||||
'role_create' => 'Vytvořit novou roli',
|
||||
'role_create_success' => 'Role byla úspěšně vytvořena',
|
||||
'role_delete' => 'Smazat roli',
|
||||
'role_delete_confirm' => "Role ':roleName' bude smazána.",
|
||||
'role_delete_users_assigned' => 'Role je přiřazena :userCount uživatelům. Pokud je chcete přesunout do jiné role, zvolte jednu z následujících.',
|
||||
'role_delete_no_migration' => 'Nepřiřazovat uživatelům novou roli',
|
||||
'role_delete_confirm' => 'Role \':roleName\' bude smazána.',
|
||||
'role_delete_users_assigned' => 'Role je přiřazena :userCount uživatelům. Pokud jim chcete náhradou přidělit jinou roli, zvolte jednu z následujících.',
|
||||
'role_delete_no_migration' => "Nepřiřazovat uživatelům náhradní roli",
|
||||
'role_delete_sure' => 'Opravdu chcete tuto roli smazat?',
|
||||
'role_delete_success' => 'Role byla úspěšně smazána',
|
||||
'role_edit' => 'Upravit roli',
|
||||
@@ -112,24 +112,24 @@ return [
|
||||
'role_desc' => 'Stručný popis role',
|
||||
'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 úživatelů',
|
||||
'role_manage_users' => 'Správa uživatelů',
|
||||
'role_manage_roles' => 'Správa rolí a jejich práv',
|
||||
'role_manage_entity_permissions' => 'Správa práv všech knih, kapitol a stránek',
|
||||
'role_manage_own_entity_permissions' => 'Správa práv vlastních knih, kapitol a stránek',
|
||||
'role_manage_page_templates' => 'Spravovat šablony stránek',
|
||||
'role_access_api' => 'Přístup k API systému',
|
||||
'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_asset' => 'Práva děl',
|
||||
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
|
||||
'role_asset_desc' => 'Tato práva řídí přístup k dílům v rámci systému. Specifická práva na knihách, kapitolách a stránkách překryjí tato nastavení.',
|
||||
'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 práva řídí přístup k obsahu napříč systémem. Specifická práva na knihách, kapitolách a stránkách převáží tato nastavení.',
|
||||
'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.',
|
||||
'role_all' => 'Vše',
|
||||
'role_own' => 'Vlastní',
|
||||
'role_controlled_by_asset' => 'Řídí se obsahem do kterého jsou nahrávány',
|
||||
'role_save' => 'Uloži roli',
|
||||
'role_update_success' => 'Role úspěšně upravena',
|
||||
'role_users' => 'Uživatelé, kteří mají tuto roli',
|
||||
'role_users_none' => 'Žádný uživatel tuto roli nemá.',
|
||||
'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
|
||||
'role_save' => 'Uložit roli',
|
||||
'role_update_success' => 'Role úspěšně aktualizována',
|
||||
'role_users' => 'Uživatelé mající tuto roli',
|
||||
'role_users_none' => 'Žádný uživatel nemá tuto roli',
|
||||
|
||||
// Users
|
||||
'users' => 'Uživatelé',
|
||||
@@ -137,38 +137,38 @@ return [
|
||||
'users_add_new' => 'Přidat nového uživatele',
|
||||
'users_search' => 'Vyhledávání uživatelů',
|
||||
'users_details' => 'Údaje o uživateli',
|
||||
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
|
||||
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
|
||||
'users_details_desc' => 'Nastavte zobrazované jméno a e-mailovou adresu pro tohoto uživatele. E-mailová adresa bude použita pro přihlášení do aplikace.',
|
||||
'users_details_desc_no_email' => 'Nastavte zobrazované jméno pro tohoto uživatele, aby jej ostatní uživatele poznali.',
|
||||
'users_role' => 'Uživatelské role',
|
||||
'users_role_desc' => 'Vyberte, do kterých rolí bude uživatel přiřazen. Pokud je uživatel přiřazen k více rolím, oprávnění z těchto rolí se budou skládat a budou dostávat všechny schopnosti přiřazených rolí.',
|
||||
'users_password' => 'Uživatelské heslo',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
|
||||
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||
'users_send_invite_option' => 'Send user invite email',
|
||||
'users_external_auth_id' => 'Přihlašovací identifikátory třetích stran',
|
||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
|
||||
'users_password_warning' => 'Vyplňujte pouze v případě, že chcete heslo změnit:',
|
||||
'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil vaší aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.',
|
||||
'users_role_desc' => 'Zvolte role, do kterých chcete uživatele zařadit. Pokud bude uživatel zařazen do více rolí, oprávnění z těchto rolí se sloučí a uživateli bude dovoleno vše, k čemu mají jednotlivé role oprávnění.',
|
||||
'users_password' => 'Heslo uživatele',
|
||||
'users_password_desc' => 'Zadejte heslo pro přihlášení do aplikace. Heslo musí být nejméně 6 znaků dlouhé.',
|
||||
'users_send_invite_text' => 'Uživateli můžete poslat pozvánku e-mailem, která umožní uživateli, aby si zvolil sám svoje heslo do aplikace a nebo můžete zadat heslo sami.',
|
||||
'users_send_invite_option' => 'Poslat uživateli pozvánku e-mailem',
|
||||
'users_external_auth_id' => 'Přihlašovací identifikátor třetích stran',
|
||||
'users_external_auth_id_desc' => 'ID použité pro rozpoznání tohoto uživatele když komunikuje s externím přihlašovacím systémem.',
|
||||
'users_password_warning' => 'Vyplňujte pouze v případě, že chcete heslo změnit.',
|
||||
'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.',
|
||||
'users_delete' => 'Smazat uživatele',
|
||||
'users_delete_named' => 'Odstranit uživatele :userName',
|
||||
'users_delete_warning' => 'Uživatel \':userName\' bude úplně smazán ze systému.',
|
||||
'users_delete_warning' => 'Uživatel \':userName\' bude zcela smazán ze systému.',
|
||||
'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?',
|
||||
'users_delete_success' => 'Uživatel byl úspěšně smazán',
|
||||
'users_edit' => 'Upravit uživatele',
|
||||
'users_edit_profile' => 'Upravit profil',
|
||||
'users_edit_success' => 'Uživatel byl úspěšně aktualizován',
|
||||
'users_avatar' => 'Obrázek uživatele',
|
||||
'users_avatar_desc' => 'Obrázek by měl být čtverec 256 pixelů široký. Bude oříznut do kruhu.',
|
||||
'users_avatar_desc' => 'Vyberte obrázek, který bude reprezentovat tohoto uživatele. Měl by být přibližně 256px velký ve tvaru čtverce.',
|
||||
'users_preferred_language' => 'Preferovaný jazyk',
|
||||
'users_preferred_language_desc' => 'tato volba ovlivní pouze jazyk používaný v uživatelském rozhraní aplikace. Vobla nemá vliv na žádný uživateli vytvářený obsah.',
|
||||
'users_social_accounts' => 'Přidružené účty ze sociálních sítí',
|
||||
'users_social_accounts_info' => 'Zde můžete přidat vaše účty ze sociálních sítí pro pohodlnější přihlašování. Zrušení přidružení účtů neznamená, že tato aplikace ztratí práva číst detaily z vašeho účtu. Zakázat této aplikaci přístup k detailům vašeho účtu musíte přímo ve svém profilu na dané sociální síti.',
|
||||
'users_social_connect' => 'Přidružit účet',
|
||||
'users_social_disconnect' => 'Zrušit přidružení',
|
||||
'users_social_connected' => 'Účet :socialAccount byl úspěšně přidružen k vašemu profilu.',
|
||||
'users_social_disconnected' => 'Přidružení účtu :socialAccount k vašemu profilu bylo úspěšně zrušeno.',
|
||||
'users_api_tokens' => 'API Klíče',
|
||||
'users_api_tokens_none' => 'Pro tohoto uživatele nebyly vytvořeny žádné API klíče',
|
||||
'users_preferred_language_desc' => 'Tato volba ovlivní pouze jazyk používaný v uživatelském rozhraní aplikace. Volba nemá vliv na žádný uživateli vytvářený obsah.',
|
||||
'users_social_accounts' => 'Sociální účty',
|
||||
'users_social_accounts_info' => 'Zde můžete přidat vaše účty ze sociálních sítí pro pohodlnější přihlašování. Odpojení účtů neznamená, že tato aplikace ztratí práva číst detaily z vašeho účtu. Zakázat této aplikaci přístup k detailům vašeho účtu musíte přímo ve svém profilu na dané sociální síti.',
|
||||
'users_social_connect' => 'Připojit účet',
|
||||
'users_social_disconnect' => 'Odpojit účet',
|
||||
'users_social_connected' => 'Účet :socialAccount byl úspěšně připojen k vašemu profilu.',
|
||||
'users_social_disconnected' => 'Účet :socialAccount byl úspěšně odpojen od vašeho profilu.',
|
||||
'users_api_tokens' => 'API Tokeny',
|
||||
'users_api_tokens_none' => 'Tento uživatel nemá vytvořené žádné API Tokeny',
|
||||
'users_api_tokens_create' => 'Vytvořit Token',
|
||||
'users_api_tokens_expires' => 'Vyprší',
|
||||
'users_api_tokens_docs' => 'API Dokumentace',
|
||||
@@ -176,13 +176,13 @@ return [
|
||||
// API Tokens
|
||||
'user_api_token_create' => 'Vytvořit API Klíč',
|
||||
'user_api_token_name' => 'Název',
|
||||
'user_api_token_name_desc' => 'Zadejte srozumitelný název tokenu, který vám později může pomoci připomenout účet, za jakým jste token vytvářeli.',
|
||||
'user_api_token_expiry' => 'Datum expirace',
|
||||
'user_api_token_name_desc' => 'Zadejte srozumitelný název tokenu, který vám později může pomoci připomenout účel, za jakým jste token vytvářeli.',
|
||||
'user_api_token_expiry' => 'Platný do',
|
||||
'user_api_token_expiry_desc' => 'Zadejte datum, kdy platnost tokenu vyprší. Po tomto datu nebudou požadavky, které používají tento token, fungovat. Pokud ponecháte pole prázdné, bude tokenu nastavena platnost na dalších 100 let.',
|
||||
'user_api_token_create_secret_message' => 'Ihned po vytvoření tokenu Vám bude vygenerován a zobrazen "Token ID" a "Token Secret". Upozorňujeme, že "Token Secret" bude možné zobrazit pouze jednou, ujistěte se, že si jej poznamenáte a uložíte na bezpečné místo před tím, než budete pokračovat dále.',
|
||||
'user_api_token_create_success' => 'API token úspěšně vytvořen',
|
||||
'user_api_token_update_success' => 'API token úspěšně updaten',
|
||||
'user_api_token' => 'API Token',
|
||||
'user_api_token_create_success' => 'API klíč úspěšně vytvořen',
|
||||
'user_api_token_update_success' => 'API klíč úspěšně aktualizován',
|
||||
'user_api_token' => 'API Klíč',
|
||||
'user_api_token_id' => 'Token ID',
|
||||
'user_api_token_id_desc' => 'Toto je neupravitelný systémový identifikátor generovaný pro tento klíč, který musí být uveden v API requestu.',
|
||||
'user_api_token_secret' => 'Token Secret',
|
||||
|
||||
@@ -105,7 +105,7 @@ return [
|
||||
// Custom validation lines
|
||||
'custom' => [
|
||||
'password-confirm' => [
|
||||
'required_with' => 'Ověření hesla je vyžadováno',
|
||||
'required_with' => 'Je nutné potvrdit heslo',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'attachments_link_url' => 'Link zu einer Datei',
|
||||
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
|
||||
'attach' => 'Hinzufügen',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => 'Link zum Anhang auf Seite einfügen',
|
||||
'attachments_edit_file' => 'Datei bearbeiten',
|
||||
'attachments_edit_file_name' => 'Dateiname',
|
||||
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hierher, um diese hochzuladen und zu überschreiben',
|
||||
|
||||
@@ -85,18 +85,18 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'maint_send_test_email_mail_text' => 'Glückwunsch! Da Sie diese E-Mail Benachrichtigung erhalten haben, scheinen Ihre E-Mail-Einstellungen korrekt konfiguriert zu sein.',
|
||||
|
||||
// Audit Log
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit' => 'Audit-Protokoll',
|
||||
'audit_desc' => 'Dieses Audit-Protokoll zeigt eine Liste der Aktivitäten an, welche vom System protokolliert werden. Im Gegensatz zu den anderen Aktivitätslisten im System, bei denen Berechtigungen angewendet werden, ist diese Liste ungefiltert.',
|
||||
'audit_event_filter' => 'Ereignisfilter',
|
||||
'audit_event_filter_no_filter' => 'Kein Filter',
|
||||
'audit_deleted_item' => 'Gelöschtes Objekt',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_table_item' => 'Related Item',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit_table_user' => 'Benutzer',
|
||||
'audit_table_event' => 'Ereignis',
|
||||
'audit_table_item' => 'Verwendetes Objekt',
|
||||
'audit_table_date' => 'Aktivitätsdatum',
|
||||
'audit_date_from' => 'Zeitraum von',
|
||||
'audit_date_to' => 'Zeitraum bis',
|
||||
|
||||
// Role Settings
|
||||
'roles' => 'Rollen',
|
||||
@@ -203,7 +203,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'language_select' => [
|
||||
'en' => 'English',
|
||||
'ar' => 'العربية',
|
||||
'bg' => 'Bǎlgarski',
|
||||
'bg' => 'Bulgarisch',
|
||||
'cs' => 'Česky',
|
||||
'da' => 'Dänisch',
|
||||
'de' => 'Deutsch (Sie)',
|
||||
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'attachments_link_url' => 'Link zu einer Datei',
|
||||
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
|
||||
'attach' => 'Hinzufügen',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => 'Link zum Anhang auf Seite einfügen',
|
||||
'attachments_edit_file' => 'Datei bearbeiten',
|
||||
'attachments_edit_file_name' => 'Dateiname',
|
||||
'attachments_edit_drop_upload' => 'Ziehe Dateien hierher, um diese hochzuladen und zu überschreiben',
|
||||
|
||||
@@ -85,18 +85,18 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'maint_send_test_email_mail_text' => 'Glückwunsch! Da du diese E-Mail Benachrichtigung erhalten hast, scheinen deine E-Mail-Einstellungen korrekt konfiguriert zu sein.',
|
||||
|
||||
// Audit Log
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit' => 'Audit-Protokoll',
|
||||
'audit_desc' => 'Dieses Audit-Protokoll zeigt eine Liste der Aktivitäten an, welche vom System protokolliert werden. Im Gegensatz zu den anderen Aktivitätslisten im System, bei denen Berechtigungen angewendet werden, ist diese Liste ungefiltert.',
|
||||
'audit_event_filter' => 'Ereignisfilter',
|
||||
'audit_event_filter_no_filter' => 'Kein Filter',
|
||||
'audit_deleted_item' => 'Gelöschtes Objekt',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_table_item' => 'Related Item',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit_table_user' => 'Benutzer',
|
||||
'audit_table_event' => 'Ereignis',
|
||||
'audit_table_item' => 'Verwendetes Objekt',
|
||||
'audit_table_date' => 'Aktivitätsdatum',
|
||||
'audit_date_from' => 'Zeitraum von',
|
||||
'audit_date_to' => 'Zeitraum bis',
|
||||
|
||||
// Role Settings
|
||||
'roles' => 'Rollen',
|
||||
@@ -203,7 +203,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'language_select' => [
|
||||
'en' => 'English',
|
||||
'ar' => 'العربية',
|
||||
'bg' => 'Bǎlgarski',
|
||||
'bg' => 'Bulgarisch',
|
||||
'cs' => 'Česky',
|
||||
'da' => 'Dänisch',
|
||||
'de' => 'Deutsch (Sie)',
|
||||
|
||||
@@ -90,6 +90,7 @@ return [
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute and :other must match.',
|
||||
'safe_url' => 'The provided link may not be safe.',
|
||||
'size' => [
|
||||
'numeric' => 'The :attribute must be :size.',
|
||||
'file' => 'The :attribute must be :size kilobytes.',
|
||||
|
||||
@@ -179,7 +179,7 @@ return [
|
||||
'user_api_token_name_desc' => 'Dale a tu token un nombre legible como un recordatorio futuro de su propósito.',
|
||||
'user_api_token_expiry' => 'Fecha de expiración',
|
||||
'user_api_token_expiry_desc' => 'Establece una fecha en la que este token expira. Después de esta fecha, las solicitudes realizadas usando este token ya no funcionarán. Dejar este campo en blanco fijará un vencimiento de 100 años en el futuro.',
|
||||
'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
|
||||
'user_api_token_create_secret_message' => 'Inmediatamente después de crear este token se generarán y mostrarán sus correspondientes "Token ID" y "Token Secret". El "Token Secret" sólo se mostrará una vez, así que asegúrese de copiar el valor a un lugar seguro antes de proceder.',
|
||||
'user_api_token_create_success' => 'Token API creado correctamente',
|
||||
'user_api_token_update_success' => 'Token API actualizado correctamente',
|
||||
'user_api_token' => 'Token API',
|
||||
@@ -187,8 +187,8 @@ return [
|
||||
'user_api_token_id_desc' => 'Este es un identificador no editable generado por el sistema y único para este token que necesitará ser proporcionado en solicitudes de API.',
|
||||
'user_api_token_secret' => 'Token Secret',
|
||||
'user_api_token_secret_desc' => 'Esta es una clave no editable generada por el sistema que necesitará ser proporcionada en solicitudes de API. Solo se monstraré esta vez así que guarde su valor en un lugar seguro.',
|
||||
'user_api_token_created' => 'Token created :timeAgo',
|
||||
'user_api_token_updated' => 'Token updated :timeAgo',
|
||||
'user_api_token_created' => 'Token creado :timeAgo',
|
||||
'user_api_token_updated' => 'Token actualizado :timeAgo',
|
||||
'user_api_token_delete' => 'Borrar token',
|
||||
'user_api_token_delete_warning' => 'Esto eliminará completamente este token API con el nombre \':tokenName\' del sistema.',
|
||||
'user_api_token_delete_confirm' => '¿Está seguro de que desea borrar este API token?',
|
||||
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'attachments_link_url' => 'Enlace a archivo',
|
||||
'attachments_link_url_hint' => 'URL del sitio o archivo',
|
||||
'attach' => 'Adjuntar',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => 'Añadir enlace al adjunto en la página',
|
||||
'attachments_edit_file' => 'Editar archivo',
|
||||
'attachments_edit_file_name' => 'Nombre del archivo',
|
||||
'attachments_edit_drop_upload' => 'Arrastre los archivos o presione aquí para subir o sobreescribir',
|
||||
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'attachments_link_url' => 'Ссылка на файл',
|
||||
'attachments_link_url_hint' => 'URL-адрес сайта или файла',
|
||||
'attach' => 'Прикрепить',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => 'Добавить ссылку на вложение',
|
||||
'attachments_edit_file' => 'Редактировать файл',
|
||||
'attachments_edit_file_name' => 'Название файла',
|
||||
'attachments_edit_drop_upload' => 'Перетащите файлы или нажмите здесь, чтобы загрузить и перезаписать',
|
||||
|
||||
@@ -82,18 +82,18 @@ return [
|
||||
'maint_send_test_email_mail_text' => 'Поздравляем! Поскольку вы получили это письмо, электронная почта настроена правильно.',
|
||||
|
||||
// Audit Log
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_table_item' => 'Related Item',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit' => 'Журнал аудита',
|
||||
'audit_desc' => 'Этот журнал аудита отображает список действий, отслеживаемых в системе. Этот список не отфильтрован в отличие от аналогичных списков действий в системе, где применяются фильтры разрешений.',
|
||||
'audit_event_filter' => 'Фильтр событий',
|
||||
'audit_event_filter_no_filter' => 'Без фильтра',
|
||||
'audit_deleted_item' => 'Удаленный элемент',
|
||||
'audit_deleted_item_name' => 'Имя: :name',
|
||||
'audit_table_user' => 'Пользователь',
|
||||
'audit_table_event' => 'Событие',
|
||||
'audit_table_item' => 'Связанный элемент',
|
||||
'audit_table_date' => 'Дата действия',
|
||||
'audit_date_from' => 'Диапазон даты от',
|
||||
'audit_date_to' => 'Диапазон даты до',
|
||||
|
||||
// Role Settings
|
||||
'roles' => 'Роли',
|
||||
|
||||
@@ -265,7 +265,7 @@ return [
|
||||
'attachments_link_url' => '链接到文件',
|
||||
'attachments_link_url_hint' => '网站或文件的网址',
|
||||
'attach' => '附加',
|
||||
'attachments_insert_link' => 'Add Attachment Link to Page',
|
||||
'attachments_insert_link' => '将附加链接添加到页面',
|
||||
'attachments_edit_file' => '编辑文件',
|
||||
'attachments_edit_file_name' => '文件名',
|
||||
'attachments_edit_drop_upload' => '删除文件或点击这里上传并覆盖',
|
||||
|
||||
@@ -57,7 +57,7 @@ return [
|
||||
'reg_enable_desc' => '启用注册后,用户将可以自己注册为站点用户。 注册后,他们将获得一个默认的单一用户角色。',
|
||||
'reg_default_role' => '注册后的默认用户角色',
|
||||
'reg_enable_external_warning' => '当启用外部LDAP或者SAML认证时,上面的选项会被忽略。当使用外部系统认证认证成功时,将自动创建非现有会员的用户账户。',
|
||||
'reg_email_confirmation' => '邮箱确认n',
|
||||
'reg_email_confirmation' => '邮件确认',
|
||||
'reg_email_confirmation_toggle' => '需要电子邮件确认',
|
||||
'reg_confirm_email_desc' => '如果使用域名限制,则需要Email验证,并且该值将被忽略。',
|
||||
'reg_confirm_restrict_domain' => '域名限制',
|
||||
@@ -82,18 +82,18 @@ return [
|
||||
'maint_send_test_email_mail_text' => '恭喜!您收到了此邮件通知,你的电子邮件设置看起来配置正确。',
|
||||
|
||||
// Audit Log
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_table_item' => 'Related Item',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit' => '审核日志',
|
||||
'audit_desc' => '该审核日志显示系统中跟踪的活动列表。与系统中应用了权限过滤器的类似活动列表不同,这个表是未经过滤的。',
|
||||
'audit_event_filter' => '事件过滤器',
|
||||
'audit_event_filter_no_filter' => '无过滤器',
|
||||
'audit_deleted_item' => '被删除的项目',
|
||||
'audit_deleted_item_name' => '姓名: :name',
|
||||
'audit_table_user' => '用户',
|
||||
'audit_table_event' => '事件',
|
||||
'audit_table_item' => '相关项目',
|
||||
'audit_table_date' => '活动日期',
|
||||
'audit_date_from' => '日期范围从',
|
||||
'audit_date_to' => '日期范围至',
|
||||
|
||||
// Role Settings
|
||||
'roles' => '角色',
|
||||
@@ -200,7 +200,7 @@ return [
|
||||
'language_select' => [
|
||||
'en' => 'English',
|
||||
'ar' => 'العربية',
|
||||
'bg' => 'Bǎlgarski',
|
||||
'bg' => '保加利亚语',
|
||||
'cs' => 'Česky',
|
||||
'da' => '丹麦',
|
||||
'de' => 'Deutsch (Sie)',
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
&.warning:before {
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,4 +274,4 @@
|
||||
.sticky-sidebar {
|
||||
position: sticky;
|
||||
top: $-m;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
@@ -11,7 +13,8 @@
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
@@ -57,7 +60,12 @@
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cm-fat-cursor-mark {
|
||||
background-color: rgba(20, 255, 20, 0.5);
|
||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
||||
-moz-animation: blink 1.06s steps(1) infinite;
|
||||
animation: blink 1.06s steps(1) infinite;
|
||||
}
|
||||
.cm-animate-fat-cursor {
|
||||
width: auto;
|
||||
border: 0;
|
||||
@@ -89,7 +97,7 @@
|
||||
|
||||
.CodeMirror-rulers {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: -50px; bottom: -20px;
|
||||
left: 0; right: 0; top: -50px; bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
@@ -118,7 +126,7 @@
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3 {color: #085;}
|
||||
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
@@ -138,8 +146,8 @@
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
@@ -156,17 +164,17 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
/* 50px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -30px; margin-right: -30px;
|
||||
padding-bottom: 30px;
|
||||
margin-bottom: -50px; margin-right: -50px;
|
||||
padding-bottom: 50px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 30px solid transparent;
|
||||
border-right: 50px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
@@ -176,6 +184,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
outline: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
@@ -204,7 +213,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -30px;
|
||||
margin-bottom: -50px;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
@@ -229,11 +238,13 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre {
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
@@ -246,12 +257,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-variant-ligatures: contextual;
|
||||
font-variant-ligatures: contextual;
|
||||
&:after {
|
||||
content: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.CodeMirror-wrap pre {
|
||||
.CodeMirror-wrap pre.CodeMirror-line,
|
||||
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
@@ -266,7 +274,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: auto;
|
||||
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
@@ -321,8 +329,8 @@ div.CodeMirror-dragcursors {
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||
|
||||
.cm-searching {
|
||||
background: #ffa;
|
||||
background: rgba(255, 255, 0, .4);
|
||||
background-color: #ffa;
|
||||
background-color: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
@@ -341,53 +349,6 @@ div.CodeMirror-dragcursors {
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
||||
|
||||
/**
|
||||
* Codemirror Default theme
|
||||
*/
|
||||
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
.cm-strikethrough {text-decoration: line-through;}
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/**
|
||||
@@ -461,6 +422,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||
border: 1px solid;
|
||||
@include lightDark(border-color, #DDD, #111);
|
||||
}
|
||||
.CodeMirror pre::after {
|
||||
display: none;
|
||||
}
|
||||
html.dark-mode .CodeMirror pre {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
body {
|
||||
background-color: #fff;
|
||||
padding-inline-start: 16px;
|
||||
margin-inline-end: 16px;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
[drawio-diagram]:hover {
|
||||
outline: 2px solid var(--color-primary);
|
||||
|
||||
@@ -124,6 +124,9 @@ body.flexbox {
|
||||
.flex-container-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
&.v-center {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-container-column {
|
||||
@@ -131,9 +134,17 @@ body.flexbox {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-container-column.wrap, .flex-container-row.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
&.fit-content {
|
||||
flex-basis: auto;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.justify-flex-end {
|
||||
|
||||
@@ -35,7 +35,7 @@ $text: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
|
||||
"Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
$mono: "Lucida Console", "DejaVu Sans Mono", "Ubunto Mono", Monaco, monospace;
|
||||
$mono: "Lucida Console", "DejaVu Sans Mono", "Ubuntu Mono", Monaco, monospace;
|
||||
$heading: $text;
|
||||
$fs-m: 14px;
|
||||
$fs-s: 12px;
|
||||
@@ -68,4 +68,4 @@ $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
|
||||
$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);
|
||||
$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div component="ajax-form"
|
||||
option:ajax-form:url="/attachments/{{ $attachment->id }}"
|
||||
option:ajax-form:method="put"
|
||||
option:ajax-form:response-container=".attachment-edit-container"
|
||||
option:ajax-form:success-message="{{ trans('entities.attachments_updated_success') }}">
|
||||
<h5>{{ trans('entities.attachments_edit_file') }}</h5>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div component="ajax-form"
|
||||
option:ajax-form:url="/attachments/link"
|
||||
option:ajax-form:method="post"
|
||||
option:ajax-form:response-container=".link-form-container"
|
||||
option:ajax-form:success-message="{{ trans('entities.attachments_link_attached') }}">
|
||||
<input type="hidden" name="attachment_link_uploaded_to" value="{{ $pageId }}">
|
||||
<p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
'successMessage' => trans('entities.attachments_file_uploaded'),
|
||||
])
|
||||
</div>
|
||||
<div refs="tabs@contentLinks" class="hidden">
|
||||
<div refs="tabs@contentLinks" class="hidden link-form-container">
|
||||
@include('attachments.manager-link-form', ['pageId' => $page->id])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div refs="attachments@editContainer" class="hidden">
|
||||
<div refs="attachments@editContainer" class="hidden attachment-edit-container">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
<ul class="contents">
|
||||
@foreach($bookChildren as $bookChild)
|
||||
<li><a href="#{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</a></li>
|
||||
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
|
||||
@if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
|
||||
<ul>
|
||||
@foreach($bookChild->pages as $page)
|
||||
@foreach($bookChild->visible_pages as $page)
|
||||
<li><a href="#page-{{$page->id}}">{{ $page->name }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@@ -59,8 +59,8 @@
|
||||
@if($bookChild->isA('chapter'))
|
||||
<p>{{ $bookChild->description }}</p>
|
||||
|
||||
@if(count($bookChild->pages) > 0)
|
||||
@foreach($bookChild->pages as $page)
|
||||
@if(count($bookChild->visible_pages) > 0)
|
||||
@foreach($bookChild->visible_pages as $page)
|
||||
<div class="page-break"></div>
|
||||
<div class="chapter-hint">{{$bookChild->name}}</div>
|
||||
<h1 id="page-{{$page->id}}">{{ $page->name }}</h1>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
@if($bookChild->isA('chapter'))
|
||||
<ul>
|
||||
@foreach($bookChild->pages as $page)
|
||||
@foreach($bookChild->visible_pages as $page)
|
||||
<li class="text-page"
|
||||
data-id="{{$page->id}}" data-type="page"
|
||||
data-name="{{ $page->name }}" data-created="{{ $page->created_at->timestamp }}"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="chapter-child-menu">
|
||||
<button chapter-toggle type="button" aria-expanded="{{ $isOpen ? 'true' : 'false' }}"
|
||||
class="text-muted @if($isOpen) open @endif">
|
||||
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->pages->count()) }}</span>
|
||||
@icon('caret-right') @icon('page') <span>{{ trans_choice('entities.x_pages', $bookChild->visible_pages->count()) }}</span>
|
||||
</button>
|
||||
<ul class="sub-menu inset-list @if($isOpen) open @endif" @if($isOpen) style="display: block;" @endif role="menu">
|
||||
@foreach($bookChild->pages as $childPage)
|
||||
@foreach($bookChild->visible_pages as $childPage)
|
||||
<li class="list-item-page {{ $childPage->isA('page') && $childPage->draft ? 'draft' : '' }}" role="presentation">
|
||||
@include('partials.entity-list-item-basic', ['entity' => $childPage, 'classes' => $current->matches($childPage)? 'selected' : '' ])
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->hasChildren()) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
|
||||
{{--This view display child pages in a list if pre-loaded onto a 'visible_pages' property,--}}
|
||||
{{--To ensure that the pages have been loaded efficiently with permissions taken into account.--}}
|
||||
<a href="{{ $chapter->getUrl() }}" class="chapter entity-list-item @if($chapter->visible_pages->count() > 0) has-children @endif" data-entity-type="chapter" data-entity-id="{{$chapter->id}}">
|
||||
<span class="icon text-chapter">@icon('chapter')</span>
|
||||
<div class="content">
|
||||
<h4 class="entity-list-item-name break-text">{{ $chapter->name }}</h4>
|
||||
@@ -7,16 +9,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@if ($chapter->hasChildren())
|
||||
@if ($chapter->visible_pages->count() > 0)
|
||||
<div class="chapter chapter-expansion">
|
||||
<span class="icon text-chapter">@icon('page')</span>
|
||||
<div class="content">
|
||||
<button type="button" chapter-toggle
|
||||
aria-expanded="false"
|
||||
class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->pages->count()) }}</span></button>
|
||||
class="text-muted chapter-expansion-toggle">@icon('caret-right') <span>{{ trans_choice('entities.x_pages', $chapter->visible_pages->count()) }}</span></button>
|
||||
<div class="inset-list">
|
||||
<div class="entity-list-item-children">
|
||||
@include('partials.entity-list', ['entities' => $chapter->pages])
|
||||
@include('partials.entity-list', ['entities' => $chapter->visible_pages])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<a refs="code-editor@languageLink" data-lang="Ruby">Ruby</a>
|
||||
<a refs="code-editor@languageLink" data-lang="shell">Shell/Bash</a>
|
||||
<a refs="code-editor@languageLink" data-lang="SQL">SQL</a>
|
||||
<a refs="code-editor@languageLink" data-lang="VBScript">VBScript</a>
|
||||
<a refs="code-editor@languageLink" data-lang="XML">XML</a>
|
||||
<a refs="code-editor@languageLink" data-lang="YAML">YAML</a>
|
||||
</small>
|
||||
@@ -66,4 +67,4 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div component="page-editor" class="page-editor flex-fill flex"
|
||||
option:page-editor:drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
|
||||
@if(config('services.drawio'))
|
||||
drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://www.draw.io/?embed=1&proto=json&spin=1' }}"
|
||||
drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://embed.diagrams.net/?embed=1&proto=json&spin=1' }}"
|
||||
@endif
|
||||
@if($model->name === trans('entities.pages_initial_name'))
|
||||
option:page-editor:has-default-title="true"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
|
||||
</div>
|
||||
<iframe srcdoc="" class="markdown-display" sandbox="allow-same-origin"></iframe>
|
||||
<iframe src="about:blank" class="markdown-display" sandbox="allow-same-origin"></iframe>
|
||||
</div>
|
||||
<input type="hidden" name="html"/>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">
|
||||
@include('partials.entity-list-item-basic', ['entity' => $bookChild, 'classes' => $current->matches($bookChild)? 'selected' : ''])
|
||||
|
||||
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
|
||||
@if($bookChild->isA('chapter') && count($bookChild->visible_pages) > 0)
|
||||
<div class="entity-list-item no-hover">
|
||||
<span role="presentation" class="icon text-chapter"></span>
|
||||
<div class="content">
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<div class="page-list">
|
||||
@if(count($pages) > 0)
|
||||
@foreach($pages as $pageIndex => $page)
|
||||
<div class="anim searchResult" style="animation-delay: {{$pageIndex*50 . 'ms'}};">
|
||||
@include('pages.list-item', ['page' => $page])
|
||||
<hr>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<p class="text-muted">{{ trans('entities.search_no_pages') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(count($chapters) > 0)
|
||||
<div class="page-list">
|
||||
@foreach($chapters as $chapterIndex => $chapter)
|
||||
<div class="anim searchResult" style="animation-delay: {{($chapterIndex+count($pages))*50 . 'ms'}};">
|
||||
@include('chapters.list-item', ['chapter' => $chapter, 'hidePages' => true])
|
||||
<hr>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -3,16 +3,7 @@
|
||||
@section('body')
|
||||
<div class="container small">
|
||||
|
||||
<div class="grid left-focus v-center no-row-gap">
|
||||
<div class="py-m">
|
||||
@include('settings.navbar', ['selected' => 'settings'])
|
||||
</div>
|
||||
<div class="text-right p-m">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
|
||||
BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@include('settings.navbar-with-version', ['selected' => 'settings'])
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h2 id="features" class="list-heading">{{ trans('settings.app_features_security') }}</h2>
|
||||
|
||||
@@ -3,16 +3,7 @@
|
||||
@section('body')
|
||||
<div class="container small">
|
||||
|
||||
<div class="grid left-focus v-center no-row-gap">
|
||||
<div class="py-m">
|
||||
@include('settings.navbar', ['selected' => 'maintenance'])
|
||||
</div>
|
||||
<div class="text-right p-m">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
|
||||
BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@include('settings.navbar-with-version', ['selected' => 'maintenance'])
|
||||
|
||||
<div id="image-cleanup" class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.maint_image_cleanup') }}</h2>
|
||||
|
||||
15
resources/views/settings/navbar-with-version.blade.php
Normal file
15
resources/views/settings/navbar-with-version.blade.php
Normal file
@@ -0,0 +1,15 @@
|
||||
{{--
|
||||
$selected - String name of the selected tab
|
||||
$version - Version of bookstack to display
|
||||
--}}
|
||||
<div class="flex-container-row v-center wrap">
|
||||
<div class="py-m flex fit-content">
|
||||
@include('settings.navbar', ['selected' => $selected])
|
||||
</div>
|
||||
<div class="flex"></div>
|
||||
<div class="text-right p-m flex fit-content">
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://github.com/BookStackApp/BookStack/releases">
|
||||
BookStack @if(strpos($version, 'v') !== 0) version @endif {{ $version }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,24 +196,6 @@ class Saml2Test extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_user_registration_with_existing_email()
|
||||
{
|
||||
config()->set([
|
||||
'saml2.onelogin.strict' => false,
|
||||
]);
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
$viewer->email = 'user@example.com';
|
||||
$viewer->save();
|
||||
|
||||
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
|
||||
$acsPost = $this->post('/saml2/acs');
|
||||
$acsPost->assertRedirect('/');
|
||||
$errorMessage = session()->get('error');
|
||||
$this->assertEquals('A user with the email user@example.com already exists but with different credentials.', $errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
public function test_saml_routes_are_only_active_if_saml_enabled()
|
||||
{
|
||||
config()->set(['auth.method' => 'standard']);
|
||||
@@ -319,6 +301,33 @@ class Saml2Test extends TestCase
|
||||
$homeGet->assertRedirect('/register/confirm/awaiting');
|
||||
}
|
||||
|
||||
public function test_login_where_existing_non_saml_user_shows_warning()
|
||||
{
|
||||
$this->post('/saml2/login');
|
||||
config()->set(['saml2.onelogin.strict' => false]);
|
||||
|
||||
// Make the user pre-existing in DB with different auth_id
|
||||
User::query()->forceCreate([
|
||||
'email' => 'user@example.com',
|
||||
'external_auth_id' => 'old_system_user_id',
|
||||
'email_confirmed' => false,
|
||||
'name' => 'Barry Scott'
|
||||
]);
|
||||
|
||||
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
|
||||
$acsPost = $this->post('/saml2/acs');
|
||||
$acsPost->assertRedirect('/login');
|
||||
$this->assertFalse($this->isAuthenticated());
|
||||
$this->assertDatabaseHas('users', [
|
||||
'email' => 'user@example.com',
|
||||
'external_auth_id' => 'old_system_user_id',
|
||||
]);
|
||||
|
||||
$loginGet = $this->get('/login');
|
||||
$loginGet->assertSee("A user with the email user@example.com already exists but with different credentials");
|
||||
});
|
||||
}
|
||||
|
||||
protected function withGet(array $options, callable $callback)
|
||||
{
|
||||
return $this->withGlobal($_GET, $options, $callback);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<?php namespace Tests\Entity;
|
||||
|
||||
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Uploads\HttpFetcher;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -154,14 +153,39 @@ class ExportTest extends TestCase
|
||||
public function test_page_export_sets_right_data_type_for_svg_embeds()
|
||||
{
|
||||
$page = Page::first();
|
||||
$page->html = '<img src="http://example.com/image.svg">';
|
||||
Storage::disk('local')->makeDirectory('uploads/images/gallery');
|
||||
Storage::disk('local')->put('uploads/images/gallery/svg_test.svg', '<svg></svg>');
|
||||
$page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg">';
|
||||
$page->save();
|
||||
|
||||
$this->asEditor();
|
||||
$this->mockHttpFetch('<svg></svg>');
|
||||
$resp = $this->get($page->getUrl('/export/html'));
|
||||
Storage::disk('local')->delete('uploads/images/gallery/svg_test.svg');
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee('<img src="data:image/svg+xml;base64');
|
||||
}
|
||||
|
||||
public function test_page_export_contained_html_image_fetches_only_run_when_url_points_to_image_upload_folder()
|
||||
{
|
||||
$page = Page::first();
|
||||
$page->html = '<img src="http://localhost/uploads/images/gallery/svg_test.svg"/>'
|
||||
."\n".'<img src="http://localhost/uploads/svg_test.svg"/>'
|
||||
."\n".'<img src="/uploads/svg_test.svg"/>';
|
||||
$storageDisk = Storage::disk('local');
|
||||
$storageDisk->makeDirectory('uploads/images/gallery');
|
||||
$storageDisk->put('uploads/images/gallery/svg_test.svg', '<svg>good</svg>');
|
||||
$storageDisk->put('uploads/svg_test.svg', '<svg>bad</svg>');
|
||||
$page->save();
|
||||
|
||||
$resp = $this->asEditor()->get($page->getUrl('/export/html'));
|
||||
|
||||
$storageDisk->delete('uploads/images/gallery/svg_test.svg');
|
||||
$storageDisk->delete('uploads/svg_test.svg');
|
||||
|
||||
$resp->assertDontSee('http://localhost/uploads/images/gallery/svg_test.svg');
|
||||
$resp->assertSee('http://localhost/uploads/svg_test.svg');
|
||||
$resp->assertSee('src="/uploads/svg_test.svg"');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -71,6 +71,25 @@ class PageContentTest extends TestCase
|
||||
$pageResp->assertSee($content);
|
||||
}
|
||||
|
||||
public function test_page_includes_rendered_on_book_export()
|
||||
{
|
||||
$page = Page::query()->first();
|
||||
$secondPage = Page::query()
|
||||
->where('book_id', '!=', $page->book_id)
|
||||
->first();
|
||||
|
||||
$content = '<p id="bkmrk-meow">my cat is awesome and scratchy</p>';
|
||||
$secondPage->html = $content;
|
||||
$secondPage->save();
|
||||
|
||||
$page->html = "{{@{$secondPage->id}#bkmrk-meow}}";
|
||||
$page->save();
|
||||
|
||||
$this->asEditor();
|
||||
$htmlContent = $this->get($page->book->getUrl('/export/html'));
|
||||
$htmlContent->assertSee('my cat is awesome and scratchy');
|
||||
}
|
||||
|
||||
public function test_page_content_scripts_removed_by_default()
|
||||
{
|
||||
$this->asEditor();
|
||||
@@ -140,6 +159,72 @@ class PageContentTest extends TestCase
|
||||
|
||||
}
|
||||
|
||||
public function test_javascript_uri_links_are_removed()
|
||||
{
|
||||
$checks = [
|
||||
'<a id="xss" href="javascript:alert(document.cookie)>Click me</a>',
|
||||
'<a id="xss" href="javascript: alert(document.cookie)>Click me</a>'
|
||||
];
|
||||
|
||||
$this->asEditor();
|
||||
$page = Page::first();
|
||||
|
||||
foreach ($checks as $check) {
|
||||
$page->html = $check;
|
||||
$page->save();
|
||||
|
||||
$pageView = $this->get($page->getUrl());
|
||||
$pageView->assertStatus(200);
|
||||
$pageView->assertElementNotContains('.page-content', '<a id="xss">');
|
||||
$pageView->assertElementNotContains('.page-content', 'href=javascript:');
|
||||
}
|
||||
}
|
||||
public function test_form_actions_with_javascript_are_removed()
|
||||
{
|
||||
$checks = [
|
||||
'<form><input id="xss" type=submit formaction=javascript:alert(document.domain) value=Submit><input></form>',
|
||||
'<form ><button id="xss" formaction=javascript:alert(document.domain)>Click me</button></form>',
|
||||
'<form id="xss" action=javascript:alert(document.domain)><input type=submit value=Submit></form>'
|
||||
];
|
||||
|
||||
$this->asEditor();
|
||||
$page = Page::first();
|
||||
|
||||
foreach ($checks as $check) {
|
||||
$page->html = $check;
|
||||
$page->save();
|
||||
|
||||
$pageView = $this->get($page->getUrl());
|
||||
$pageView->assertStatus(200);
|
||||
$pageView->assertElementNotContains('.page-content', '<button id="xss"');
|
||||
$pageView->assertElementNotContains('.page-content', '<input id="xss"');
|
||||
$pageView->assertElementNotContains('.page-content', '<form id="xss"');
|
||||
$pageView->assertElementNotContains('.page-content', 'action=javascript:');
|
||||
$pageView->assertElementNotContains('.page-content', 'formaction=javascript:');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_metadata_redirects_are_removed()
|
||||
{
|
||||
$checks = [
|
||||
'<meta http-equiv="refresh" content="0; url=//external_url">',
|
||||
];
|
||||
|
||||
$this->asEditor();
|
||||
$page = Page::first();
|
||||
|
||||
foreach ($checks as $check) {
|
||||
$page->html = $check;
|
||||
$page->save();
|
||||
|
||||
$pageView = $this->get($page->getUrl());
|
||||
$pageView->assertStatus(200);
|
||||
$pageView->assertElementNotContains('.page-content', '<meta>');
|
||||
$pageView->assertElementNotContains('.page-content', '</meta>');
|
||||
$pageView->assertElementNotContains('.page-content', 'content=');
|
||||
$pageView->assertElementNotContains('.page-content', 'external_url');
|
||||
}
|
||||
}
|
||||
public function test_page_inline_on_attributes_removed_by_default()
|
||||
{
|
||||
$this->asEditor();
|
||||
@@ -243,6 +328,23 @@ class PageContentTest extends TestCase
|
||||
$this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
|
||||
}
|
||||
|
||||
public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save()
|
||||
{
|
||||
$this->asEditor();
|
||||
$page = Page::first();
|
||||
|
||||
$content = '<h1 id="non-standard-id">test</h1><p><a href="#non-standard-id">link</a></p>';
|
||||
$this->put($page->getUrl(), [
|
||||
'name' => $page->name,
|
||||
'html' => $content,
|
||||
'summary' => ''
|
||||
]);
|
||||
|
||||
$updatedPage = Page::where('id', '=', $page->id)->first();
|
||||
$this->assertStringContainsString('id="bkmrk-test"', $updatedPage->html);
|
||||
$this->assertStringContainsString('href="#bkmrk-test"', $updatedPage->html);
|
||||
}
|
||||
|
||||
public function test_get_page_nav_sets_correct_properties()
|
||||
{
|
||||
$content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';
|
||||
|
||||
67
tests/Permissions/ExportPermissionsTest.php
Normal file
67
tests/Permissions/ExportPermissionsTest.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php namespace Tests\Permissions;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Chapter;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExportPermissionsTest extends TestCase
|
||||
{
|
||||
|
||||
public function test_page_content_without_view_access_hidden_on_chapter_export()
|
||||
{
|
||||
$chapter = Chapter::query()->first();
|
||||
$page = $chapter->pages()->firstOrFail();
|
||||
$pageContent = Str::random(48);
|
||||
$page->html = '<p>' . $pageContent . '</p>';
|
||||
$page->save();
|
||||
$viewer = $this->getViewer();
|
||||
$this->actingAs($viewer);
|
||||
$formats = ['html', 'plaintext'];
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$resp = $this->get($chapter->getUrl("export/{$format}"));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertSee($pageContent);
|
||||
}
|
||||
|
||||
$this->setEntityRestrictions($page, []);
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$resp = $this->get($chapter->getUrl("export/{$format}"));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertDontSee($page->name);
|
||||
$resp->assertDontSee($pageContent);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_page_content_without_view_access_hidden_on_book_export()
|
||||
{
|
||||
$book = Book::query()->first();
|
||||
$page = $book->pages()->firstOrFail();
|
||||
$pageContent = Str::random(48);
|
||||
$page->html = '<p>' . $pageContent . '</p>';
|
||||
$page->save();
|
||||
$viewer = $this->getViewer();
|
||||
$this->actingAs($viewer);
|
||||
$formats = ['html', 'plaintext'];
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$resp = $this->get($book->getUrl("export/{$format}"));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($page->name);
|
||||
$resp->assertSee($pageContent);
|
||||
}
|
||||
|
||||
$this->setEntityRestrictions($page, []);
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$resp = $this->get($book->getUrl("export/{$format}"));
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertDontSee($page->name);
|
||||
$resp->assertDontSee($pageContent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -490,6 +490,22 @@ class RestrictionsTest extends BrowserKitTest
|
||||
->dontSee($page->name);
|
||||
}
|
||||
|
||||
public function test_restricted_chapter_pages_not_visible_on_book_page()
|
||||
{
|
||||
$chapter = Chapter::query()->first();
|
||||
$this->actingAs($this->user)
|
||||
->visit($chapter->book->getUrl())
|
||||
->see($chapter->pages->first()->name);
|
||||
|
||||
foreach ($chapter->pages as $page) {
|
||||
$this->setEntityRestrictions($page, []);
|
||||
}
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->visit($chapter->book->getUrl())
|
||||
->dontSee($chapter->pages->first()->name);
|
||||
}
|
||||
|
||||
public function test_bookshelf_update_restriction_override()
|
||||
{
|
||||
$shelf = Bookshelf::first();
|
||||
|
||||
@@ -3,39 +3,51 @@
|
||||
use BookStack\Uploads\Attachment;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\TestCase;
|
||||
use Tests\TestResponse;
|
||||
|
||||
class AttachmentTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Get a test file that can be uploaded
|
||||
* @param $fileName
|
||||
* @return \Illuminate\Http\UploadedFile
|
||||
*/
|
||||
protected function getTestFile($fileName)
|
||||
protected function getTestFile(string $fileName): UploadedFile
|
||||
{
|
||||
return new \Illuminate\Http\UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
|
||||
return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file with the given name.
|
||||
* @param $name
|
||||
* @param int $uploadedTo
|
||||
* @return \Illuminate\Foundation\Testing\TestResponse
|
||||
*/
|
||||
protected function uploadFile($name, $uploadedTo = 0)
|
||||
protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Foundation\Testing\TestResponse
|
||||
{
|
||||
$file = $this->getTestFile($name);
|
||||
return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new attachment
|
||||
*/
|
||||
protected function createAttachment(Page $page): Attachment
|
||||
{
|
||||
$this->post('attachments/link', [
|
||||
'attachment_link_url' => 'https://example.com',
|
||||
'attachment_link_name' => 'Example Attachment Link',
|
||||
'attachment_link_uploaded_to' => $page->id,
|
||||
]);
|
||||
|
||||
return Attachment::query()->latest()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all uploaded files.
|
||||
* To assist with cleanup.
|
||||
*/
|
||||
protected function deleteUploads()
|
||||
{
|
||||
$fileService = $this->app->make(\BookStack\Uploads\AttachmentService::class);
|
||||
$fileService = $this->app->make(AttachmentService::class);
|
||||
foreach (Attachment::all() as $file) {
|
||||
$fileService->deleteFile($file);
|
||||
}
|
||||
@@ -145,21 +157,14 @@ class AttachmentTest extends TestCase
|
||||
$page = Page::first();
|
||||
$this->asAdmin();
|
||||
|
||||
$this->call('POST', 'attachments/link', [
|
||||
'attachment_link_url' => 'https://example.com',
|
||||
'attachment_link_name' => 'Example Attachment Link',
|
||||
'attachment_link_uploaded_to' => $page->id,
|
||||
]);
|
||||
|
||||
$attachmentId = Attachment::first()->id;
|
||||
|
||||
$update = $this->call('PUT', 'attachments/' . $attachmentId, [
|
||||
$attachment = $this->createAttachment($page);
|
||||
$update = $this->call('PUT', 'attachments/' . $attachment->id, [
|
||||
'attachment_edit_name' => 'My new attachment name',
|
||||
'attachment_edit_url' => 'https://test.example.com'
|
||||
]);
|
||||
|
||||
$expectedData = [
|
||||
'id' => $attachmentId,
|
||||
'id' => $attachment->id,
|
||||
'path' => 'https://test.example.com',
|
||||
'name' => 'My new attachment name',
|
||||
'uploaded_to' => $page->id
|
||||
@@ -242,4 +247,45 @@ class AttachmentTest extends TestCase
|
||||
|
||||
$this->deleteUploads();
|
||||
}
|
||||
|
||||
public function test_data_and_js_links_cannot_be_attached_to_a_page()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asAdmin();
|
||||
|
||||
$badLinks = [
|
||||
'javascript:alert("bunny")',
|
||||
' javascript:alert("bunny")',
|
||||
'JavaScript:alert("bunny")',
|
||||
"\t\n\t\nJavaScript:alert(\"bunny\")",
|
||||
"data:text/html;<a></a>",
|
||||
"Data:text/html;<a></a>",
|
||||
"Data:text/html;<a></a>",
|
||||
];
|
||||
|
||||
foreach ($badLinks as $badLink) {
|
||||
$linkReq = $this->post('attachments/link', [
|
||||
'attachment_link_url' => $badLink,
|
||||
'attachment_link_name' => 'Example Attachment Link',
|
||||
'attachment_link_uploaded_to' => $page->id,
|
||||
]);
|
||||
$linkReq->assertStatus(422);
|
||||
$this->assertDatabaseMissing('attachments', [
|
||||
'path' => $badLink,
|
||||
]);
|
||||
}
|
||||
|
||||
$attachment = $this->createAttachment($page);
|
||||
|
||||
foreach ($badLinks as $badLink) {
|
||||
$linkReq = $this->put('attachments/' . $attachment->id, [
|
||||
'attachment_edit_url' => $badLink,
|
||||
'attachment_edit_name' => 'Example Attachment Link',
|
||||
]);
|
||||
$linkReq->assertStatus(422);
|
||||
$this->assertDatabaseMissing('attachments', [
|
||||
'path' => $badLink,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class DrawioTest extends TestCase
|
||||
$editor = $this->getEditor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
|
||||
$resp->assertSee('drawio-url="https://www.draw.io/?embed=1&proto=json&spin=1"');
|
||||
$resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&proto=json&spin=1"');
|
||||
|
||||
config()->set('services.drawio', false);
|
||||
$resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
|
||||
|
||||
Reference in New Issue
Block a user