mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-10 11:19:37 +03:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa8553839b | ||
|
|
b8fcefc794 | ||
|
|
2eafd8335c | ||
|
|
e2f9089f56 | ||
|
|
ef459ca4c4 | ||
|
|
fb80bb5d58 | ||
|
|
88c698796b | ||
|
|
88bcb68fcb | ||
|
|
7c000553ae | ||
|
|
d815e1b9f2 | ||
|
|
492af79c27 | ||
|
|
253f386f00 | ||
|
|
fd44e4ba74 | ||
|
|
040997fdc4 | ||
|
|
5e6092aaf8 | ||
|
|
391fa35c80 | ||
|
|
c6773a8c9f | ||
|
|
a579b7da21 | ||
|
|
bc34914ac1 | ||
|
|
7028025380 | ||
|
|
ff494be952 |
3
.github/translators.txt
vendored
3
.github/translators.txt
vendored
@@ -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
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
45
app/Http/Middleware/ApplyCspRules.php
Normal file
45
app/Http/Middleware/ApplyCspRules.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Theming/CustomHtmlHeadContentProvider.php
Normal file
64
app/Theming/CustomHtmlHeadContentProvider.php
Normal 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
96
app/Util/CspService.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
app/Util/HtmlNonceApplicator.php
Normal file
63
app/Util/HtmlNonceApplicator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
public/dist/export-styles.css
vendored
2
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles.css
vendored
2
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
];
|
||||
@@ -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.',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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ł',
|
||||
|
||||
@@ -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' => 'прокомментировал',
|
||||
|
||||
@@ -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' => '多重身份认证已设置,请使用新配置的方法重新登录。',
|
||||
];
|
||||
@@ -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' => '无法上传文件。 服务器可能不接受此大小的文件。',
|
||||
|
||||
@@ -145,6 +145,7 @@ body.flexbox {
|
||||
.flex {
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
&.fit-content {
|
||||
flex-basis: auto;
|
||||
flex-grow: 0;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
66
tests/Settings/CustomHeadContentTest.php
Normal file
66
tests/Settings/CustomHeadContentTest.php
Normal 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="<script text>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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Settings;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class FooterLinksTest extends TestCase
|
||||
Reference in New Issue
Block a user