Compare commits

...

62 Commits

Author SHA1 Message Date
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
2acef3c2ec Fixed issue where restricted page content in plaintext export
The content of pages made non-viewable to a user via permissions, within a visible parent, could be seen via the plaintext export option. Before v0.30.6 this would have applied only to scenarios where all pages within the chapter were made non-visible. In v0.30.6 this would make all pages within the chapter visible.

As per #2414
2020-12-18 13:56:00 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
3f3fad7113 Fixed book-tree-gen page visibility issue
When book trees were generated, pages in chapters where ALL pages within
were not supposed to be visibile, would be visible due to the code
falling back on the raw relation which would not account for
permissions.

This has now been changed so that a custom 'visible_pages' attribute is set and used by any book tree structures, to ensure it does not fall back to the raw relation.

Added an extra test to cover.

For #2414
2020-12-17 17:31:18 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
884664bfe9 Ensured base64 images are read from image upload folder
Also removed unused storage systems and updated testing.
2020-12-06 15:34:18 +00:00
Dan Brown
8911e3f441 Removed http fetching from image base64 generation 2020-12-06 14:24:22 +00:00
Dan Brown
7d38c96a23 Removed generic "UploadService" which was doing very little 2020-12-06 12:58:40 +00:00
Dan Brown
162d893143 Updated .env.example to encorage use of setting APP_URL
For the purposes of secure URL generation and to avoid common problems
found when people are using reverse proxies.
2020-12-06 12:31:36 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
6d8b0605a0 Merge branch 'xss_and_redir_patch' of git://github.com/PercussiveElbow/BookStack into xss_and_redirect 2020-10-31 15:19:33 +00:00
Dan Brown
349162ea13 Prevented possible XSS via link attachments
This filters out potentially malicious javascript: or data: uri's coming
through to be attached to attachments.
Added tests to cover.

Thanks to Yassine ABOUKIR (@yassineaboukir on twitter) for reporting this
vulnerability.
2020-10-31 15:01:52 +00:00
PercussiveElbow
bbd1384acb XSS and redirect fixes with test cases 2020-10-27 01:34:51 +00:00
Dan Brown
6aa2bf9e27 Merge pull request #2296 from timoschwarzer/esbuild-watch-first-time-fix
Fix build:js:watch not building at first launch in Docker
2020-10-13 23:17:23 +01:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
18bcafaee4 Updated translator attribution before release v0.30.3 2020-10-13 22:49:55 +01:00
Dan Brown
8d07b7cf1c Added alias for vbscript 2020-10-13 22:44:33 +01:00
Dan Brown
080f9c3025 Merge pull request #2302 from nutsflag/master
Add VBScript Codemirror
2020-10-13 22:41:09 +01:00
Dan Brown
617fe6bc8c Merge pull request #2303 from BookStackApp/l10n_master
New Crowdin updates
2020-10-13 22:39:52 +01:00
Dan Brown
bb1f1a9ecd Fixed error on drawing edit on markdown editor
Was preventing save of drawings.
For #2313
2020-10-13 22:36:07 +01:00
Dan Brown
d688e43197 New translations settings.php (Chinese Simplified) 2020-10-05 06:26:38 +01:00
Dan Brown
c82c3023c5 New translations settings.php (Spanish) 2020-10-02 17:18:27 +01:00
Dan Brown
d0d75afc66 New translations settings.php (Chinese Simplified) 2020-10-02 15:55:46 +01:00
nutsflag
467176ee78 Update code.js 2020-10-02 15:14:29 +02:00
nutsflag
521a002001 Update code-editor.blade.php 2020-10-02 15:13:31 +02:00
Timo Schwarzer
aca37b8784 Fix build:js:watch not building at first launch in Docker 2020-10-01 11:25:22 +02:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
f3ee8f2d4c Updated http service to not read 204 response data 2020-09-30 22:32:03 +01:00
Dan Brown
ea406690f5 Updated esbuild options and version & updated npm deps
Had to change way sortable is imported due to changes, Still
seemed to have functioning multi-select.
2020-09-30 22:28:53 +01:00
Dan Brown
465d405926 Updated page content related links on content id changes
For #2278
2020-09-28 22:26:50 +01:00
Dan Brown
1097c61d6d Fixed duplicate requests in attachment manager issue
Closes #2286
2020-09-28 21:55:24 +01:00
Dan Brown
def2d61ad8 Merge pull request #2272 from jakubboucek/feature/fix-invalid-canonical-redirect
Fixed canonical redirects on non-root url app instances
2020-09-28 21:15:23 +01:00
Dan Brown
8b0f5e7000 Updated draw.io references to diagrams.net
Related to #2044
2020-09-28 20:45:38 +01:00
Jakub Bouček
1e88e8086f Fixed canonical redirects on non-root url app instances
If BookStack instance is deployed to any non-root path, e.g. http://example.com/wiki/,
requests for http://example.com/wiki/shelves/
was redirected to http://example.com/shelves
instead of http://example.com/wiki/shelves

Synced with: https://github.com/laravel/laravel/blob/master/public/.htaccess
2020-09-27 02:50:37 +02:00
Dan Brown
d48ac0a37d Removed redundant test
Now replaced in recent commit by one that checks actual message gets
displayed on the redirect page.
Redirect page changed to login page.
2020-09-26 18:24:05 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
3eeb1e7d08 Updated translators fiel with latest 2020-09-26 17:48:02 +01:00
Dan Brown
0d43b50f9d New Crowdin updates (#2262)
* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Czech)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations settings.php (Czech)

* New translations errors.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (German)

* New translations settings.php (German)

* New translations entities.php (German)

* New translations validation.php (Czech)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (German Informal)

* New translations settings.php (German Informal)

* New translations auth.php (Czech)
2020-09-26 17:46:32 +01:00
Dan Brown
6bcfac6751 Updated codemirror and updated codemirror base styles
Aligns styles with current release, since was causing overflow
with scrollbars.

Fixes #2267
2020-09-26 17:33:43 +01:00
Dan Brown
68489e5b44 Updated PR code to use isA and updated that function definition
Related to #2227
2020-09-26 17:00:17 +01:00
Dan Brown
fe0e307313 Merge branch 'renderpages' of git://github.com/mr-vinn/BookStack into mr-vinn-renderpages 2020-09-26 16:55:05 +01:00
Dan Brown
9985046685 Added test for includes on book export
Related to #2227
2020-09-26 16:54:24 +01:00
Dan Brown
53ec794e53 Fixed issue where SAML login not notifiy on existing user
Added testing to cover

Fixes #2263
2020-09-26 16:43:06 +01:00
Dan Brown
328d2514c4 Updated settings nav to be more flexible
Uses flexbox layout, flexed to content instead of rigid thirds like
before. Also extracted row into own file
2020-09-26 16:26:30 +01:00
Dan Brown
de2756dd95 Updated callout links to be correct colors
- Also updated to be underlined instead of bold
2020-09-26 15:40:51 +01:00
Dan Brown
1f97047799 Merge branch 'master' of git://github.com/alexmannuk/BookStack into alexmannuk-master 2020-09-26 15:35:13 +01:00
Dan Brown
c870c10e38 Merge pull request #2270 from gertjankrol/feature/test-migrations-workflow
Add `test-migrations` workflow
2020-09-26 15:25:17 +01:00
Dan Brown
49fa21c1e2 Merge pull request #2268 from gertjankrol/master
Fix the `AddActivityIndexes` migration's `down()` method
2020-09-26 15:21:21 +01:00
Dan Brown
9f87423584 Merge pull request #2274 from abulgatz/patch-1
Fixed "Ubunto Mono" $mono type misspelling
2020-09-26 12:11:53 +01:00
Dan Brown
08fbd39fcb Fixed markdown iframe loading and content alignment
Fixes #2280
2020-09-26 12:01:01 +01:00
Adam
5f75a9f32c Fix "Ubunto Mono" $mono type misspelling 2020-09-23 16:19:30 -05:00
Gertjan Krol
3750922c3e Added the test-migrations workflow 2020-09-22 19:53:45 +02:00
Gertjan Krol
4b0d1ddf39 Fixed the AddActivityIndexes migration's down() method 2020-09-22 19:22:27 +02:00
Vinnie Okada
2c3f453c1f Implement the renderPages parameter
Render page content when getTree() is called with a true $renderPages
argument.
2020-09-07 09:05:51 -06:00
alexmannuk
7837b8c4ee Updated callout link formatting
Updated callout links to use font colouring based on type, with bold text to denote link, instead of using the theme link colour per issue #303.
2020-08-24 20:03:08 +01:00
76 changed files with 902 additions and 623 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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',
],
],
];

View File

@@ -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.
*/

View File

@@ -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);
}

View File

@@ -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')) {

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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); ?>";

View File

@@ -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;
}

View File

@@ -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);
}
};

View File

@@ -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');

View File

@@ -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);
}
}

View File

@@ -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');
});
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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)

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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();

View File

@@ -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!'
];
];

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.'
];
];

View File

@@ -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',

View File

@@ -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í 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 ne 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',

View File

@@ -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',
],
],

View File

@@ -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',

View File

@@ -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)',

View File

@@ -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',

View File

@@ -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)',

View File

@@ -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.',

View File

@@ -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?',

View File

@@ -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',

View File

@@ -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' => 'Перетащите файлы или нажмите здесь, чтобы загрузить и перезаписать',

View File

@@ -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' => 'Роли',

View File

@@ -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' => '删除文件或点击这里上传并覆盖',

View File

@@ -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)',

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }}"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"/>

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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);

View File

@@ -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"');
}
}

View File

@@ -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>';

View 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);
}
}
}

View File

@@ -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();

View File

@@ -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,
]);
}
}
}

View File

@@ -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&amp;proto=json&amp;spin=1"');
$resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1"');
config()->set('services.drawio', false);
$resp = $this->actingAs($editor)->get($page->getUrl('/edit'));

View File

@@ -1 +1 @@
v0.30.0
v0.30.7