Compare commits

..

21 Commits

Author SHA1 Message Date
Dan Brown
fa8553839b Updated version and assets for release v21.08.3 2021-09-12 16:31:02 +01:00
Dan Brown
b8fcefc794 Merge branch 'master' into release 2021-09-12 16:30:35 +01:00
Dan Brown
2eafd8335c Updated translators for v21.08.3 2021-09-12 16:25:33 +01:00
Dan Brown
e2f9089f56 New Crowdin updates (#2915)
* New translations auth.php (Spanish)

* New translations activities.php (Italian)

* New translations settings.php (Italian)

* New translations entities.php (Italian)

* New translations validation.php (Italian)

* New translations activities.php (Danish)

* New translations auth.php (Danish)

* New translations common.php (Danish)

* New translations settings.php (Danish)

* New translations entities.php (Danish)

* New translations auth.php (Danish)

* New translations common.php (Danish)

* New translations errors.php (Danish)

* New translations validation.php (Danish)

* New translations activities.php (Russian)

* New translations auth.php (French)

* New translations auth.php (French)

* New translations settings.php (French)

* New translations entities.php (French)

* New translations auth.php (French)
2021-09-12 16:25:05 +01:00
Dan Brown
ef459ca4c4 Altered the parsing of custom head to prevent htmlentities on content
Was causing things like emjoi within script content to be somewhat
mangled. Instead we force UTF8 only parsing via XML declaration.

Added test to cover.

For #2923
2021-09-12 16:19:17 +01:00
Dan Brown
fb80bb5d58 Applied latest styleci changes 2021-09-06 22:19:06 +01:00
Dan Brown
88c698796b Fixed issue with HTML tags in custom head scripts
Fixes a strange issue of HTML tags within script tags being malformed
when part of the HTML custom head content due to the PHP parsing we do.
DOMDocument seemed to cause this upon load.
Adding LIBXML_SCHEMA_CREATE to the ->loadHTML call seems to fix this but
not really sure why. Doesn't seem to cause further issues though.
Tested with multiple scripts and styles and comments and meta tags.

- Also added new testing class to cover.
- As part of testing, added new folder within tests to house setting
  specific tests.

For #2914
2021-09-05 23:52:39 +01:00
Dan Brown
88bcb68fcb Updated version and assets for release v21.08.2 2021-09-04 15:07:20 +01:00
Dan Brown
7c000553ae Merge branch 'master' into release 2021-09-04 15:06:33 +01:00
Dan Brown
d815e1b9f2 Merge branch 'html-filtering' 2021-09-04 14:53:46 +01:00
Dan Brown
492af79c27 Added a couple of additional CSP rules
As per guidance from google's CSP evaluator.
2021-09-04 14:34:43 +01:00
Dan Brown
253f386f00 Finished off script CSP rules
- Added caching for custom html head parsing to add nonce.
- Also moved api docs page into web routes to prevent issues.
2021-09-04 13:57:04 +01:00
Dan Brown
fd44e4ba74 Started application of CSP headers 2021-09-03 23:32:42 +01:00
Dan Brown
040997fdc4 Added filter for xlink:href svg xss
Simply remove all such attributes
2021-09-03 22:34:49 +01:00
Dan Brown
5e6092aaf8 Added extra HTML filtering of dangerous content
In particular, That around the casing of dangerous values within
attributes. This uses some xpath translation to handle different casing
in contains searching.
2021-09-02 22:02:30 +01:00
Dan Brown
391fa35c80 Updated version and assets for release v21.08.1 2021-09-02 21:13:09 +01:00
Dan Brown
c6773a8c9f Merge branch 'master' into release 2021-09-02 21:12:06 +01:00
Dan Brown
a579b7da21 Updated translator attribution before release v21.08.1 2021-09-02 21:11:23 +01:00
Dan Brown
bc34914ac1 New Crowdin updates (#2906)
* New translations auth.php (Chinese Simplified)

* New translations auth.php (Chinese Simplified)

* New translations validation.php (Chinese Simplified)

* New translations activities.php (Latvian)

* New translations auth.php (Latvian)

* New translations common.php (Latvian)

* New translations validation.php (Latvian)

* New translations entities.php (Latvian)

* New translations activities.php (Polish)
2021-09-02 21:07:31 +01:00
Dan Brown
7028025380 Made the TOTP URL visible during setup
Useful for some non-scanner type apps.
Closes #2908
2021-09-01 20:58:19 +01:00
Dan Brown
ff494be952 Fixed lack of proper ordering of pages
Added test to cover
Fixes #2905
2021-09-01 20:30:02 +01:00
53 changed files with 654 additions and 208 deletions

View File

@@ -183,3 +183,6 @@ A Ibnu Hibban (abd.ibnuhibban) :: Indonesian
Frost-ZX :: Chinese Simplified
Kuzma Simonov (ovmach) :: Russian
Vojtěch Krystek (acantophis) :: Czech
Michał Lipok (mLipok) :: Polish
Nicolas Pawlak (Mikolajek) :: French
Thomas Hansen (thomasdk81) :: Danish

View File

@@ -25,8 +25,8 @@ use Permissions;
*/
class Page extends BookChild
{
public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at'];
public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at'];
public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at', 'priority'];
public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at', 'priority'];
protected $fillable = ['name', 'priority', 'markdown'];

View File

@@ -35,8 +35,8 @@ class MfaTotpController extends Controller
$svg = $totp->generateQrCodeSvg($qrCodeUrl);
return view('mfa.totp-generate', [
'secret' => $totpSecret,
'svg' => $svg,
'url' => $qrCodeUrl,
'svg' => $svg,
]);
}

View File

@@ -24,7 +24,7 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\BookStack\Http\Middleware\ControlIframeSecurity::class,
\BookStack\Http\Middleware\ApplyCspRules::class,
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,

View File

@@ -0,0 +1,45 @@
<?php
namespace BookStack\Http\Middleware;
use BookStack\Util\CspService;
use Closure;
use Illuminate\Http\Request;
class ApplyCspRules
{
/**
* @var CspService
*/
protected $cspService;
public function __construct(CspService $cspService)
{
$this->cspService = $cspService;
}
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
view()->share('cspNonce', $this->cspService->getNonce());
if ($this->cspService->allowedIFrameHostsConfigured()) {
config()->set('session.same_site', 'none');
}
$response = $next($request);
$this->cspService->setFrameAncestors($response);
$this->cspService->setScriptSrc($response);
$this->cspService->setObjectSrc($response);
$this->cspService->setBaseUri($response);
return $response;
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace BookStack\Http\Middleware;
use Closure;
/**
* Sets CSP headers to restrict the hosts that BookStack can be
* iframed within. Also adjusts the cookie samesite options
* so that cookies will operate in the third-party context.
*/
class ControlIframeSecurity
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
*
* @return mixed
*/
public function handle($request, Closure $next)
{
$iframeHosts = collect(explode(' ', config('app.iframe_hosts', '')))->filter();
if ($iframeHosts->count() > 0) {
config()->set('session.same_site', 'none');
}
$iframeHosts->prepend("'self'");
$response = $next($request);
$cspValue = 'frame-ancestors ' . $iframeHosts->join(' ');
$response->headers->set('Content-Security-Policy', $cspValue);
return $response;
}
}

View File

@@ -12,6 +12,7 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use BookStack\Util\CspService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\View;
@@ -71,5 +72,9 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(SocialAuthService::class, function ($app) {
return new SocialAuthService($app->make(SocialiteFactory::class), $app->make(LoginService::class));
});
$this->app->singleton(CspService::class, function ($app) {
return new CspService();
});
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace BookStack\Theming;
use BookStack\Util\CspService;
use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlNonceApplicator;
use Illuminate\Contracts\Cache\Repository as Cache;
class CustomHtmlHeadContentProvider
{
/**
* @var CspService
*/
protected $cspService;
/**
* @var Cache
*/
protected $cache;
public function __construct(CspService $cspService, Cache $cache)
{
$this->cspService = $cspService;
$this->cache = $cache;
}
/**
* Fetch our custom HTML head content prepared for use on web pages.
* Content has a nonce applied for CSP.
*/
public function forWeb(): string
{
$content = $this->getSourceContent();
$hash = md5($content);
$html = $this->cache->remember('custom-head-web:' . $hash, 86400, function () use ($content) {
return HtmlNonceApplicator::prepare($content);
});
return HtmlNonceApplicator::apply($html, $this->cspService->getNonce());
}
/**
* Fetch our custom HTML head content prepared for use in export formats.
* Scripts are stripped to avoid potential issues.
*/
public function forExport(): string
{
$content = $this->getSourceContent();
$hash = md5($content);
return $this->cache->remember('custom-head-export:' . $hash, 86400, function () use ($content) {
return HtmlContentFilter::removeScripts($content);
});
}
/**
* Get the original custom head content to use.
*/
protected function getSourceContent(): string
{
return setting('app-custom-head', '');
}
}

96
app/Util/CspService.php Normal file
View File

@@ -0,0 +1,96 @@
<?php
namespace BookStack\Util;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class CspService
{
/** @var string */
protected $nonce;
public function __construct(string $nonce = '')
{
$this->nonce = $nonce ?: Str::random(24);
}
/**
* Get the nonce value for CSP.
*/
public function getNonce(): string
{
return $this->nonce;
}
/**
* Sets CSP 'script-src' headers to restrict the forms of script that can
* run on the page.
*/
public function setScriptSrc(Response $response)
{
if (config('app.allow_content_scripts')) {
return;
}
$parts = [
'http:',
'https:',
'\'nonce-' . $this->nonce . '\'',
'\'strict-dynamic\'',
];
$value = 'script-src ' . implode(' ', $parts);
$response->headers->set('Content-Security-Policy', $value, false);
}
/**
* Sets CSP "frame-ancestors" headers to restrict the hosts that BookStack can be
* iframed within. Also adjusts the cookie samesite options so that cookies will
* operate in the third-party context.
*/
public function setFrameAncestors(Response $response)
{
$iframeHosts = $this->getAllowedIframeHosts();
array_unshift($iframeHosts, "'self'");
$cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts);
$response->headers->set('Content-Security-Policy', $cspValue, false);
}
/**
* Check if the user has configured some allowed iframe hosts.
*/
public function allowedIFrameHostsConfigured(): bool
{
return count($this->getAllowedIframeHosts()) > 0;
}
/**
* Sets CSP 'object-src' headers to restrict the types of dynamic content
* that can be embedded on the page.
*/
public function setObjectSrc(Response $response)
{
if (config('app.allow_content_scripts')) {
return;
}
$response->headers->set('Content-Security-Policy', 'object-src \'self\'', false);
}
/**
* Sets CSP 'base-uri' headers to restrict what base tags can be set on
* the page to prevent manipulation of relative links.
*/
public function setBaseUri(Response $response)
{
$response->headers->set('Content-Security-Policy', 'base-uri \'self\'', false);
}
protected function getAllowedIframeHosts(): array
{
$hosts = config('app.iframe_hosts', '');
return array_filter(explode(' ', $hosts));
}
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Util;
use DOMAttr;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
@@ -9,7 +10,7 @@ use DOMXPath;
class HtmlContentFilter
{
/**
* Remove all of the script elements from the given HTML.
* Remove all the script elements from the given HTML.
*/
public static function removeScripts(string $html): string
{
@@ -28,28 +29,29 @@ class HtmlContentFilter
static::removeNodes($scriptElems);
// Remove clickable links to JavaScript URI
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
$badLinks = $xPath->query('//*[' . static::xpathContains('@href', 'javascript:') . ']');
static::removeNodes($badLinks);
// Remove forms with calls to JavaScript URI
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
$badForms = $xPath->query('//*[' . static::xpathContains('@action', 'javascript:') . '] | //*[' . static::xpathContains('@formaction', 'javascript:') . ']');
static::removeNodes($badForms);
// Remove meta tag to prevent external redirects
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
$metaTags = $xPath->query('//meta[' . static::xpathContains('@content', 'url') . ']');
static::removeNodes($metaTags);
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
$badIframes = $xPath->query('//*[' . static::xpathContains('@src', 'data:') . '] | //*[' . static::xpathContains('@src', 'javascript:') . '] | //*[@srcdoc]');
static::removeNodes($badIframes);
// Remove elements with a xlink:href attribute
// Used in SVG but deprecated anyway, so we'll be a bit more heavy-handed here.
$xlinkHrefAttributes = $xPath->query('//@*[contains(name(), \'xlink:href\')]');
static::removeAttributes($xlinkHrefAttributes);
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr */
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
static::removeAttributes($onAttributes);
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
@@ -61,7 +63,19 @@ class HtmlContentFilter
}
/**
* Removed all of the given DOMNodes.
* Create a xpath contains statement with a translation automatically built within
* to affectively search in a cases-insensitive manner.
*/
protected static function xpathContains(string $property, string $value): string
{
$value = strtolower($value);
$upperVal = strtoupper($value);
return 'contains(translate(' . $property . ', \'' . $upperVal . '\', \'' . $value . '\'), \'' . $value . '\')';
}
/**
* Remove all the given DOMNodes.
*/
protected static function removeNodes(DOMNodeList $nodes): void
{
@@ -69,4 +83,16 @@ class HtmlContentFilter
$node->parentNode->removeChild($node);
}
}
/**
* Remove all the given attribute nodes.
*/
protected static function removeAttributes(DOMNodeList $attrs): void
{
/** @var DOMAttr $attr */
foreach ($attrs as $attr) {
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace BookStack\Util;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use DOMXPath;
class HtmlNonceApplicator
{
protected static $placeholder = '[CSP_NONCE_VALUE]';
/**
* Prepare the given HTML content with nonce attributes including a placeholder
* value which we can target later.
*/
public static function prepare(string $html): string
{
if (empty($html)) {
return $html;
}
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML($html, LIBXML_SCHEMA_CREATE);
$xPath = new DOMXPath($doc);
// Apply to scripts
$scriptElems = $xPath->query('//script');
static::addNonceAttributes($scriptElems, static::$placeholder);
// Apply to styles
$styleElems = $xPath->query('//style');
static::addNonceAttributes($styleElems, static::$placeholder);
$returnHtml = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$content = $doc->saveHTML($child);
$returnHtml .= $content;
}
return $returnHtml;
}
/**
* Apply the give nonce value to the given prepared HTML.
*/
public static function apply(string $html, string $nonce): string
{
return str_replace(static::$placeholder, $nonce, $html);
}
protected static function addNonceAttributes(DOMNodeList $nodes, string $attrValue): void
{
/** @var DOMElement $node */
foreach ($nodes as $node) {
$node->setAttribute('nonce', $attrValue);
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -44,12 +44,12 @@ return [
'bookshelf_delete_notification' => 'Bogreolen blev opdateret',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
'favourite_add_notification' => '":name" er blevet tilføjet til dine favoritter',
'favourite_remove_notification' => '":name" er blevet fjernet fra dine favoritter',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Multi-faktor metode konfigureret',
'mfa_remove_method_notification' => 'Multi-faktor metode fjernet',
// Other
'commented_on' => 'kommenterede til',

View File

@@ -76,19 +76,19 @@ return [
'user_invite_success' => 'Adgangskode indstillet, du har nu adgang til :appName!',
// Multi-factor Authentication
'mfa_setup' => 'Setup Multi-Factor Authentication',
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'mfa_setup_configured' => 'Already configured',
'mfa_setup_reconfigure' => 'Reconfigure',
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
'mfa_setup_action' => 'Setup',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_option_totp_title' => 'Mobile App',
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Backup Codes',
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
'mfa_setup' => 'Opsætning af Multi-faktor godkendelse',
'mfa_setup_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',
'mfa_setup_configured' => 'Allerede konfigureret',
'mfa_setup_reconfigure' => 'Genkonfigurer',
'mfa_setup_remove_confirmation' => 'Er du sikker på, at du vil fjerne denne multi-faktor godkendelsesmetode?',
'mfa_setup_action' => 'Opsætning',
'mfa_backup_codes_usage_limit_warning' => 'Du har mindre end 5 backup koder tilbage, generere og gem et nyt sæt før du løber tør for koder, for at forhindre at blive lukket ude af din konto.',
'mfa_option_totp_title' => 'Mobil app',
'mfa_option_totp_desc' => 'For at bruge multi-faktor godkendelse, skal du bruge en mobil app, der understøtter TOTP såsom Google Authenticator, Authy eller Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Backup koder',
'mfa_option_backup_codes_desc' => 'Gem sikkert et sæt af engangs backup koder, som du kan indtaste for at bekræfte din identitet.',
'mfa_gen_confirm_and_enable' => 'Bekræft og aktivér',
'mfa_gen_backup_codes_title' => 'Backup koder opsætning',
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
'mfa_gen_backup_codes_download' => 'Download Codes',
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',

View File

@@ -39,12 +39,12 @@ return [
'reset' => 'Nulstil',
'remove' => 'Fjern',
'add' => 'Tilføj',
'configure' => 'Configure',
'configure' => 'Konfigurer',
'fullscreen' => 'Fuld skærm',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
'favourite' => 'Foretrukken',
'unfavourite' => 'Fjern som foretrukken',
'next' => 'Næste',
'previous' => 'Forrige',
// Sort Options
'sort_options' => 'Sorteringsindstillinger',
@@ -61,7 +61,7 @@ return [
'no_activity' => 'Ingen aktivitet at vise',
'no_items' => 'Intet indhold tilgængeligt',
'back_to_top' => 'Tilbage til toppen',
'skip_to_main_content' => 'Skip to main content',
'skip_to_main_content' => 'Spring til indhold',
'toggle_details' => 'Vis/skjul detaljer',
'toggle_thumbnails' => 'Vis/skjul miniaturer',
'details' => 'Detaljer',

View File

@@ -27,8 +27,8 @@ return [
'images' => 'Billeder',
'my_recent_drafts' => 'Mine seneste kladder',
'my_recently_viewed' => 'Mine senest viste',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'my_most_viewed_favourites' => 'Mine mest viste favoritter',
'my_favourites' => 'Mine favoritter',
'no_pages_viewed' => 'Du har ikke besøgt nogle sider',
'no_pages_recently_created' => 'Ingen sider er blevet oprettet for nyligt',
'no_pages_recently_updated' => 'Ingen sider er blevet opdateret for nyligt',
@@ -36,7 +36,7 @@ return [
'export_html' => 'Indeholdt webfil',
'export_pdf' => 'PDF-fil',
'export_text' => 'Almindelig tekstfil',
'export_md' => 'Markdown File',
'export_md' => 'Markdown Fil',
// Permissions and restrictions
'permissions' => 'Rettigheder',
@@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Reoltilladelser',
'shelves_permissions_updated' => 'Reoltilladelser opdateret',
'shelves_permissions_active' => 'Aktive reoltilladelser',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_cascade_warning' => 'Tilladelser på reoler nedarves ikke automatisk til indeholdte bøger. Dette skyldes, at en bog kan eksistere på flere hylder. Tilladelser kan dog kopieres ned til underliggende bøger ved hjælp af muligheden, der findes nedenfor.',
'shelves_copy_permissions_to_books' => 'Kopier tilladelser til bøger',
'shelves_copy_permissions' => 'Kopier tilladelser',
'shelves_copy_permissions_explain' => 'Dette vil anvende de aktuelle tilladelsesindstillinger på denne boghylde på alle bøger indeholdt i. Før aktivering skal du sikre dig, at ændringer i tilladelserne til denne boghylde er blevet gemt.',

View File

@@ -83,9 +83,9 @@ return [
'404_page_not_found' => 'Siden blev ikke fundet',
'sorry_page_not_found' => 'Beklager, siden du leder efter blev ikke fundet.',
'sorry_page_not_found_permission_warning' => 'Hvis du forventede, at denne side skulle eksistere, har du muligvis ikke tilladelse til at se den.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'image_not_found' => 'Billede ikke fundet',
'image_not_found_subtitle' => 'Beklager, billedet du ledte efter kunne ikke findes.',
'image_not_found_details' => 'Hvis du forventede, at dette billede skulle eksistere, kan det være blevet slettet.',
'return_home' => 'Gå tilbage til hjem',
'error_occurred' => 'Der opstod en fejl',
'app_down' => ':appName er nede lige nu',

View File

@@ -92,7 +92,7 @@ return [
'recycle_bin' => 'Papirkurv',
'recycle_bin_desc' => 'Her kan du gendanne elementer, der er blevet slettet eller vælge at permanent fjerne dem fra systemet. Denne liste er ufiltreret, i modsætning til lignende aktivitetslister i systemet, hvor tilladelsesfiltre anvendes.',
'recycle_bin_deleted_item' => 'Slettet element',
'recycle_bin_deleted_parent' => 'Parent',
'recycle_bin_deleted_parent' => 'Overordnet',
'recycle_bin_deleted_by' => 'Slettet af',
'recycle_bin_deleted_at' => 'Sletningstidspunkt',
'recycle_bin_permanently_delete' => 'Slet permanent',
@@ -105,7 +105,7 @@ return [
'recycle_bin_restore_list' => 'Elementer der skal gendannes',
'recycle_bin_restore_confirm' => 'Denne handling vil gendanne det slettede element, herunder alle underelementer, til deres oprindelige placering. Hvis den oprindelige placering siden er blevet slettet, og nu er i papirkurven, vil det overordnede element også skulle gendannes.',
'recycle_bin_restore_deleted_parent' => 'Det overordnede element til dette element er også blevet slettet. Disse vil forblive slettet indtil det overordnede også er gendannet.',
'recycle_bin_restore_parent' => 'Restore Parent',
'recycle_bin_restore_parent' => 'Gendan Overordnet',
'recycle_bin_destroy_notification' => 'Slettede :count elementer fra papirkurven.',
'recycle_bin_restore_notification' => 'Gendannede :count elementer fra papirkurven.',
@@ -138,7 +138,7 @@ return [
'role_details' => 'Rolledetaljer',
'role_name' => 'Rollenavn',
'role_desc' => 'Kort beskrivelse af rolle',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_mfa_enforced' => 'Kræver multifaktor godkendelse',
'role_external_auth_id' => 'Eksterne godkendelses-IDer',
'role_system' => 'Systemtilladelser',
'role_manage_users' => 'Administrere brugere',
@@ -148,7 +148,7 @@ return [
'role_manage_page_templates' => 'Administrer side-skabeloner',
'role_access_api' => 'Tilgå system-API',
'role_manage_settings' => 'Administrer app-indstillinger',
'role_export_content' => 'Export content',
'role_export_content' => 'Eksporter indhold',
'role_asset' => 'Tilladelser for medier og "assets"',
'roles_system_warning' => 'Vær opmærksom på, at adgang til alle af de ovennævnte tre tilladelser, kan give en bruger mulighed for at ændre deres egne brugerrettigheder eller brugerrettigheder for andre i systemet. Tildel kun roller med disse tilladelser til betroede brugere.',
'role_asset_desc' => 'Disse tilladelser kontrollerer standardadgang til medier og "assets" i systemet. Tilladelser til bøger, kapitler og sider tilsidesætter disse tilladelser.',
@@ -206,10 +206,10 @@ return [
'users_api_tokens_create' => 'Opret Token',
'users_api_tokens_expires' => 'Udløber',
'users_api_tokens_docs' => 'API-dokumentation',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users_mfa' => 'Multi-faktor godkendelse',
'users_mfa_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.',
'users_mfa_x_methods' => ':count metode konfigureret|:count metoder konfigureret',
'users_mfa_configure' => 'Konfigurer metoder',
// API Tokens
'user_api_token_create' => 'Opret API-token',

View File

@@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute må kun bestå af bogstaver, tal, binde- og under-streger.',
'alpha_num' => ':attribute må kun indeholde bogstaver og tal.',
'array' => ':attribute skal være et array.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'Den angivne kode er ikke gyldig eller er allerede brugt.',
'before' => ':attribute skal være en dato før :date.',
'between' => [
'numeric' => ':attribute skal være mellem :min og :max.',
@@ -99,7 +99,7 @@ return [
],
'string' => ':attribute skal være tekst.',
'timezone' => ':attribute skal være en gyldig zone.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'Den angivne kode er ikke gyldig eller er udløbet.',
'unique' => ':attribute er allerede i brug.',
'url' => ':attribute-formatet er ugyldigt.',
'uploaded' => 'Filen kunne ikke oploades. Serveren accepterer muligvis ikke filer af denne størrelse.',

View File

@@ -80,11 +80,11 @@ return [
'mfa_setup_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta de usuario.',
'mfa_setup_configured' => 'Ya está configurado',
'mfa_setup_reconfigure' => 'Reconfigurar',
'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación de dos pasos?',
'mfa_setup_remove_confirmation' => '¿Está seguro de que desea eliminar este método de autenticación en dos pasos?',
'mfa_setup_action' => 'Configuración',
'mfa_backup_codes_usage_limit_warning' => 'Quedan menos de 5 códigos de respaldo, Por favor, genera y almacena un nuevo conjunto antes de que te quedes sin códigos para evitar que te bloquees fuera de tu cuenta.',
'mfa_option_totp_title' => 'Aplicación para móviles',
'mfa_option_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
'mfa_option_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Códigos de Respaldo',
'mfa_option_backup_codes_desc' => 'Almacena de forma segura un conjunto de códigos de respaldo de un solo uso que puedes introducir para verificar tu identidad.',
'mfa_gen_confirm_and_enable' => 'Confirmar y Activar',
@@ -93,7 +93,7 @@ return [
'mfa_gen_backup_codes_download' => 'Descargar Códigos',
'mfa_gen_backup_codes_usage_warning' => 'Cada código sólo puede utilizarse una vez',
'mfa_gen_totp_title' => 'Configuración de Aplicación móvil',
'mfa_gen_totp_desc' => 'Para utilizar la autenticación de dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
'mfa_gen_totp_desc' => 'Para utilizar la autenticación en dos pasos necesitarás una aplicación móvil que soporte TOTP como Google Authenticator, Authy o Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Escanea el código QR mostrado a continuación usando tu aplicación de autenticación preferida para empezar.',
'mfa_gen_totp_verify_setup' => 'Verificar Configuración',
'mfa_gen_totp_verify_setup_desc' => 'Verifica que todo está funcionando introduciendo un código, generado en tu aplicación de autenticación, en el campo de texto a continuación:',
@@ -101,7 +101,7 @@ return [
'mfa_verify_access' => 'Verificar Acceso',
'mfa_verify_access_desc' => 'Tu cuenta de usuario requiere que confirmes tu identidad a través de un nivel adicional de verificación antes de que te conceda el acceso. Verifica tu identidad usando uno de los métodos configurados para continuar.',
'mfa_verify_no_methods' => 'No hay Métodos Configurados',
'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación de dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',
'mfa_verify_no_methods_desc' => 'No se han encontrado métodos de autenticación en dos pasos para tu cuenta. Tendrás que configurar al menos un método antes de obtener acceso.',
'mfa_verify_use_totp' => 'Verificar usando una aplicación móvil',
'mfa_verify_use_backup_codes' => 'Verificar usando un código de respaldo',
'mfa_verify_backup_code' => 'Códigos de Respaldo',

View File

@@ -82,31 +82,31 @@ return [
'mfa_setup_reconfigure' => 'Reconfigurer',
'mfa_setup_remove_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette méthode d\'authentification multi-facteurs ?',
'mfa_setup_action' => 'Configuration',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_backup_codes_usage_limit_warning' => 'Il vous reste moins de 5 codes de secours, veuillez générer et stocker un nouveau jeu de codes afin d\'éviter tout verrouillage de votre compte.',
'mfa_option_totp_title' => 'Application mobile',
'mfa_option_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'Backup Codes',
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
'mfa_option_backup_codes_title' => 'Codes de secours',
'mfa_option_backup_codes_desc' => 'Stockez en toute sécurité un jeu de codes de secours que vous pourrez utiliser pour vérifier votre identité.',
'mfa_gen_confirm_and_enable' => 'Confirmer et activer',
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
'mfa_gen_backup_codes_title' => 'Configuration des codes de secours',
'mfa_gen_backup_codes_desc' => 'Stockez la liste des codes ci-dessous dans un endroit sûr. Lorsque vous accédez au système, vous pourrez utiliser l\'un des codes comme un deuxième mécanisme d\'authentification.',
'mfa_gen_backup_codes_download' => 'Télécharger le code',
'mfa_gen_backup_codes_usage_warning' => 'Chaque code ne peut être utilisé qu\'une seule fois',
'mfa_gen_totp_title' => 'Configuration de l\'application mobile',
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
'mfa_gen_totp_desc' => 'Pour utiliser l\'authentification multi-facteurs, vous aurez besoin d\'une application mobile qui supporte TOTP comme Google Authenticator, Authy ou Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Scannez le QR code ci-dessous avec votre application d\'authentification préférée pour débuter.',
'mfa_gen_totp_verify_setup' => 'Vérifier la configuration',
'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
'mfa_gen_totp_verify_setup_desc' => 'Vérifiez que tout fonctionne en utilisant un code généré par votre application d\'authentification, dans la zone ci-dessous :',
'mfa_gen_totp_provide_code_here' => 'Fournir le code généré par votre application ici',
'mfa_verify_access' => 'Vérifier l\'accès',
'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
'mfa_verify_access_desc' => 'Votre compte d\'utilisateur vous demande de confirmer votre identité par un niveau supplémentaire de vérification avant que vous n\'ayez accès. Vérifiez-la en utilisant l\'une de vos méthodes configurées pour continuer.',
'mfa_verify_no_methods' => 'Aucune méthode configurée',
'mfa_verify_no_methods_desc' => 'Aucune méthode d\'authentification multi-facteurs n\'a pu être trouvée pour votre compte. Vous devez configurer au moins une méthode avant d\'obtenir l\'accès.',
'mfa_verify_use_totp' => 'Vérifier à l\'aide d\'une application mobile',
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
'mfa_verify_backup_code' => 'Backup Code',
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
'mfa_verify_use_backup_codes' => 'Vérifier en utilisant un code de secours',
'mfa_verify_backup_code' => 'Code de secours',
'mfa_verify_backup_code_desc' => 'Entrez l\'un de vos codes de secours restants ci-dessous :',
'mfa_verify_backup_code_enter_here' => 'Saisissez un code de secours ici',
'mfa_verify_totp_desc' => 'Entrez ci-dessous le code généré à l\'aide de votre application mobile :',
'mfa_setup_login_notification' => 'Méthode multi-facteurs configurée. Veuillez maintenant vous reconnecter en utilisant la méthode configurée.',
];

View File

@@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Permissions de l\'étagère',
'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour',
'shelves_permissions_active' => 'Permissions de l\'étagère activées',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_cascade_warning' => 'Les permissions sur les étagères ne sont pas automatiquement recopiées aux livres qu\'elles contiennent, car un livre peut exister dans plusieurs étagères. Les permissions peuvent cependant être recopiées vers les livres contenus en utilisant l\'option ci-dessous.',
'shelves_copy_permissions_to_books' => 'Copier les permissions vers les livres',
'shelves_copy_permissions' => 'Copier les permissions',
'shelves_copy_permissions_explain' => 'Ceci va appliquer les permissions actuelles de cette étagère à tous les livres qu\'elle contient. Avant de continuer, assurez-vous que toutes les permissions de cette étagère ont été sauvegardées.',

View File

@@ -38,7 +38,7 @@ return [
'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.',
'app_homepage_select' => 'Choisissez une page',
'app_footer_links' => 'Liens de pied de page',
'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécesittent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::<key>" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".',
'app_footer_links_desc' => 'Ajouter des liens à afficher dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, y compris ceux qui ne nécessitent pas de connexion. Vous pouvez utiliser une étiquette de "trans::<key>" pour utiliser les traductions définies par le système. Par exemple : utiliser "trans::common.privacy_policy" fournira le texte traduit "Privacy Policy" et "trans::common.terms_of_service" fournira le texte traduit "Terms of Service".',
'app_footer_links_label' => 'Libellé du lien',
'app_footer_links_url' => 'URL du lien',
'app_footer_links_add' => 'Ajouter un lien en pied de page',
@@ -148,7 +148,7 @@ return [
'role_manage_page_templates' => 'Gérer les modèles de page',
'role_access_api' => 'Accès à l\'API du système',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_export_content' => 'Export content',
'role_export_content' => 'Exporter le contenu',
'role_asset' => 'Permissions des ressources',
'roles_system_warning' => 'Sachez que l\'accès à l\'une des trois permissions ci-dessus peut permettre à un utilisateur de modifier ses propres privilèges ou les privilèges des autres utilisateurs du système. Attribuer uniquement des rôles avec ces permissions à des utilisateurs de confiance.',
'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
@@ -178,7 +178,7 @@ return [
'users_send_invite_option' => 'Envoyer l\'e-mail d\'invitation',
'users_external_auth_id' => 'Identifiant d\'authentification externe',
'users_external_auth_id_desc' => 'C\'est l\'ID utilisé pour correspondre à cet utilisateur lors de la communication avec votre système d\'authentification externe.',
'users_password_warning' => 'Remplissez ce formulaire uniquement si vous souhaitez changer de mot de passe:',
'users_password_warning' => 'Remplissez ce formulaire uniquement si vous souhaitez changer de mot de passe :',
'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
@@ -208,7 +208,7 @@ return [
'users_api_tokens_docs' => 'Documentation de l\'API',
'users_mfa' => 'Authentification multi-facteurs',
'users_mfa_desc' => 'Configurez l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées',
'users_mfa_configure' => 'Méthode de configuration',
// API Tokens

View File

@@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" è stato rimosso dai tuoi preferiti',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Metodo multi-fattore impostato con successo',
'mfa_remove_method_notification' => 'Metodo multi-fattore rimosso con successo',
// Other
'commented_on' => 'ha commentato in',

View File

@@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Permessi Libreria',
'shelves_permissions_updated' => 'Permessi Libreria Aggiornati',
'shelves_permissions_active' => 'Permessi Attivi Libreria',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_cascade_warning' => 'I permessi sugli scaffali non si estendono automaticamente ai libri contenuti. Questo avviene in quanto un libro può essere presente su più scaffali. I permessi possono comunque essere copiati ai libri contenuti usando l\'opzione qui sotto.',
'shelves_copy_permissions_to_books' => 'Copia Permessi ai Libri',
'shelves_copy_permissions' => 'Copia Permessi',
'shelves_copy_permissions_explain' => 'Verranno applicati tutti i permessi della libreria ai libri contenuti. Prima di attivarlo, assicurati che ogni permesso di questa libreria sia salvato.',

View File

@@ -138,7 +138,7 @@ return [
'role_details' => 'Dettagli Ruolo',
'role_name' => 'Nome Ruolo',
'role_desc' => 'Breve Descrizione del Ruolo',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_mfa_enforced' => 'Richiesta autenticazione multi-fattore',
'role_external_auth_id' => 'ID Autenticazione Esterna',
'role_system' => 'Permessi di Sistema',
'role_manage_users' => 'Gestire gli utenti',
@@ -148,7 +148,7 @@ return [
'role_manage_page_templates' => 'Gestisci template pagine',
'role_access_api' => 'API sistema d\'accesso',
'role_manage_settings' => 'Gestire impostazioni app',
'role_export_content' => 'Export content',
'role_export_content' => 'Esporta contenuto',
'role_asset' => 'Permessi Entità',
'roles_system_warning' => 'Siate consapevoli che l\'accesso a uno dei tre permessi qui sopra, può consentire a un utente di modificare i propri privilegi o i privilegi di altri nel sistema. Assegna ruoli con questi permessi solo ad utenti fidati.',
'role_asset_desc' => 'Questi permessi controllano l\'accesso di default alle entità. I permessi nei Libri, Capitoli e Pagine sovrascriveranno questi.',
@@ -206,10 +206,10 @@ return [
'users_api_tokens_create' => 'Crea Token',
'users_api_tokens_expires' => 'Scade',
'users_api_tokens_docs' => 'Documentazione API',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users_mfa' => 'Autenticazione multi-fattore',
'users_mfa_desc' => 'Imposta l\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.',
'users_mfa_x_methods' => ':count metodo configurato|:count metodi configurati',
'users_mfa_configure' => 'Configura metodi',
// API Tokens
'user_api_token_create' => 'Crea Token API',

View File

@@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute deve contenere solo lettere, numeri e meno.',
'alpha_num' => ':attribute deve contenere solo lettere e numeri.',
'array' => ':attribute deve essere un array.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'Il codice fornito non è valido o è già stato utilizzato.',
'before' => ':attribute deve essere una data prima del :date.',
'between' => [
'numeric' => 'Il campo :attribute deve essere tra :min e :max.',
@@ -99,7 +99,7 @@ return [
],
'string' => ':attribute deve essere una stringa.',
'timezone' => ':attribute deve essere una zona valida.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'Il codice fornito non è valido o è scaduto.',
'unique' => ':attribute è già preso.',
'url' => 'Il formato :attribute non è valido.',
'uploaded' => 'Il file non può essere caricato. Il server potrebbe non accettare file di questa dimensione.',

View File

@@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" ir izņemts no jūsu favorītiem',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => '2FA funkcija aktivizēta',
'mfa_remove_method_notification' => '2FA funkcija noņemta',
// Other
'commented_on' => 'komentēts',

View File

@@ -76,11 +76,11 @@ return [
'user_invite_success' => 'Parole iestatīta, tagad varat piekļūt :appName!',
// Multi-factor Authentication
'mfa_setup' => 'Setup Multi-Factor Authentication',
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'mfa_setup_configured' => 'Already configured',
'mfa_setup_reconfigure' => 'Reconfigure',
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
'mfa_setup' => 'Iestati divfaktoru autentifikāciju (2FA)',
'mfa_setup_desc' => 'Iestati divfaktoru autentifikāciju kā papildus drošību tavam lietotāja kontam.',
'mfa_setup_configured' => 'Divfaktoru autentifikācija jau ir nokonfigurēta',
'mfa_setup_reconfigure' => 'Mainīt 2FA konfigurāciju',
'mfa_setup_remove_confirmation' => 'Vai esi drošs, ka vēlies noņemt divfaktoru autentifikāciju?',
'mfa_setup_action' => 'Setup',
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'mfa_option_totp_title' => 'Mobile App',

View File

@@ -39,7 +39,7 @@ return [
'reset' => 'Atiestatīt',
'remove' => 'Noņemt',
'add' => 'Pievienot',
'configure' => 'Configure',
'configure' => 'Mainīt konfigurāciju',
'fullscreen' => 'Pilnekrāns',
'favourite' => 'Pievienot favorītiem',
'unfavourite' => 'Noņemt no favorītiem',

View File

@@ -99,7 +99,7 @@ return [
'shelves_permissions' => 'Grāmatplaukta atļaujas',
'shelves_permissions_updated' => 'Grāmatplaukta atļaujas atjauninātas',
'shelves_permissions_active' => 'Grāmatplaukta atļaujas ir aktīvas',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_cascade_warning' => 'Grāmatu plauktu atļaujas netiek automātiski pārvietotas uz grāmatām. Tas ir tāpēc, ka grāmata var atrasties vairākos plauktos. Tomēr atļaujas var nokopēt uz plauktam pievienotajām grāmatām, izmantojot zemāk norādīto opciju.',
'shelves_copy_permissions_to_books' => 'Kopēt grāmatplaukta atļaujas uz grāmatām',
'shelves_copy_permissions' => 'Kopēt atļaujas',
'shelves_copy_permissions_explain' => 'Šis piemēros pašreizējās grāmatplaukta piekļuves tiesības visām tajā esošajām grāmatām. Pirms ieslēgšanas pārliecinieties, ka ir saglabātas izmaiņas grāmatplaukta piekļuves tiesībām.',

View File

@@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute var saturēt tikai burtus, ciparus, domuzīmes un apakš svītras.',
'alpha_num' => ':attribute var saturēt tikai burtus un ciparus.',
'array' => ':attribute ir jābūt masīvam.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'Ievadītais kods nav derīgs vai arī jau ir izmantots.',
'before' => ':attribute jābūt datumam pirms :date.',
'between' => [
'numeric' => ':attribute jābūt starp :min un :max.',
@@ -99,7 +99,7 @@ return [
],
'string' => ':attribute jābūt teksta virknei.',
'timezone' => ':attribute jābūt derīgai zonai.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'Ievadītais kods nav derīgs.',
'unique' => ':attribute jau ir aizņemts.',
'url' => ':attribute formāts nav derīgs.',
'uploaded' => 'Fails netika ielādēts. Serveris nevar pieņemt šāda izmēra failus.',

View File

@@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" został usunięty z ulubionych',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Metoda wieloskładnikowa została pomyślnie skonfigurowana',
'mfa_remove_method_notification' => 'Metoda wieloskładnikowa pomyślnie usunięta',
// Other
'commented_on' => 'skomentował',

View File

@@ -48,8 +48,8 @@ return [
'favourite_remove_notification' => '":name" удалено из избранного',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Двухфакторный метод авторизации успешно настроен',
'mfa_remove_method_notification' => 'Двухфакторный метод авторизации успешно удален',
// Other
'commented_on' => 'прокомментировал',

View File

@@ -26,7 +26,7 @@ return [
'remember_me' => '记住我',
'ldap_email_hint' => '请输入用于此帐户的电子邮件。',
'create_account' => '创建账户',
'already_have_account' => '已经有账号?',
'already_have_account' => '已经有账号',
'dont_have_account' => '您还没有账号吗?',
'social_login' => 'SNS登录',
'social_registration' => '使用社交网站账号注册',
@@ -82,31 +82,31 @@ return [
'mfa_setup_reconfigure' => '重新配置',
'mfa_setup_remove_confirmation' => '您确定想要移除多重身份认证吗?',
'mfa_setup_action' => '设置',
'mfa_backup_codes_usage_limit_warning' => '您剩余的备用证码少于 5 个,请在用完证码之前生成并保存新的证码,以防止您的帐户被锁定。',
'mfa_backup_codes_usage_limit_warning' => '您剩余的备用证码少于 5 个,请在用完证码之前生成并保存新的证码,以防止您的帐户被锁定。',
'mfa_option_totp_title' => '移动设备 App',
'mfa_option_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP基于时间的一次性密码算法 的移动设备 App如谷歌身份验证器Google Authenticator、Authy 或微软身份验证器Microsoft Authenticator。',
'mfa_option_backup_codes_title' => '备用证码',
'mfa_option_backup_codes_desc' => '安全地保存一组一次性使用的备用证码,您可以输入这些证码来验证您的身份。',
'mfa_option_backup_codes_title' => '备用证码',
'mfa_option_backup_codes_desc' => '安全地保存这些一次性使用的备用证码,您可以输入这些证码来验证您的身份。',
'mfa_gen_confirm_and_enable' => '确认并启用',
'mfa_gen_backup_codes_title' => '备用证码设置',
'mfa_gen_backup_codes_desc' => '将下面的证码存放在一个安全的地方。访问系统时,您可以使用其中的一个验证码进行二次认证。',
'mfa_gen_backup_codes_download' => '下载证码',
'mfa_gen_backup_codes_usage_warning' => '每个证码只能使用一次',
'mfa_gen_backup_codes_title' => '备用证码设置',
'mfa_gen_backup_codes_desc' => '将下面的证码存放在一个安全的地方。访问系统时,您可以使用其中的一个验证码进行二次认证。',
'mfa_gen_backup_codes_download' => '下载证码',
'mfa_gen_backup_codes_usage_warning' => '每个证码只能使用一次',
'mfa_gen_totp_title' => '移动设备 App',
'mfa_gen_totp_desc' => '要使用多重身份认证功能,您需要一个支持 TOTP基于时间的一次性密码算法 的移动设备 App如谷歌身份验证器Google Authenticator、Authy 或微软身份验证器Microsoft Authenticator。',
'mfa_gen_totp_scan' => '要开始操作,请使用你的身份验证 App 扫描下面的二维码。',
'mfa_gen_totp_verify_setup' => '验证设置',
'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的证码来验证一切是否正常:',
'mfa_gen_totp_provide_code_here' => '此输入您的 App 生成的证码',
'mfa_gen_totp_verify_setup_desc' => '请在下面的框中输入您在身份验证 App 中生成的证码来验证一切是否正常:',
'mfa_gen_totp_provide_code_here' => '此输入您的 App 生成的证码',
'mfa_verify_access' => '认证访问',
'mfa_verify_access_desc' => '您的账户要求您在访问前通过额外的验证确认您的身份。使用您设置的认证方法认证以继续。',
'mfa_verify_no_methods' => '没有设置认证方法',
'mfa_verify_no_methods_desc' => '您的账户没有设置多重身份认证。您需要至少设置一种才能访问。',
'mfa_verify_use_totp' => '使用移动设备 App 进行认证',
'mfa_verify_use_backup_codes' => '使用备用证码进行认证',
'mfa_verify_backup_code' => '备用证码',
'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用证码:',
'mfa_verify_backup_code_enter_here' => '在这里输入备用证码',
'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的证码:',
'mfa_verify_use_backup_codes' => '使用备用证码进行认证',
'mfa_verify_backup_code' => '备用证码',
'mfa_verify_backup_code_desc' => '在下面输入你的其中一个备用证码:',
'mfa_verify_backup_code_enter_here' => '在这里输入备用证码',
'mfa_verify_totp_desc' => '在下面输入您的移动 App 生成的证码:',
'mfa_setup_login_notification' => '多重身份认证已设置,请使用新配置的方法重新登录。',
];

View File

@@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute 只能包含字母、数字和短横线。',
'alpha_num' => ':attribute 只能包含字母和数字。',
'array' => ':attribute 必须是一个数组。',
'backup_codes' => '您输入的证码无效或已被使用。',
'backup_codes' => '您输入的证码无效或已被使用。',
'before' => ':attribute 必须是在 :date 前的日期。',
'between' => [
'numeric' => ':attribute 必须在:min到:max之间。',
@@ -99,7 +99,7 @@ return [
],
'string' => ':attribute 必须是字符串。',
'timezone' => ':attribute 必须是有效的区域。',
'totp' => '您输入的证码无效或已过期。',
'totp' => '您输入的证码无效或已过期。',
'unique' => ':attribute 已经被使用。',
'url' => ':attribute 格式无效。',
'uploaded' => '无法上传文件。 服务器可能不接受此大小的文件。',

View File

@@ -145,6 +145,7 @@ body.flexbox {
.flex {
min-height: 0;
flex: 1;
max-width: 100%;
&.fit-content {
flex-basis: auto;
flex-grow: 0;

View File

@@ -1,5 +1,7 @@
@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
@if(setting('app-custom-head') && \Route::currentRouteName() !== 'settings')
<!-- Custom user content -->
{!! setting('app-custom-head') !!}
{!! $headContent->forWeb() !!}
<!-- End custom user content -->
@endif

View File

@@ -1,5 +1,7 @@
@inject('headContent', 'BookStack\Theming\CustomHtmlHeadContentProvider')
@if(setting('app-custom-head'))
<!-- Custom user content -->
{!! \BookStack\Util\HtmlContentFilter::removeScripts(setting('app-custom-head')) !!}
{!! $headContent->forExport() !!}
<!-- End custom user content -->
@endif

View File

@@ -15,7 +15,6 @@
<meta property="og:title" content="{{ isset($pageTitle) ? $pageTitle . ' | ' : '' }}{{ setting('app-name') }}">
<meta property="og:url" content="{{ url()->current() }}">
@stack('social-meta')
<!-- Styles and Fonts -->
<link rel="stylesheet" href="{{ versioned_asset('dist/styles.css') }}">
@@ -51,7 +50,7 @@
</div>
@yield('bottom')
<script src="{{ versioned_asset('dist/app.js') }}"></script>
<script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
@yield('scripts')
</body>

View File

@@ -12,6 +12,9 @@
<div class="block inline">
{!! $svg !!}
</div>
<div class="code-base small text-muted px-s py-xs my-xs" style="overflow-x: scroll; white-space: nowrap;">
{{ $url }}
</div>
</div>
<h2 class="list-heading">{{ trans('auth.mfa_gen_totp_verify_setup') }}</h2>

View File

@@ -1,7 +1,7 @@
@extends('layouts.base')
@section('head')
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}"></script>
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}" nonce="{{ $cspNonce }}"></script>
@stop
@section('body-class', 'flexbox')

View File

@@ -5,7 +5,6 @@
* Routes have a uri prefix of /api/.
* Controllers are all within app/Http/Controllers/Api.
*/
Route::get('docs', 'ApiDocsController@display');
Route::get('docs.json', 'ApiDocsController@json');
Route::get('books', 'BookApiController@list');

View File

@@ -10,6 +10,9 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/uploads/images/{path}', 'Images\ImageController@showImage')
->where('path', '.*$');
// API docs routes
Route::get('/api/docs', 'Api\ApiDocsController@display');
Route::get('/pages/recently-updated', 'PageController@showRecentlyUpdated');
// Shelves

View File

@@ -2,7 +2,6 @@
namespace Tests\Api;
use BookStack\Auth\User;
use Tests\TestCase;
class ApiDocsTest extends TestCase
@@ -11,16 +10,6 @@ class ApiDocsTest extends TestCase
protected $endpoint = '/api/docs';
public function test_docs_page_not_visible_to_normal_viewers()
{
$viewer = $this->getViewer();
$resp = $this->actingAs($viewer)->get($this->endpoint);
$resp->assertStatus(403);
$resp = $this->actingAsApiEditor()->get($this->endpoint);
$resp->assertStatus(200);
}
public function test_docs_page_returns_view_with_docs_content()
{
$resp = $this->actingAsApiEditor()->get($this->endpoint);
@@ -42,19 +31,4 @@ class ApiDocsTest extends TestCase
]],
]);
}
public function test_docs_page_visible_by_public_user_if_given_permission()
{
$this->setSettings(['app-public' => true]);
$guest = User::getDefault();
$this->startSession();
$resp = $this->get('/api/docs');
$resp->assertStatus(403);
$this->giveUserPermissions($guest, ['access-api']);
$resp = $this->get('/api/docs');
$resp->assertStatus(200);
}
}

View File

@@ -36,10 +36,12 @@ class MfaConfigurationTest extends TestCase
$resp->assertSee('The provided code is not valid or has expired.');
$revisitSvg = $resp->getElementHtml('#main-content .card svg');
$this->assertTrue($svg === $revisitSvg);
$secret = decrypt(session()->get('mfa-setup-totp-secret'));
$resp->assertSee(htmlentities("?secret={$secret}&issuer=BookStack&algorithm=SHA1&digits=6&period=30"));
// Successful confirmation
$google2fa = new Google2FA();
$secret = decrypt(session()->get('mfa-setup-totp-secret'));
$otp = $google2fa->getCurrentOtp($secret);
$resp = $this->post('/mfa/totp/confirm', [
'code' => $otp,

View File

@@ -135,14 +135,26 @@ class PageContentTest extends TestCase
}
}
public function test_iframe_js_and_base64_urls_are_removed()
public function test_js_and_base64_src_urls_are_removed()
{
$checks = [
'<iframe src="javascript:alert(document.cookie)"></iframe>',
'<iframe src="JavAScRipT:alert(document.cookie)"></iframe>',
'<iframe src="JavAScRipT:alert(document.cookie)"></iframe>',
'<iframe SRC=" javascript: alert(document.cookie)"></iframe>',
'<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<iframe src="DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<iframe src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<img src="javascript:alert(document.cookie)"/>',
'<img src="JavAScRipT:alert(document.cookie)"/>',
'<img src="JavAScRipT:alert(document.cookie)"/>',
'<img SRC=" javascript: alert(document.cookie)"/>',
'<img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
'<img src="DaTa:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
'<img src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg=="/>',
'<iframe srcdoc="<script>window.alert(document.cookie)</script>"></iframe>',
'<iframe SRCdoc="<script>window.alert(document.cookie)</script>"></iframe>',
'<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>',
];
$this->asEditor();
@@ -155,6 +167,7 @@ class PageContentTest extends TestCase
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', '<iframe>');
$pageView->assertElementNotContains('.page-content', '<img');
$pageView->assertElementNotContains('.page-content', '</iframe>');
$pageView->assertElementNotContains('.page-content', 'src=');
$pageView->assertElementNotContains('.page-content', 'javascript:');
@@ -168,6 +181,8 @@ class PageContentTest extends TestCase
$checks = [
'<a id="xss" href="javascript:alert(document.cookie)>Click me</a>',
'<a id="xss" href="javascript: alert(document.cookie)>Click me</a>',
'<a id="xss" href="JaVaScRiPt: alert(document.cookie)>Click me</a>',
'<a id="xss" href=" JaVaScRiPt: alert(document.cookie)>Click me</a>',
];
$this->asEditor();
@@ -179,7 +194,7 @@ class PageContentTest extends TestCase
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', '<a id="xss">');
$pageView->assertElementNotContains('.page-content', '<a id="xss"');
$pageView->assertElementNotContains('.page-content', 'href=javascript:');
}
}
@@ -188,8 +203,10 @@ class PageContentTest extends TestCase
{
$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 ><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>',
'<form id="xss" action="JaVaScRiPt:alert(document.domain)"><input type=submit value=Submit></form>',
];
$this->asEditor();
@@ -213,6 +230,8 @@ class PageContentTest extends TestCase
{
$checks = [
'<meta http-equiv="refresh" content="0; url=//external_url">',
'<meta http-equiv="refresh" ConTeNt="0; url=//external_url">',
'<meta http-equiv="refresh" content="0; UrL=//external_url">',
];
$this->asEditor();
@@ -249,11 +268,13 @@ class PageContentTest extends TestCase
{
$checks = [
'<p onclick="console.log(\'test\')">Hello</p>',
'<p OnCliCk="console.log(\'test\')">Hello</p>',
'<div>Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p>',
'<div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div>',
'<div><div><div><div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div></div></div></div>',
'<div onclick="console.log(\'test\')">Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p><div></div>',
'<a a="<img src=1 onerror=\'alert(1)\'> ',
'\<a onclick="alert(document.cookie)"\>xss link\</a\>',
];
$this->asEditor();
@@ -284,6 +305,28 @@ class PageContentTest extends TestCase
$pageView->assertDontSee('abc123abc123');
}
public function test_svg_xlink_hrefs_are_removed()
{
$checks = [
'<svg id="test" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100"><a xlink:href="javascript:alert(document.domain)"><rect x="0" y="0" width="100" height="100" /></a></svg>',
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="data:application/xml;base64 ,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KPGRlZnM+CjxjaXJjbGUgaWQ9InRlc3QiIHI9IjAiIGN4PSIwIiBjeT0iMCIgc3R5bGU9ImZpbGw6ICNGMDAiPgo8c2V0IGF0dHJpYnV0ZU5hbWU9ImZpbGwiIGF0dHJpYnV0ZVR5cGU9IkNTUyIgb25iZWdpbj0nYWxlcnQoZG9jdW1lbnQuZG9tYWluKScKb25lbmQ9J2FsZXJ0KCJvbmVuZCIpJyB0bz0iIzAwRiIgYmVnaW49IjBzIiBkdXI9Ijk5OXMiIC8+CjwvY2lyY2xlPgo8L2RlZnM+Cjx1c2UgeGxpbms6aHJlZj0iI3Rlc3QiLz4KPC9zdmc+#test"/></svg>',
];
$this->asEditor();
$page = Page::query()->first();
foreach ($checks as $check) {
$page->html = $check;
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', 'alert');
$pageView->assertElementNotContains('.page-content', 'xlink:href');
$pageView->assertElementNotContains('.page-content', 'application/xml');
}
}
public function test_page_inline_on_attributes_show_if_configured()
{
$this->asEditor();

View File

@@ -258,4 +258,25 @@ class SortTest extends TestCase
$checkResp = $this->get(Page::find($checkPage->id)->getUrl());
$checkResp->assertSee($newBook->name);
}
public function test_pages_in_book_show_sorted_by_priority()
{
/** @var Book $book */
$book = Book::query()->whereHas('pages')->first();
$book->chapters()->forceDelete();
/** @var Page[] $pages */
$pages = $book->pages()->where('chapter_id', '=', 0)->take(2)->get();
$book->pages()->whereNotIn('id', $pages->pluck('id'))->delete();
$resp = $this->asEditor()->get($book->getUrl());
$resp->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[0]->name);
$resp->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[1]->name);
$pages[0]->forceFill(['priority' => 10])->save();
$pages[1]->forceFill(['priority' => 5])->save();
$resp = $this->asEditor()->get($book->getUrl());
$resp->assertElementContains('.content-wrap a.page:nth-child(1)', $pages[1]->name);
$resp->assertElementContains('.content-wrap a.page:nth-child(2)', $pages[0]->name);
}
}

View File

@@ -2,7 +2,7 @@
namespace Tests;
use Illuminate\Support\Str;
use BookStack\Util\CspService;
class SecurityHeaderTest extends TestCase
{
@@ -44,26 +44,90 @@ class SecurityHeaderTest extends TestCase
public function test_iframe_csp_self_only_by_default()
{
$resp = $this->get('/');
$cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
$frameHeaders = $cspHeaders->filter(function ($val) {
return Str::startsWith($val, 'frame-ancestors');
});
$frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
$this->assertTrue($frameHeaders->count() === 1);
$this->assertEquals('frame-ancestors \'self\'', $frameHeaders->first());
$this->assertEquals('frame-ancestors \'self\'', $frameHeader);
}
public function test_iframe_csp_includes_extra_hosts_if_configured()
{
$this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'https://a.example.com https://b.example.com', function () {
$resp = $this->get('/');
$cspHeaders = collect($resp->headers->get('Content-Security-Policy'));
$frameHeaders = $cspHeaders->filter(function ($val) {
return Str::startsWith($val, 'frame-ancestors');
});
$frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
$this->assertTrue($frameHeaders->count() === 1);
$this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeaders->first());
$this->assertNotEmpty($frameHeader);
$this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeader);
});
}
public function test_script_csp_set_on_responses()
{
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'script-src');
$this->assertStringContainsString('\'strict-dynamic\'', $scriptHeader);
$this->assertStringContainsString('\'nonce-', $scriptHeader);
}
public function test_script_csp_nonce_matches_nonce_used_in_custom_head()
{
$this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
$resp = $this->get('/login');
$scriptHeader = $this->getCspHeader($resp, 'script-src');
$nonce = app()->make(CspService::class)->getNonce();
$this->assertStringContainsString('nonce-' . $nonce, $scriptHeader);
$resp->assertSee('<script nonce="' . $nonce . '">console.log("cat");</script>');
}
public function test_script_csp_nonce_changes_per_request()
{
$resp = $this->get('/');
$firstHeader = $this->getCspHeader($resp, 'script-src');
$this->refreshApplication();
$resp = $this->get('/');
$secondHeader = $this->getCspHeader($resp, 'script-src');
$this->assertNotEquals($firstHeader, $secondHeader);
}
public function test_allow_content_scripts_settings_controls_csp_script_headers()
{
config()->set('app.allow_content_scripts', true);
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'script-src');
$this->assertEmpty($scriptHeader);
config()->set('app.allow_content_scripts', false);
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'script-src');
$this->assertNotEmpty($scriptHeader);
}
public function test_object_src_csp_header_set()
{
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'object-src');
$this->assertEquals('object-src \'self\'', $scriptHeader);
}
public function test_base_uri_csp_header_set()
{
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'base-uri');
$this->assertEquals('base-uri \'self\'', $scriptHeader);
}
/**
* Get the value of the first CSP header of the given type.
*/
protected function getCspHeader(TestResponse $resp, string $type): string
{
$cspHeaders = collect($resp->headers->all('Content-Security-Policy'));
return $cspHeaders->filter(function ($val) use ($type) {
return strpos($val, $type) === 0;
})->first() ?? '';
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Tests\Settings;
use BookStack\Util\CspService;
use Tests\TestCase;
class CustomHeadContentTest extends TestCase
{
public function test_configured_content_shows_on_pages()
{
$this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
$resp = $this->get('/login');
$resp->assertSee('console.log("cat")');
}
public function test_configured_content_does_not_show_on_settings_page()
{
$this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
$resp = $this->asAdmin()->get('/settings');
$resp->assertDontSee('console.log("cat")');
}
public function test_divs_in_js_preserved_in_configured_content()
{
$this->setSettings(['app-custom-head' => '<script><div id="hello">cat</div></script>']);
$resp = $this->get('/login');
$resp->assertSee('<div id="hello">cat</div>');
}
public function test_nonce_application_handles_edge_cases()
{
$mockCSP = $this->mock(CspService::class);
$mockCSP->shouldReceive('getNonce')->andReturn('abc123');
$content = trim('
<script>console.log("cat");</script>
<script type="text/html"><\script>const a = `<div></div>`<\/\script></script>
<script >const a = `<div></div>`;</script>
<script type="<script text>test">const c = `<div></div>`;</script>
<script
type="text/html"
>
const a = `<\script><\/script>`;
const b = `<script`;
</script>
<SCRIPT>const b = `↗️£`;</SCRIPT>
');
$expectedOutput = trim('
<script nonce="abc123">console.log("cat");</script>
<script type="text/html" nonce="abc123"><\script>const a = `<div></div>`<\/\script></script>
<script nonce="abc123">const a = `<div></div>`;</script>
<script type="&lt;script text&gt;test" nonce="abc123">const c = `<div></div>`;</script>
<script type="text/html" nonce="abc123">
const a = `<\script><\/script>`;
const b = `<script`;
</script>
<script nonce="abc123">const b = `↗️£`;</script>
');
$this->setSettings(['app-custom-head' => $content]);
$resp = $this->get('/login');
$resp->assertSee($expectedOutput);
}
}

View File

@@ -1,5 +1,7 @@
<?php
namespace Tests\Settings;
use Tests\TestCase;
class FooterLinksTest extends TestCase

View File

@@ -1 +1 @@
v21.08
v21.08.3