Merge branch 'development' into release

This commit is contained in:
Dan Brown 2022-03-07 15:12:09 +00:00
commit 882c609296
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
14 changed files with 382 additions and 265 deletions

View File

@ -42,7 +42,7 @@ APP_TIMEZONE=UTC
# overrides can be made. Defaults to disabled. # overrides can be made. Defaults to disabled.
APP_THEME=false APP_THEME=false
# Trusted Proxies # Trusted proxies
# Used to indicate trust of systems that proxy to the application so # Used to indicate trust of systems that proxy to the application so
# certain header values (Such as "X-Forwarded-For") can be used from the # certain header values (Such as "X-Forwarded-For") can be used from the
# incoming proxy request to provide origin detail. # incoming proxy request to provide origin detail.
@ -58,6 +58,13 @@ DB_DATABASE=database_database
DB_USERNAME=database_username DB_USERNAME=database_username
DB_PASSWORD=database_user_password DB_PASSWORD=database_user_password
# MySQL specific connection options
# Path to Certificate Authority (CA) certificate file for your MySQL instance.
# When this option is used host name identity verification will be performed
# which checks the hostname, used by the client, against names within the
# certificate itself (Common Name or Subject Alternative Name).
MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
# Mail system to use # Mail system to use
# Can be 'smtp' or 'sendmail' # Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
@ -324,6 +331,13 @@ ALLOW_UNTRUSTED_SERVER_FETCHING=false
# Setting this option will also auto-adjust cookies to be SameSite=None. # Setting this option will also auto-adjust cookies to be SameSite=None.
ALLOWED_IFRAME_HOSTS=null ALLOWED_IFRAME_HOSTS=null
# A list of sources/hostnames that can be loaded within iframes within BookStack.
# Space separated if multiple. BookStack host domain is auto-inferred.
# Can be set to a lone "*" to allow all sources for iframe content (Not advised).
# Defaults to a set of common services.
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
# The default and maximum item-counts for listing API requests. # The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100 API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500 API_MAX_ITEM_COUNT=500

View File

@ -230,3 +230,5 @@ roncallyt :: Portuguese, Brazilian
goegol :: Dutch goegol :: Dutch
msevgen :: Turkish msevgen :: Turkish
Khroners :: French Khroners :: French
MASOUD HOSSEINY (masoudme) :: Persian
Thomerson Roncally (roncallyt) :: Portuguese, Brazilian

View File

@ -57,6 +57,13 @@ return [
// Space separated if multiple. BookStack host domain is auto-inferred. // Space separated if multiple. BookStack host domain is auto-inferred.
'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null), 'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
// A list of sources/hostnames that can be loaded within iframes within BookStack.
// Space separated if multiple. BookStack host domain is auto-inferred.
// Can be set to a lone "*" to allow all sources for iframe content (Not advised).
// Defaults to a set of common services.
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
// Application timezone for back-end date functions. // Application timezone for back-end date functions.
'timezone' => env('APP_TIMEZONE', 'UTC'), 'timezone' => env('APP_TIMEZONE', 'UTC'),

View File

@ -7,6 +7,7 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
use BookStack\Uploads\ImageService; use BookStack\Uploads\ImageService;
use BookStack\Util\CspService;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use DOMXPath; use DOMXPath;
@ -15,16 +16,18 @@ use Throwable;
class ExportFormatter class ExportFormatter
{ {
protected $imageService; protected ImageService $imageService;
protected $pdfGenerator; protected PdfGenerator $pdfGenerator;
protected CspService $cspService;
/** /**
* ExportService constructor. * ExportService constructor.
*/ */
public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator) public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator, CspService $cspService)
{ {
$this->imageService = $imageService; $this->imageService = $imageService;
$this->pdfGenerator = $pdfGenerator; $this->pdfGenerator = $pdfGenerator;
$this->cspService = $cspService;
} }
/** /**
@ -37,8 +40,9 @@ class ExportFormatter
{ {
$page->html = (new PageContent($page))->render(); $page->html = (new PageContent($page))->render();
$pageHtml = view('pages.export', [ $pageHtml = view('pages.export', [
'page' => $page, 'page' => $page,
'format' => 'html', 'format' => 'html',
'cspContent' => $this->cspService->getCspMetaTagValue(),
])->render(); ])->render();
return $this->containHtml($pageHtml); return $this->containHtml($pageHtml);
@ -56,9 +60,10 @@ class ExportFormatter
$page->html = (new PageContent($page))->render(); $page->html = (new PageContent($page))->render();
}); });
$html = view('chapters.export', [ $html = view('chapters.export', [
'chapter' => $chapter, 'chapter' => $chapter,
'pages' => $pages, 'pages' => $pages,
'format' => 'html', 'format' => 'html',
'cspContent' => $this->cspService->getCspMetaTagValue(),
])->render(); ])->render();
return $this->containHtml($html); return $this->containHtml($html);
@ -76,6 +81,7 @@ class ExportFormatter
'book' => $book, 'book' => $book,
'bookChildren' => $bookTree, 'bookChildren' => $bookTree,
'format' => 'html', 'format' => 'html',
'cspContent' => $this->cspService->getCspMetaTagValue(),
])->render(); ])->render();
return $this->containHtml($html); return $this->containHtml($html);

View File

@ -8,10 +8,7 @@ use Illuminate\Http\Request;
class ApplyCspRules class ApplyCspRules
{ {
/** protected CspService $cspService;
* @var CspService
*/
protected $cspService;
public function __construct(CspService $cspService) public function __construct(CspService $cspService)
{ {
@ -35,10 +32,8 @@ class ApplyCspRules
$response = $next($request); $response = $next($request);
$this->cspService->setFrameAncestors($response); $cspHeader = $this->cspService->getCspHeader();
$this->cspService->setScriptSrc($response); $response->headers->set('Content-Security-Policy', $cspHeader, false);
$this->cspService->setObjectSrc($response);
$this->cspService->setBaseUri($response);
return $response; return $response;
} }

View File

@ -3,12 +3,10 @@
namespace BookStack\Util; namespace BookStack\Util;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class CspService class CspService
{ {
/** @var string */ protected string $nonce;
protected $nonce;
public function __construct(string $nonce = '') public function __construct(string $nonce = '')
{ {
@ -24,37 +22,34 @@ class CspService
} }
/** /**
* Sets CSP 'script-src' headers to restrict the forms of script that can * Get the CSP headers for the application
* run on the page.
*/ */
public function setScriptSrc(Response $response) public function getCspHeader(): string
{ {
if (config('app.allow_content_scripts')) { $headers = [
return; $this->getFrameAncestors(),
} $this->getFrameSrc(),
$this->getScriptSrc(),
$parts = [ $this->getObjectSrc(),
'http:', $this->getBaseUri(),
'https:',
'\'nonce-' . $this->nonce . '\'',
'\'strict-dynamic\'',
]; ];
$value = 'script-src ' . implode(' ', $parts); return implode('; ', array_filter($headers));
$response->headers->set('Content-Security-Policy', $value, false);
} }
/** /**
* Sets CSP "frame-ancestors" headers to restrict the hosts that BookStack can be * Get the CSP rules for the application for a HTML meta tag.
* iframed within. Also adjusts the cookie samesite options so that cookies will
* operate in the third-party context.
*/ */
public function setFrameAncestors(Response $response) public function getCspMetaTagValue(): string
{ {
$iframeHosts = $this->getAllowedIframeHosts(); $headers = [
array_unshift($iframeHosts, "'self'"); $this->getFrameSrc(),
$cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts); $this->getScriptSrc(),
$response->headers->set('Content-Security-Policy', $cspValue, false); $this->getObjectSrc(),
$this->getBaseUri(),
];
return implode('; ', array_filter($headers));
} }
/** /**
@ -66,25 +61,65 @@ class CspService
} }
/** /**
* Sets CSP 'object-src' headers to restrict the types of dynamic content * Create CSP 'script-src' rule to restrict the forms of script that can run on the page.
* that can be embedded on the page.
*/ */
public function setObjectSrc(Response $response) protected function getScriptSrc(): string
{ {
if (config('app.allow_content_scripts')) { if (config('app.allow_content_scripts')) {
return; return '';
} }
$response->headers->set('Content-Security-Policy', 'object-src \'self\'', false); $parts = [
'http:',
'https:',
'\'nonce-' . $this->nonce . '\'',
'\'strict-dynamic\'',
];
return 'script-src ' . implode(' ', $parts);
} }
/** /**
* Sets CSP 'base-uri' headers to restrict what base tags can be set on * Create CSP "frame-ancestors" rule to restrict the hosts that BookStack can be iframed within.
*/
protected function getFrameAncestors(): string
{
$iframeHosts = $this->getAllowedIframeHosts();
array_unshift($iframeHosts, "'self'");
return 'frame-ancestors ' . implode(' ', $iframeHosts);
}
/**
* Creates CSP "frame-src" rule to restrict what hosts/sources can be loaded
* within iframes to provide an allow-list-style approach to iframe content.
*/
protected function getFrameSrc(): string
{
$iframeHosts = $this->getAllowedIframeSources();
array_unshift($iframeHosts, "'self'");
return 'frame-src ' . implode(' ', $iframeHosts);
}
/**
* Creates CSP 'object-src' rule to restrict the types of dynamic content
* that can be embedded on the page.
*/
protected function getObjectSrc(): string
{
if (config('app.allow_content_scripts')) {
return '';
}
return "object-src 'self'";
}
/**
* Creates CSP 'base-uri' rule to restrict what base tags can be set on
* the page to prevent manipulation of relative links. * the page to prevent manipulation of relative links.
*/ */
public function setBaseUri(Response $response) protected function getBaseUri(): string
{ {
$response->headers->set('Content-Security-Policy', 'base-uri \'self\'', false); return "base-uri 'self'";
} }
protected function getAllowedIframeHosts(): array protected function getAllowedIframeHosts(): array
@ -93,4 +128,21 @@ class CspService
return array_filter(explode(' ', $hosts)); return array_filter(explode(' ', $hosts));
} }
protected function getAllowedIframeSources(): array
{
$sources = config('app.iframe_sources', '');
$hosts = array_filter(explode(' ', $sources));
// Extract drawing service url to allow embedding if active
$drawioConfigValue = config('services.drawio');
if ($drawioConfigValue) {
$drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/';
$drawioSourceParsed = parse_url($drawioSource);
$drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host'];
$hosts[] = $drawioHost;
}
return $hosts;
}
} }

View File

@ -7,42 +7,42 @@
*/ */
return [ return [
// General editor terms // General editor terms
'general' => 'General', 'general' => 'عمومی',
'advanced' => 'Advanced', 'advanced' => 'پیشرفته',
'none' => 'None', 'none' => 'هیچ کدام',
'cancel' => 'Cancel', 'cancel' => 'لغو',
'save' => 'Save', 'save' => 'ذخیره',
'close' => 'Close', 'close' => 'بستن',
'undo' => 'Undo', 'undo' => 'برگشت',
'redo' => 'Redo', 'redo' => 'از نو',
'left' => 'Left', 'left' => 'چپ',
'center' => 'Center', 'center' => 'مرکز',
'right' => 'Right', 'right' => 'راست',
'top' => 'Top', 'top' => 'بالا',
'middle' => 'Middle', 'middle' => 'میانه',
'bottom' => 'Bottom', 'bottom' => 'پایین',
'width' => 'Width', 'width' => 'عرض',
'height' => 'Height', 'height' => 'ارتفاع',
'More' => 'More', 'More' => 'بیشتر',
// Toolbar // Toolbar
'formats' => 'Formats', 'formats' => 'الگو',
'header_large' => 'Large Header', 'header_large' => 'عنوان بزرگ',
'header_medium' => 'Medium Header', 'header_medium' => 'عنوان متوسط',
'header_small' => 'Small Header', 'header_small' => 'عنوان کوچک',
'header_tiny' => 'Tiny Header', 'header_tiny' => 'هدر کوچک',
'paragraph' => 'Paragraph', 'paragraph' => 'پاراگراف',
'blockquote' => 'Blockquote', 'blockquote' => 'نقل قول',
'inline_code' => 'Inline code', 'inline_code' => 'کد درون خطی',
'callouts' => 'Callouts', 'callouts' => 'تعليق تفسيري',
'callout_information' => 'Information', 'callout_information' => 'اطلاعات',
'callout_success' => 'Success', 'callout_success' => 'موفق',
'callout_warning' => 'Warning', 'callout_warning' => 'هشدار',
'callout_danger' => 'Danger', 'callout_danger' => 'خطر',
'bold' => 'Bold', 'bold' => 'توپر',
'italic' => 'Italic', 'italic' => 'حروف کج(ایتالیک)',
'underline' => 'Underline', 'underline' => 'زیرخط',
'strikethrough' => 'Strikethrough', 'strikethrough' => 'خط خورده',
'superscript' => 'Superscript', 'superscript' => 'Superscript',
'subscript' => 'Subscript', 'subscript' => 'Subscript',
'text_color' => 'Text color', 'text_color' => 'Text color',

View File

@ -7,148 +7,148 @@
*/ */
return [ return [
// General editor terms // General editor terms
'general' => 'General', 'general' => '一般',
'advanced' => 'Advanced', 'advanced' => '詳細設定',
'none' => 'None', 'none' => 'なし',
'cancel' => 'Cancel', 'cancel' => '取消',
'save' => 'Save', 'save' => '保存',
'close' => 'Close', 'close' => '閉じる',
'undo' => 'Undo', 'undo' => '元に戻す',
'redo' => 'Redo', 'redo' => 'やり直し',
'left' => 'Left', 'left' => '左寄せ',
'center' => 'Center', 'center' => '中央揃え',
'right' => 'Right', 'right' => '右寄せ',
'top' => 'Top', 'top' => '',
'middle' => 'Middle', 'middle' => '中央',
'bottom' => 'Bottom', 'bottom' => '',
'width' => 'Width', 'width' => '',
'height' => 'Height', 'height' => '高さ',
'More' => 'More', 'More' => 'さらに表示',
// Toolbar // Toolbar
'formats' => 'Formats', 'formats' => '書式',
'header_large' => 'Large Header', 'header_large' => '大見出し',
'header_medium' => 'Medium Header', 'header_medium' => '中見出し',
'header_small' => 'Small Header', 'header_small' => '小見出し',
'header_tiny' => 'Tiny Header', 'header_tiny' => '極小見出し',
'paragraph' => 'Paragraph', 'paragraph' => '段落',
'blockquote' => 'Blockquote', 'blockquote' => '引用',
'inline_code' => 'Inline code', 'inline_code' => 'インラインコード',
'callouts' => 'Callouts', 'callouts' => 'コールアウト',
'callout_information' => 'Information', 'callout_information' => '情報',
'callout_success' => 'Success', 'callout_success' => '成功',
'callout_warning' => 'Warning', 'callout_warning' => '警告',
'callout_danger' => 'Danger', 'callout_danger' => '危険',
'bold' => 'Bold', 'bold' => '太字',
'italic' => 'Italic', 'italic' => '斜体',
'underline' => 'Underline', 'underline' => '下線',
'strikethrough' => 'Strikethrough', 'strikethrough' => '取消線',
'superscript' => 'Superscript', 'superscript' => '上付き',
'subscript' => 'Subscript', 'subscript' => '下付き',
'text_color' => 'Text color', 'text_color' => 'テキストの色',
'custom_color' => 'Custom color', 'custom_color' => 'カスタムカラー',
'remove_color' => 'Remove color', 'remove_color' => '色設定を解除',
'background_color' => 'Background color', 'background_color' => '背景色',
'align_left' => 'Align left', 'align_left' => '左揃え',
'align_center' => 'Align center', 'align_center' => '中央揃え',
'align_right' => 'Align right', 'align_right' => '右揃え',
'align_justify' => 'Align justify', 'align_justify' => '両端揃え',
'list_bullet' => 'Bullet list', 'list_bullet' => '箇条書き',
'list_numbered' => 'Numbered list', 'list_numbered' => '番号付き箇条書き',
'indent_increase' => 'Increase indent', 'indent_increase' => 'インデントを増やす',
'indent_decrease' => 'Decrease indent', 'indent_decrease' => 'インデントを減らす',
'table' => 'Table', 'table' => '',
'insert_image' => 'Insert image', 'insert_image' => '画像の挿入',
'insert_image_title' => 'Insert/Edit Image', 'insert_image_title' => '画像の挿入・編集',
'insert_link' => 'Insert/edit link', 'insert_link' => 'リンクの挿入・編集',
'insert_link_title' => 'Insert/Edit Link', 'insert_link_title' => 'リンクの挿入・編集',
'insert_horizontal_line' => 'Insert horizontal line', 'insert_horizontal_line' => '水平線を挿入',
'insert_code_block' => 'Insert code block', 'insert_code_block' => 'コードブロックを挿入',
'insert_drawing' => 'Insert/edit drawing', 'insert_drawing' => '描画を挿入・編集',
'drawing_manager' => 'Drawing manager', 'drawing_manager' => '描画マネージャー',
'insert_media' => 'Insert/edit media', 'insert_media' => 'メディアの挿入・編集',
'insert_media_title' => 'Insert/Edit Media', 'insert_media_title' => 'メディアの挿入・編集',
'clear_formatting' => 'Clear formatting', 'clear_formatting' => '書式をクリア',
'source_code' => 'Source code', 'source_code' => 'ソースコード',
'source_code_title' => 'Source Code', 'source_code_title' => 'ソースコード',
'fullscreen' => 'Fullscreen', 'fullscreen' => '全画面表示',
'image_options' => 'Image options', 'image_options' => '画像オプション',
// Tables // Tables
'table_properties' => 'Table properties', 'table_properties' => '表の詳細設定',
'table_properties_title' => 'Table Properties', 'table_properties_title' => '表の詳細設定',
'delete_table' => 'Delete table', 'delete_table' => '表の削除',
'insert_row_before' => 'Insert row before', 'insert_row_before' => '上側に行を挿入',
'insert_row_after' => 'Insert row after', 'insert_row_after' => '下側に行を挿入',
'delete_row' => 'Delete row', 'delete_row' => '行の削除',
'insert_column_before' => 'Insert column before', 'insert_column_before' => '左側に列を挿入',
'insert_column_after' => 'Insert column after', 'insert_column_after' => '右側に列を挿入',
'delete_column' => 'Delete column', 'delete_column' => '列の削除',
'table_cell' => 'Cell', 'table_cell' => 'セル',
'table_row' => 'Row', 'table_row' => '',
'table_column' => 'Column', 'table_column' => '',
'cell_properties' => 'Cell properties', 'cell_properties' => 'セルの詳細設定',
'cell_properties_title' => 'Cell Properties', 'cell_properties_title' => 'セルの詳細設定',
'cell_type' => 'Cell type', 'cell_type' => 'セルタイプ',
'cell_type_cell' => 'Cell', 'cell_type_cell' => 'セル',
'cell_type_header' => 'Header cell', 'cell_type_header' => 'ヘッダーセル',
'table_row_group' => 'Row Group', 'table_row_group' => '行グループ',
'table_column_group' => 'Column Group', 'table_column_group' => '列グループ',
'horizontal_align' => 'Horizontal align', 'horizontal_align' => '水平方向の配置',
'vertical_align' => 'Vertical align', 'vertical_align' => '垂直方向の配置',
'border_width' => 'Border width', 'border_width' => '枠線幅',
'border_style' => 'Border style', 'border_style' => '枠線スタイル',
'border_color' => 'Border color', 'border_color' => '枠線の色',
'row_properties' => 'Row properties', 'row_properties' => '行の詳細設定',
'row_properties_title' => 'Row Properties', 'row_properties_title' => '行の詳細設定',
'cut_row' => 'Cut row', 'cut_row' => '行の切り取り',
'copy_row' => 'Copy row', 'copy_row' => '行のコピー',
'paste_row_before' => 'Paste row before', 'paste_row_before' => '上側に行を貼り付け',
'paste_row_after' => 'Paste row after', 'paste_row_after' => '下側に行を貼り付け',
'row_type' => 'Row type', 'row_type' => '行タイプ',
'row_type_header' => 'Header', 'row_type_header' => 'ヘッダー',
'row_type_body' => 'Body', 'row_type_body' => 'ボディー',
'row_type_footer' => 'Footer', 'row_type_footer' => 'フッター',
'alignment' => 'Alignment', 'alignment' => '配置',
'cut_column' => 'Cut column', 'cut_column' => '列の切り取り',
'copy_column' => 'Copy column', 'copy_column' => '列のコピー',
'paste_column_before' => 'Paste column before', 'paste_column_before' => '左側に列を貼り付け',
'paste_column_after' => 'Paste column after', 'paste_column_after' => '右側に列を貼り付け',
'cell_padding' => 'Cell padding', 'cell_padding' => 'セル内余白(パディング)',
'cell_spacing' => 'Cell spacing', 'cell_spacing' => 'セルの間隔',
'caption' => 'Caption', 'caption' => '表題',
'show_caption' => 'Show caption', 'show_caption' => 'キャプションの表示',
'constrain' => 'Constrain proportions', 'constrain' => '縦横比を保持する',
// Images, links, details/summary & embed // Images, links, details/summary & embed
'source' => 'Source', 'source' => '画像のソース',
'alt_desc' => 'Alternative description', 'alt_desc' => '代替の説明文',
'embed' => 'Embed', 'embed' => '埋め込み',
'paste_embed' => 'Paste your embed code below:', 'paste_embed' => '埋め込み用コードを下記に貼り付けてください。',
'url' => 'URL', 'url' => 'リンク先URL',
'text_to_display' => 'Text to display', 'text_to_display' => 'リンク元テキスト',
'title' => 'Title', 'title' => 'タイトル',
'open_link' => 'Open link in...', 'open_link' => 'リンクの開き方...',
'open_link_current' => 'Current window', 'open_link_current' => '同じウィンドウ',
'open_link_new' => 'New window', 'open_link_new' => '新規ウィンドウ',
'insert_collapsible' => 'Insert collapsible block', 'insert_collapsible' => '折りたたみブロックを追加',
'collapsible_unwrap' => 'Unwrap', 'collapsible_unwrap' => 'ブロックの解除',
'edit_label' => 'Edit label', 'edit_label' => 'ラベルを編集',
'toggle_open_closed' => 'Toggle open/closed', 'toggle_open_closed' => '折りたたみ状態の切替',
'collapsible_edit' => 'Edit collapsible block', 'collapsible_edit' => '折りたたみブロックを編集',
'toggle_label' => 'Toggle label', 'toggle_label' => 'ブロックのラベル',
// About view // About view
'about_title' => 'About the WYSIWYG Editor', 'about_title' => 'WYSIWYGエディタについて',
'editor_license' => 'Editor License & Copyright', 'editor_license' => 'エディタのライセンスと著作権',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.', 'editor_tiny_license' => 'このエディタはLGPL v2.1ライセンスの下で提供される:tinyLinkを利用して構築されています。',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.', 'editor_tiny_license_link' => 'TinyMCEの著作権およびライセンスの詳細は、こちらをご覧ください。',
'save_continue' => 'Save Page & Continue', 'save_continue' => 'ページを保存して続行',
'callouts_cycle' => '(Keep pressing to toggle through types)', 'callouts_cycle' => '(押し続けて種類を切り替え)',
'shortcuts' => 'Shortcuts', 'shortcuts' => 'ショートカット',
'shortcut' => 'Shortcut', 'shortcut' => 'ショートカット',
'shortcuts_intro' => 'The following shortcuts are available in the editor:', 'shortcuts_intro' => 'エディタでは次に示すショートカットが利用できます。',
'windows_linux' => '(Windows/Linux)', 'windows_linux' => '(Windows/Linux)',
'mac' => '(Mac)', 'mac' => '(Mac)',
'description' => 'Description', 'description' => 'テンプレートの内容',
]; ];

View File

@ -32,7 +32,7 @@ return [
'digits_between' => ':attributeは:min〜:maxである必要があります。', 'digits_between' => ':attributeは:min〜:maxである必要があります。',
'email' => ':attributeは正しいEメールアドレスである必要があります。', 'email' => ':attributeは正しいEメールアドレスである必要があります。',
'ends_with' => ':attributeは:valuesのいずれかで終わる必要があります。', 'ends_with' => ':attributeは:valuesのいずれかで終わる必要があります。',
'file' => 'The :attribute must be provided as a valid file.', 'file' => ':attributeは有効なファイルである必要があります。',
'filled' => ':attributeは必須です。', 'filled' => ':attributeは必須です。',
'gt' => [ 'gt' => [
'numeric' => ':attributeは:valueより大きな値である必要があります。', 'numeric' => ':attributeは:valueより大きな値である必要があります。',

View File

@ -60,8 +60,8 @@ return [
'webhook_delete_notification' => 'Вебхук успешно удален', 'webhook_delete_notification' => 'Вебхук успешно удален',
// Users // Users
'user_update_notification' => 'User successfully updated', 'user_update_notification' => 'Пользователь успешно обновлен',
'user_delete_notification' => 'User successfully removed', 'user_delete_notification' => 'Пользователь успешно удален',
// Other // Other
'commented_on' => 'прокомментировал', 'commented_on' => 'прокомментировал',

View File

@ -15,30 +15,30 @@ return [
'close' => 'Закрыть', 'close' => 'Закрыть',
'undo' => 'Отменить', 'undo' => 'Отменить',
'redo' => 'Повторить', 'redo' => 'Повторить',
'left' => 'Left', 'left' => 'Слева',
'center' => 'Center', 'center' => 'По центру',
'right' => 'Right', 'right' => 'Справа',
'top' => 'Top', 'top' => 'Сверху',
'middle' => 'Middle', 'middle' => 'Посередине',
'bottom' => 'Bottom', 'bottom' => 'Снизу',
'width' => 'Width', 'width' => 'Ширина',
'height' => 'Height', 'height' => 'Высота',
'More' => 'Еще', 'More' => 'Еще',
// Toolbar // Toolbar
'formats' => 'Formats', 'formats' => 'Форматы',
'header_large' => 'Крупный заголовок', 'header_large' => 'Большой',
'header_medium' => 'Средний заголовок', 'header_medium' => 'Средний',
'header_small' => 'Небольшой заголовок', 'header_small' => 'Маленький',
'header_tiny' => 'Маленький заголовок', 'header_tiny' => 'Крошечный',
'paragraph' => 'Абзац', 'paragraph' => 'Обычный текст',
'blockquote' => 'Цитата', 'blockquote' => 'Цитата',
'inline_code' => 'Встроенный код', 'inline_code' => 'Встроенный код',
'callouts' => 'Выноска', 'callouts' => 'Выноска',
'callout_information' => 'Информация', 'callout_information' => 'Информация',
'callout_success' => 'Успешно', 'callout_success' => 'Успех',
'callout_warning' => 'Предупреждение', 'callout_warning' => 'Предупреждение',
'callout_danger' => 'Опасность', 'callout_danger' => 'Ошибка',
'bold' => 'Жирный', 'bold' => 'Жирный',
'italic' => 'Курсив', 'italic' => 'Курсив',
'underline' => 'Подчёркнутый', 'underline' => 'Подчёркнутый',
@ -47,8 +47,8 @@ return [
'subscript' => 'Подстрочный', 'subscript' => 'Подстрочный',
'text_color' => 'Цвет текста', 'text_color' => 'Цвет текста',
'custom_color' => 'Пользовательский цвет', 'custom_color' => 'Пользовательский цвет',
'remove_color' => 'Убрать цвет', 'remove_color' => 'Удалить цвет',
'background_color' => 'Фоновый цвет', 'background_color' => 'Цвет фона',
'align_left' => 'По левому краю', 'align_left' => 'По левому краю',
'align_center' => 'По центру', 'align_center' => 'По центру',
'align_right' => 'По правому краю', 'align_right' => 'По правому краю',
@ -71,7 +71,7 @@ return [
'clear_formatting' => 'Очистить форматирование', 'clear_formatting' => 'Очистить форматирование',
'source_code' => 'Исходный код', 'source_code' => 'Исходный код',
'source_code_title' => 'Исходный код', 'source_code_title' => 'Исходный код',
'fullscreen' => 'Полный экран', 'fullscreen' => 'Полноэкранный режим',
'image_options' => 'Параметры изображения', 'image_options' => 'Параметры изображения',
// Tables // Tables
@ -91,7 +91,7 @@ return [
'cell_properties_title' => 'Свойства ячейки', 'cell_properties_title' => 'Свойства ячейки',
'cell_type' => 'Тип ячейки', 'cell_type' => 'Тип ячейки',
'cell_type_cell' => 'Ячейка', 'cell_type_cell' => 'Ячейка',
'cell_type_header' => 'Header cell', 'cell_type_header' => 'Заголовок ячейки',
'table_row_group' => 'Объединить строки', 'table_row_group' => 'Объединить строки',
'table_column_group' => 'Объединить столбцы', 'table_column_group' => 'Объединить столбцы',
'horizontal_align' => 'Выровнять по горизонтали', 'horizontal_align' => 'Выровнять по горизонтали',
@ -107,44 +107,44 @@ return [
'paste_row_after' => 'Вставить строку ниже', 'paste_row_after' => 'Вставить строку ниже',
'row_type' => 'Тип строки', 'row_type' => 'Тип строки',
'row_type_header' => 'Заголовок', 'row_type_header' => 'Заголовок',
'row_type_body' => 'Body', 'row_type_body' => 'Тело',
'row_type_footer' => 'Footer', 'row_type_footer' => 'Нижняя часть',
'alignment' => 'Выравнивание', 'alignment' => 'Выравнивание',
'cut_column' => 'Вырезать столбец', 'cut_column' => 'Вырезать столбец',
'copy_column' => 'Копировать столбец', 'copy_column' => 'Копировать столбец',
'paste_column_before' => 'Вставить столбец слева', 'paste_column_before' => 'Вставить столбец слева',
'paste_column_after' => 'Вставить столбец справа', 'paste_column_after' => 'Вставить столбец справа',
'cell_padding' => 'Cell padding', 'cell_padding' => 'Расстояние между границей и содержимым',
'cell_spacing' => 'Cell spacing', 'cell_spacing' => 'Расстояние между ячейками',
'caption' => 'Caption', 'caption' => 'Подпись',
'show_caption' => 'Show caption', 'show_caption' => 'Показать подпись',
'constrain' => 'Constrain proportions', 'constrain' => 'Сохранять пропорции',
// Images, links, details/summary & embed // Images, links, details/summary & embed
'source' => 'Source', 'source' => 'Источник',
'alt_desc' => 'Alternative description', 'alt_desc' => 'Альтернативное описание',
'embed' => 'Embed', 'embed' => 'Код для вставки',
'paste_embed' => 'Paste your embed code below:', 'paste_embed' => 'Введите код для вставки ниже:',
'url' => 'URL-адрес', 'url' => 'URL-адрес',
'text_to_display' => 'Текст для отображения', 'text_to_display' => 'Текст для отображения',
'title' => 'Заголовок', 'title' => 'Заголовок',
'open_link' => 'Открыть ссылку в...', 'open_link' => 'Открыть ссылку в...',
'open_link_current' => 'В текущем окне', 'open_link_current' => 'В текущем окне',
'open_link_new' => 'Новое окно', 'open_link_new' => 'В новом окне',
'insert_collapsible' => 'Вставить свернутый блок', 'insert_collapsible' => 'Вставить свернутый блок',
'collapsible_unwrap' => 'Unwrap', 'collapsible_unwrap' => 'Удалить блок',
'edit_label' => 'Изменить метку', 'edit_label' => 'Изменить метку',
'toggle_open_closed' => 'Toggle open/closed', 'toggle_open_closed' => 'Развернуть/свернуть',
'collapsible_edit' => 'Edit collapsible block', 'collapsible_edit' => 'Редактировать свернутый блок',
'toggle_label' => 'Toggle label', 'toggle_label' => 'Метка',
// About view // About view
'about_title' => 'О редакторе WYSIWYG', 'about_title' => 'О редакторе WYSIWYG',
'editor_license' => 'Лицензия редактора и авторские права', 'editor_license' => 'Лицензия редактора и авторские права',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.', 'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под лицензией LGPL v2.1.',
'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE Вы можете найти здесь.', 'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE вы можете найти здесь.',
'save_continue' => 'Сохранить страницу и продолжить', 'save_continue' => 'Сохранить страницу и продолжить',
'callouts_cycle' => '(Keep pressing to toggle through types)', 'callouts_cycle' => '(Держите нажатым для переключения типов)',
'shortcuts' => 'Сочетания клавиш', 'shortcuts' => 'Сочетания клавиш',
'shortcut' => 'Сочетания клавиш', 'shortcut' => 'Сочетания клавиш',
'shortcuts_intro' => 'Следующие сочетания клавиш доступны в редакторе:', 'shortcuts_intro' => 'Следующие сочетания клавиш доступны в редакторе:',

View File

@ -4,6 +4,10 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>@yield('title')</title> <title>@yield('title')</title>
@if($cspContent ?? false)
<meta http-equiv="Content-Security-Policy" content="{{ $cspContent }}">
@endif
@include('common.export-styles', ['format' => $format, 'engine' => $engine ?? '']) @include('common.export-styles', ['format' => $format, 'engine' => $engine ?? ''])
@include('common.export-custom-head') @include('common.export-custom-head')
</head> </head>

View File

@ -268,7 +268,7 @@ class ExportTest extends TestCase
foreach ($entities as $entity) { foreach ($entities as $entity) {
$resp = $this->asEditor()->get($entity->getUrl('/export/html')); $resp = $this->asEditor()->get($entity->getUrl('/export/html'));
$resp->assertDontSee('window.donkey'); $resp->assertDontSee('window.donkey');
$resp->assertDontSee('script'); $resp->assertDontSee('<script', false);
$resp->assertSee('.my-test-class { color: red; }'); $resp->assertSee('.my-test-class { color: red; }');
} }
} }
@ -448,4 +448,18 @@ class ExportTest extends TestCase
$resp = $this->get($page->getUrl('/export/pdf')); $resp = $this->get($page->getUrl('/export/pdf'));
$resp->assertStatus(500); // Bad response indicates wkhtml usage $resp->assertStatus(500); // Bad response indicates wkhtml usage
} }
public function test_html_exports_contain_csp_meta_tag()
{
$entities = [
Page::query()->first(),
Book::query()->first(),
Chapter::query()->first(),
];
foreach ($entities as $entity) {
$resp = $this->asEditor()->get($entity->getUrl('/export/html'));
$resp->assertElementExists('head meta[http-equiv="Content-Security-Policy"][content*="script-src "]');
}
}
} }

View File

@ -119,6 +119,25 @@ class SecurityHeaderTest extends TestCase
$this->assertEquals('base-uri \'self\'', $scriptHeader); $this->assertEquals('base-uri \'self\'', $scriptHeader);
} }
public function test_frame_src_csp_header_set()
{
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'frame-src');
$this->assertEquals('frame-src \'self\' https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com', $scriptHeader);
}
public function test_frame_src_csp_header_has_drawio_host_added()
{
config()->set([
'app.iframe_sources' => 'https://example.com',
'services.drawio' => 'https://diagrams.example.com/testing?cat=dog',
]);
$resp = $this->get('/');
$scriptHeader = $this->getCspHeader($resp, 'frame-src');
$this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com', $scriptHeader);
}
public function test_cache_control_headers_are_strict_on_responses_when_logged_in() public function test_cache_control_headers_are_strict_on_responses_when_logged_in()
{ {
$this->asEditor(); $this->asEditor();
@ -133,10 +152,14 @@ class SecurityHeaderTest extends TestCase
*/ */
protected function getCspHeader(TestResponse $resp, string $type): string protected function getCspHeader(TestResponse $resp, string $type): string
{ {
$cspHeaders = collect($resp->headers->all('Content-Security-Policy')); $cspHeaders = explode('; ', $resp->headers->get('Content-Security-Policy'));
return $cspHeaders->filter(function ($val) use ($type) { foreach ($cspHeaders as $cspHeader) {
return strpos($val, $type) === 0; if (strpos($cspHeader, $type) === 0) {
})->first() ?? ''; return $cspHeader;
}
}
return '';
} }
} }