diff --git a/.env.example.complete b/.env.example.complete index 9d24fceeb..fb947408d 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -42,7 +42,7 @@ APP_TIMEZONE=UTC # overrides can be made. Defaults to disabled. APP_THEME=false -# Trusted Proxies +# Trusted proxies # 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 # incoming proxy request to provide origin detail. @@ -58,6 +58,13 @@ DB_DATABASE=database_database DB_USERNAME=database_username 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 # Can be 'smtp' or 'sendmail' MAIL_DRIVER=smtp @@ -324,6 +331,13 @@ ALLOW_UNTRUSTED_SERVER_FETCHING=false # Setting this option will also auto-adjust cookies to be SameSite=None. 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. API_DEFAULT_ITEM_COUNT=100 API_MAX_ITEM_COUNT=500 diff --git a/.github/translators.txt b/.github/translators.txt index 1e1d2c201..93a9c21f6 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -230,3 +230,5 @@ roncallyt :: Portuguese, Brazilian goegol :: Dutch msevgen :: Turkish Khroners :: French +MASOUD HOSSEINY (masoudme) :: Persian +Thomerson Roncally (roncallyt) :: Portuguese, Brazilian diff --git a/app/Config/app.php b/app/Config/app.php index 39bfa7134..2329043b6 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -57,6 +57,13 @@ return [ // Space separated if multiple. BookStack host domain is auto-inferred. '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. 'timezone' => env('APP_TIMEZONE', 'UTC'), diff --git a/app/Entities/Tools/ExportFormatter.php b/app/Entities/Tools/ExportFormatter.php index 7edd1b50f..9029d7270 100644 --- a/app/Entities/Tools/ExportFormatter.php +++ b/app/Entities/Tools/ExportFormatter.php @@ -7,6 +7,7 @@ use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\Markdown\HtmlToMarkdown; use BookStack\Uploads\ImageService; +use BookStack\Util\CspService; use DOMDocument; use DOMElement; use DOMXPath; @@ -15,16 +16,18 @@ use Throwable; class ExportFormatter { - protected $imageService; - protected $pdfGenerator; + protected ImageService $imageService; + protected PdfGenerator $pdfGenerator; + protected CspService $cspService; /** * ExportService constructor. */ - public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator) + public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator, CspService $cspService) { $this->imageService = $imageService; $this->pdfGenerator = $pdfGenerator; + $this->cspService = $cspService; } /** @@ -37,8 +40,9 @@ class ExportFormatter { $page->html = (new PageContent($page))->render(); $pageHtml = view('pages.export', [ - 'page' => $page, - 'format' => 'html', + 'page' => $page, + 'format' => 'html', + 'cspContent' => $this->cspService->getCspMetaTagValue(), ])->render(); return $this->containHtml($pageHtml); @@ -56,9 +60,10 @@ class ExportFormatter $page->html = (new PageContent($page))->render(); }); $html = view('chapters.export', [ - 'chapter' => $chapter, - 'pages' => $pages, - 'format' => 'html', + 'chapter' => $chapter, + 'pages' => $pages, + 'format' => 'html', + 'cspContent' => $this->cspService->getCspMetaTagValue(), ])->render(); return $this->containHtml($html); @@ -76,6 +81,7 @@ class ExportFormatter 'book' => $book, 'bookChildren' => $bookTree, 'format' => 'html', + 'cspContent' => $this->cspService->getCspMetaTagValue(), ])->render(); return $this->containHtml($html); diff --git a/app/Http/Middleware/ApplyCspRules.php b/app/Http/Middleware/ApplyCspRules.php index 6c9d14e7b..9f3a8d1d8 100644 --- a/app/Http/Middleware/ApplyCspRules.php +++ b/app/Http/Middleware/ApplyCspRules.php @@ -8,10 +8,7 @@ use Illuminate\Http\Request; class ApplyCspRules { - /** - * @var CspService - */ - protected $cspService; + protected CspService $cspService; public function __construct(CspService $cspService) { @@ -35,10 +32,8 @@ class ApplyCspRules $response = $next($request); - $this->cspService->setFrameAncestors($response); - $this->cspService->setScriptSrc($response); - $this->cspService->setObjectSrc($response); - $this->cspService->setBaseUri($response); + $cspHeader = $this->cspService->getCspHeader(); + $response->headers->set('Content-Security-Policy', $cspHeader, false); return $response; } diff --git a/app/Util/CspService.php b/app/Util/CspService.php index 812e1a4be..ba927c93b 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -3,12 +3,10 @@ namespace BookStack\Util; use Illuminate\Support\Str; -use Symfony\Component\HttpFoundation\Response; class CspService { - /** @var string */ - protected $nonce; + protected 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 - * run on the page. + * Get the CSP headers for the application */ - public function setScriptSrc(Response $response) + public function getCspHeader(): string { - if (config('app.allow_content_scripts')) { - return; - } - - $parts = [ - 'http:', - 'https:', - '\'nonce-' . $this->nonce . '\'', - '\'strict-dynamic\'', + $headers = [ + $this->getFrameAncestors(), + $this->getFrameSrc(), + $this->getScriptSrc(), + $this->getObjectSrc(), + $this->getBaseUri(), ]; - $value = 'script-src ' . implode(' ', $parts); - $response->headers->set('Content-Security-Policy', $value, false); + return implode('; ', array_filter($headers)); } /** - * 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. + * Get the CSP rules for the application for a HTML meta tag. */ - public function setFrameAncestors(Response $response) + public function getCspMetaTagValue(): string { - $iframeHosts = $this->getAllowedIframeHosts(); - array_unshift($iframeHosts, "'self'"); - $cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts); - $response->headers->set('Content-Security-Policy', $cspValue, false); + $headers = [ + $this->getFrameSrc(), + $this->getScriptSrc(), + $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 - * that can be embedded on the page. + * Create CSP 'script-src' rule to restrict the forms of script that can run on the page. */ - public function setObjectSrc(Response $response) + protected function getScriptSrc(): string { 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. */ - 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 @@ -93,4 +128,21 @@ class CspService 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; + } } diff --git a/resources/lang/fa/editor.php b/resources/lang/fa/editor.php index 76a9f7fca..22d463365 100644 --- a/resources/lang/fa/editor.php +++ b/resources/lang/fa/editor.php @@ -7,42 +7,42 @@ */ return [ // General editor terms - 'general' => 'General', - 'advanced' => 'Advanced', - 'none' => 'None', - 'cancel' => 'Cancel', - 'save' => 'Save', - 'close' => 'Close', - 'undo' => 'Undo', - 'redo' => 'Redo', - 'left' => 'Left', - 'center' => 'Center', - 'right' => 'Right', - 'top' => 'Top', - 'middle' => 'Middle', - 'bottom' => 'Bottom', - 'width' => 'Width', - 'height' => 'Height', - 'More' => 'More', + 'general' => 'عمومی', + 'advanced' => 'پیشرفته', + 'none' => 'هیچ کدام', + 'cancel' => 'لغو', + 'save' => 'ذخیره', + 'close' => 'بستن', + 'undo' => 'برگشت', + 'redo' => 'از نو', + 'left' => 'چپ', + 'center' => 'مرکز', + 'right' => 'راست', + 'top' => 'بالا', + 'middle' => 'میانه', + 'bottom' => 'پایین', + 'width' => 'عرض', + 'height' => 'ارتفاع', + 'More' => 'بیشتر', // Toolbar - 'formats' => 'Formats', - 'header_large' => 'Large Header', - 'header_medium' => 'Medium Header', - 'header_small' => 'Small Header', - 'header_tiny' => 'Tiny Header', - 'paragraph' => 'Paragraph', - 'blockquote' => 'Blockquote', - 'inline_code' => 'Inline code', - 'callouts' => 'Callouts', - 'callout_information' => 'Information', - 'callout_success' => 'Success', - 'callout_warning' => 'Warning', - 'callout_danger' => 'Danger', - 'bold' => 'Bold', - 'italic' => 'Italic', - 'underline' => 'Underline', - 'strikethrough' => 'Strikethrough', + 'formats' => 'الگو', + 'header_large' => 'عنوان بزرگ', + 'header_medium' => 'عنوان متوسط', + 'header_small' => 'عنوان کوچک', + 'header_tiny' => 'هدر کوچک', + 'paragraph' => 'پاراگراف', + 'blockquote' => 'نقل قول', + 'inline_code' => 'کد درون خطی', + 'callouts' => 'تعليق تفسيري', + 'callout_information' => 'اطلاعات', + 'callout_success' => 'موفق', + 'callout_warning' => 'هشدار', + 'callout_danger' => 'خطر', + 'bold' => 'توپر', + 'italic' => 'حروف کج(ایتالیک)', + 'underline' => 'زیرخط', + 'strikethrough' => 'خط خورده', 'superscript' => 'Superscript', 'subscript' => 'Subscript', 'text_color' => 'Text color', diff --git a/resources/lang/ja/editor.php b/resources/lang/ja/editor.php index 76a9f7fca..de38b0cbe 100644 --- a/resources/lang/ja/editor.php +++ b/resources/lang/ja/editor.php @@ -7,148 +7,148 @@ */ return [ // General editor terms - 'general' => 'General', - 'advanced' => 'Advanced', - 'none' => 'None', - 'cancel' => 'Cancel', - 'save' => 'Save', - 'close' => 'Close', - 'undo' => 'Undo', - 'redo' => 'Redo', - 'left' => 'Left', - 'center' => 'Center', - 'right' => 'Right', - 'top' => 'Top', - 'middle' => 'Middle', - 'bottom' => 'Bottom', - 'width' => 'Width', - 'height' => 'Height', - 'More' => 'More', + 'general' => '一般', + 'advanced' => '詳細設定', + 'none' => 'なし', + 'cancel' => '取消', + 'save' => '保存', + 'close' => '閉じる', + 'undo' => '元に戻す', + 'redo' => 'やり直し', + 'left' => '左寄せ', + 'center' => '中央揃え', + 'right' => '右寄せ', + 'top' => '上', + 'middle' => '中央', + 'bottom' => '下', + 'width' => '幅', + 'height' => '高さ', + 'More' => 'さらに表示', // Toolbar - 'formats' => 'Formats', - 'header_large' => 'Large Header', - 'header_medium' => 'Medium Header', - 'header_small' => 'Small Header', - 'header_tiny' => 'Tiny Header', - 'paragraph' => 'Paragraph', - 'blockquote' => 'Blockquote', - 'inline_code' => 'Inline code', - 'callouts' => 'Callouts', - 'callout_information' => 'Information', - 'callout_success' => 'Success', - 'callout_warning' => 'Warning', - 'callout_danger' => 'Danger', - 'bold' => 'Bold', - 'italic' => 'Italic', - 'underline' => 'Underline', - 'strikethrough' => 'Strikethrough', - 'superscript' => 'Superscript', - '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', - 'align_justify' => 'Align justify', - 'list_bullet' => 'Bullet list', - 'list_numbered' => 'Numbered list', - 'indent_increase' => 'Increase indent', - 'indent_decrease' => 'Decrease indent', - 'table' => 'Table', - 'insert_image' => 'Insert image', - 'insert_image_title' => 'Insert/Edit Image', - 'insert_link' => 'Insert/edit link', - 'insert_link_title' => 'Insert/Edit Link', - 'insert_horizontal_line' => 'Insert horizontal line', - 'insert_code_block' => 'Insert code block', - 'insert_drawing' => 'Insert/edit drawing', - 'drawing_manager' => 'Drawing manager', - 'insert_media' => 'Insert/edit media', - 'insert_media_title' => 'Insert/Edit Media', - 'clear_formatting' => 'Clear formatting', - 'source_code' => 'Source code', - 'source_code_title' => 'Source Code', - 'fullscreen' => 'Fullscreen', - 'image_options' => 'Image options', + 'formats' => '書式', + 'header_large' => '大見出し', + 'header_medium' => '中見出し', + 'header_small' => '小見出し', + 'header_tiny' => '極小見出し', + 'paragraph' => '段落', + 'blockquote' => '引用', + 'inline_code' => 'インラインコード', + 'callouts' => 'コールアウト', + 'callout_information' => '情報', + 'callout_success' => '成功', + 'callout_warning' => '警告', + 'callout_danger' => '危険', + 'bold' => '太字', + 'italic' => '斜体', + 'underline' => '下線', + 'strikethrough' => '取消線', + 'superscript' => '上付き', + 'subscript' => '下付き', + 'text_color' => 'テキストの色', + 'custom_color' => 'カスタムカラー', + 'remove_color' => '色設定を解除', + 'background_color' => '背景色', + 'align_left' => '左揃え', + 'align_center' => '中央揃え', + 'align_right' => '右揃え', + 'align_justify' => '両端揃え', + 'list_bullet' => '箇条書き', + 'list_numbered' => '番号付き箇条書き', + 'indent_increase' => 'インデントを増やす', + 'indent_decrease' => 'インデントを減らす', + 'table' => '表', + 'insert_image' => '画像の挿入', + 'insert_image_title' => '画像の挿入・編集', + 'insert_link' => 'リンクの挿入・編集', + 'insert_link_title' => 'リンクの挿入・編集', + 'insert_horizontal_line' => '水平線を挿入', + 'insert_code_block' => 'コードブロックを挿入', + 'insert_drawing' => '描画を挿入・編集', + 'drawing_manager' => '描画マネージャー', + 'insert_media' => 'メディアの挿入・編集', + 'insert_media_title' => 'メディアの挿入・編集', + 'clear_formatting' => '書式をクリア', + 'source_code' => 'ソースコード', + 'source_code_title' => 'ソースコード', + 'fullscreen' => '全画面表示', + 'image_options' => '画像オプション', // Tables - 'table_properties' => 'Table properties', - 'table_properties_title' => 'Table Properties', - 'delete_table' => 'Delete table', - 'insert_row_before' => 'Insert row before', - 'insert_row_after' => 'Insert row after', - 'delete_row' => 'Delete row', - 'insert_column_before' => 'Insert column before', - 'insert_column_after' => 'Insert column after', - 'delete_column' => 'Delete column', - 'table_cell' => 'Cell', - 'table_row' => 'Row', - 'table_column' => 'Column', - 'cell_properties' => 'Cell properties', - 'cell_properties_title' => 'Cell Properties', - 'cell_type' => 'Cell type', - 'cell_type_cell' => 'Cell', - 'cell_type_header' => 'Header cell', - 'table_row_group' => 'Row Group', - 'table_column_group' => 'Column Group', - 'horizontal_align' => 'Horizontal align', - 'vertical_align' => 'Vertical align', - 'border_width' => 'Border width', - 'border_style' => 'Border style', - 'border_color' => 'Border color', - 'row_properties' => 'Row properties', - 'row_properties_title' => 'Row Properties', - 'cut_row' => 'Cut row', - 'copy_row' => 'Copy row', - 'paste_row_before' => 'Paste row before', - 'paste_row_after' => 'Paste row after', - 'row_type' => 'Row type', - 'row_type_header' => 'Header', - 'row_type_body' => 'Body', - 'row_type_footer' => '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_spacing' => 'Cell spacing', - 'caption' => 'Caption', - 'show_caption' => 'Show caption', - 'constrain' => 'Constrain proportions', + 'table_properties' => '表の詳細設定', + 'table_properties_title' => '表の詳細設定', + 'delete_table' => '表の削除', + 'insert_row_before' => '上側に行を挿入', + 'insert_row_after' => '下側に行を挿入', + 'delete_row' => '行の削除', + 'insert_column_before' => '左側に列を挿入', + 'insert_column_after' => '右側に列を挿入', + 'delete_column' => '列の削除', + 'table_cell' => 'セル', + 'table_row' => '行', + 'table_column' => '列', + 'cell_properties' => 'セルの詳細設定', + 'cell_properties_title' => 'セルの詳細設定', + 'cell_type' => 'セルタイプ', + 'cell_type_cell' => 'セル', + 'cell_type_header' => 'ヘッダーセル', + 'table_row_group' => '行グループ', + 'table_column_group' => '列グループ', + 'horizontal_align' => '水平方向の配置', + 'vertical_align' => '垂直方向の配置', + 'border_width' => '枠線幅', + 'border_style' => '枠線スタイル', + 'border_color' => '枠線の色', + 'row_properties' => '行の詳細設定', + 'row_properties_title' => '行の詳細設定', + 'cut_row' => '行の切り取り', + 'copy_row' => '行のコピー', + 'paste_row_before' => '上側に行を貼り付け', + 'paste_row_after' => '下側に行を貼り付け', + 'row_type' => '行タイプ', + 'row_type_header' => 'ヘッダー', + 'row_type_body' => 'ボディー', + 'row_type_footer' => 'フッター', + 'alignment' => '配置', + 'cut_column' => '列の切り取り', + 'copy_column' => '列のコピー', + 'paste_column_before' => '左側に列を貼り付け', + 'paste_column_after' => '右側に列を貼り付け', + 'cell_padding' => 'セル内余白(パディング)', + 'cell_spacing' => 'セルの間隔', + 'caption' => '表題', + 'show_caption' => 'キャプションの表示', + 'constrain' => '縦横比を保持する', // Images, links, details/summary & embed - 'source' => 'Source', - 'alt_desc' => 'Alternative description', - 'embed' => 'Embed', - 'paste_embed' => 'Paste your embed code below:', - 'url' => 'URL', - 'text_to_display' => 'Text to display', - 'title' => 'Title', - 'open_link' => 'Open link in...', - 'open_link_current' => 'Current window', - 'open_link_new' => 'New window', - 'insert_collapsible' => 'Insert collapsible block', - 'collapsible_unwrap' => 'Unwrap', - 'edit_label' => 'Edit label', - 'toggle_open_closed' => 'Toggle open/closed', - 'collapsible_edit' => 'Edit collapsible block', - 'toggle_label' => 'Toggle label', + 'source' => '画像のソース', + 'alt_desc' => '代替の説明文', + 'embed' => '埋め込み', + 'paste_embed' => '埋め込み用コードを下記に貼り付けてください。', + 'url' => 'リンク先URL', + 'text_to_display' => 'リンク元テキスト', + 'title' => 'タイトル', + 'open_link' => 'リンクの開き方...', + 'open_link_current' => '同じウィンドウ', + 'open_link_new' => '新規ウィンドウ', + 'insert_collapsible' => '折りたたみブロックを追加', + 'collapsible_unwrap' => 'ブロックの解除', + 'edit_label' => 'ラベルを編集', + 'toggle_open_closed' => '折りたたみ状態の切替', + 'collapsible_edit' => '折りたたみブロックを編集', + 'toggle_label' => 'ブロックのラベル', // About view - 'about_title' => 'About the WYSIWYG Editor', - 'editor_license' => 'Editor License & Copyright', - 'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.', - 'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.', - 'save_continue' => 'Save Page & Continue', - 'callouts_cycle' => '(Keep pressing to toggle through types)', - 'shortcuts' => 'Shortcuts', - 'shortcut' => 'Shortcut', - 'shortcuts_intro' => 'The following shortcuts are available in the editor:', + 'about_title' => 'WYSIWYGエディタについて', + 'editor_license' => 'エディタのライセンスと著作権', + 'editor_tiny_license' => 'このエディタはLGPL v2.1ライセンスの下で提供される:tinyLinkを利用して構築されています。', + 'editor_tiny_license_link' => 'TinyMCEの著作権およびライセンスの詳細は、こちらをご覧ください。', + 'save_continue' => 'ページを保存して続行', + 'callouts_cycle' => '(押し続けて種類を切り替え)', + 'shortcuts' => 'ショートカット', + 'shortcut' => 'ショートカット', + 'shortcuts_intro' => 'エディタでは次に示すショートカットが利用できます。', 'windows_linux' => '(Windows/Linux)', 'mac' => '(Mac)', - 'description' => 'Description', + 'description' => 'テンプレートの内容', ]; diff --git a/resources/lang/ja/validation.php b/resources/lang/ja/validation.php index 4c96087f9..96ae7dfff 100644 --- a/resources/lang/ja/validation.php +++ b/resources/lang/ja/validation.php @@ -32,7 +32,7 @@ return [ 'digits_between' => ':attributeは:min〜:maxである必要があります。', 'email' => ':attributeは正しいEメールアドレスである必要があります。', 'ends_with' => ':attributeは:valuesのいずれかで終わる必要があります。', - 'file' => 'The :attribute must be provided as a valid file.', + 'file' => ':attributeは有効なファイルである必要があります。', 'filled' => ':attributeは必須です。', 'gt' => [ 'numeric' => ':attributeは:valueより大きな値である必要があります。', diff --git a/resources/lang/ru/activities.php b/resources/lang/ru/activities.php index 47a589f3f..a015e183d 100644 --- a/resources/lang/ru/activities.php +++ b/resources/lang/ru/activities.php @@ -60,8 +60,8 @@ return [ 'webhook_delete_notification' => 'Вебхук успешно удален', // Users - 'user_update_notification' => 'User successfully updated', - 'user_delete_notification' => 'User successfully removed', + 'user_update_notification' => 'Пользователь успешно обновлен', + 'user_delete_notification' => 'Пользователь успешно удален', // Other 'commented_on' => 'прокомментировал', diff --git a/resources/lang/ru/editor.php b/resources/lang/ru/editor.php index 44021900a..45a8d1e90 100644 --- a/resources/lang/ru/editor.php +++ b/resources/lang/ru/editor.php @@ -15,30 +15,30 @@ return [ 'close' => 'Закрыть', 'undo' => 'Отменить', 'redo' => 'Повторить', - 'left' => 'Left', - 'center' => 'Center', - 'right' => 'Right', - 'top' => 'Top', - 'middle' => 'Middle', - 'bottom' => 'Bottom', - 'width' => 'Width', - 'height' => 'Height', + 'left' => 'Слева', + 'center' => 'По центру', + 'right' => 'Справа', + 'top' => 'Сверху', + 'middle' => 'Посередине', + 'bottom' => 'Снизу', + 'width' => 'Ширина', + 'height' => 'Высота', 'More' => 'Еще', // Toolbar - 'formats' => 'Formats', - 'header_large' => 'Крупный заголовок', - 'header_medium' => 'Средний заголовок', - 'header_small' => 'Небольшой заголовок', - 'header_tiny' => 'Маленький заголовок', - 'paragraph' => 'Абзац', + 'formats' => 'Форматы', + 'header_large' => 'Большой', + 'header_medium' => 'Средний', + 'header_small' => 'Маленький', + 'header_tiny' => 'Крошечный', + 'paragraph' => 'Обычный текст', 'blockquote' => 'Цитата', 'inline_code' => 'Встроенный код', 'callouts' => 'Выноска', 'callout_information' => 'Информация', - 'callout_success' => 'Успешно', + 'callout_success' => 'Успех', 'callout_warning' => 'Предупреждение', - 'callout_danger' => 'Опасность', + 'callout_danger' => 'Ошибка', 'bold' => 'Жирный', 'italic' => 'Курсив', 'underline' => 'Подчёркнутый', @@ -47,8 +47,8 @@ return [ 'subscript' => 'Подстрочный', 'text_color' => 'Цвет текста', 'custom_color' => 'Пользовательский цвет', - 'remove_color' => 'Убрать цвет', - 'background_color' => 'Фоновый цвет', + 'remove_color' => 'Удалить цвет', + 'background_color' => 'Цвет фона', 'align_left' => 'По левому краю', 'align_center' => 'По центру', 'align_right' => 'По правому краю', @@ -71,7 +71,7 @@ return [ 'clear_formatting' => 'Очистить форматирование', 'source_code' => 'Исходный код', 'source_code_title' => 'Исходный код', - 'fullscreen' => 'Полный экран', + 'fullscreen' => 'Полноэкранный режим', 'image_options' => 'Параметры изображения', // Tables @@ -91,7 +91,7 @@ return [ 'cell_properties_title' => 'Свойства ячейки', 'cell_type' => 'Тип ячейки', 'cell_type_cell' => 'Ячейка', - 'cell_type_header' => 'Header cell', + 'cell_type_header' => 'Заголовок ячейки', 'table_row_group' => 'Объединить строки', 'table_column_group' => 'Объединить столбцы', 'horizontal_align' => 'Выровнять по горизонтали', @@ -107,44 +107,44 @@ return [ 'paste_row_after' => 'Вставить строку ниже', 'row_type' => 'Тип строки', 'row_type_header' => 'Заголовок', - 'row_type_body' => 'Body', - 'row_type_footer' => 'Footer', + 'row_type_body' => 'Тело', + 'row_type_footer' => 'Нижняя часть', 'alignment' => 'Выравнивание', 'cut_column' => 'Вырезать столбец', 'copy_column' => 'Копировать столбец', 'paste_column_before' => 'Вставить столбец слева', 'paste_column_after' => 'Вставить столбец справа', - 'cell_padding' => 'Cell padding', - 'cell_spacing' => 'Cell spacing', - 'caption' => 'Caption', - 'show_caption' => 'Show caption', - 'constrain' => 'Constrain proportions', + 'cell_padding' => 'Расстояние между границей и содержимым', + 'cell_spacing' => 'Расстояние между ячейками', + 'caption' => 'Подпись', + 'show_caption' => 'Показать подпись', + 'constrain' => 'Сохранять пропорции', // Images, links, details/summary & embed - 'source' => 'Source', - 'alt_desc' => 'Alternative description', - 'embed' => 'Embed', - 'paste_embed' => 'Paste your embed code below:', + 'source' => 'Источник', + 'alt_desc' => 'Альтернативное описание', + 'embed' => 'Код для вставки', + 'paste_embed' => 'Введите код для вставки ниже:', 'url' => 'URL-адрес', 'text_to_display' => 'Текст для отображения', 'title' => 'Заголовок', 'open_link' => 'Открыть ссылку в...', 'open_link_current' => 'В текущем окне', - 'open_link_new' => 'Новое окно', + 'open_link_new' => 'В новом окне', 'insert_collapsible' => 'Вставить свернутый блок', - 'collapsible_unwrap' => 'Unwrap', + 'collapsible_unwrap' => 'Удалить блок', 'edit_label' => 'Изменить метку', - 'toggle_open_closed' => 'Toggle open/closed', - 'collapsible_edit' => 'Edit collapsible block', - 'toggle_label' => 'Toggle label', + 'toggle_open_closed' => 'Развернуть/свернуть', + 'collapsible_edit' => 'Редактировать свернутый блок', + 'toggle_label' => 'Метка', // About view 'about_title' => 'О редакторе WYSIWYG', 'editor_license' => 'Лицензия редактора и авторские права', - 'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under an LGPL v2.1 license.', - 'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE Вы можете найти здесь.', + 'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под лицензией LGPL v2.1.', + 'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE вы можете найти здесь.', 'save_continue' => 'Сохранить страницу и продолжить', - 'callouts_cycle' => '(Keep pressing to toggle through types)', + 'callouts_cycle' => '(Держите нажатым для переключения типов)', 'shortcuts' => 'Сочетания клавиш', 'shortcut' => 'Сочетания клавиш', 'shortcuts_intro' => 'Следующие сочетания клавиш доступны в редакторе:', diff --git a/resources/views/layouts/export.blade.php b/resources/views/layouts/export.blade.php index a951e262d..36568fef4 100644 --- a/resources/views/layouts/export.blade.php +++ b/resources/views/layouts/export.blade.php @@ -4,6 +4,10 @@ @yield('title') + @if($cspContent ?? false) + + @endif + @include('common.export-styles', ['format' => $format, 'engine' => $engine ?? '']) @include('common.export-custom-head') diff --git a/tests/Entity/ExportTest.php b/tests/Entity/ExportTest.php index 445cd24f3..fc15bb8f3 100644 --- a/tests/Entity/ExportTest.php +++ b/tests/Entity/ExportTest.php @@ -268,7 +268,7 @@ class ExportTest extends TestCase foreach ($entities as $entity) { $resp = $this->asEditor()->get($entity->getUrl('/export/html')); $resp->assertDontSee('window.donkey'); - $resp->assertDontSee('script'); + $resp->assertDontSee('assertSee('.my-test-class { color: red; }'); } } @@ -448,4 +448,18 @@ class ExportTest extends TestCase $resp = $this->get($page->getUrl('/export/pdf')); $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 "]'); + } + } } diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index 78691badb..1a0a6c9b3 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -119,6 +119,25 @@ class SecurityHeaderTest extends TestCase $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() { $this->asEditor(); @@ -133,10 +152,14 @@ class SecurityHeaderTest extends TestCase */ 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) { - return strpos($val, $type) === 0; - })->first() ?? ''; + foreach ($cspHeaders as $cspHeader) { + if (strpos($cspHeader, $type) === 0) { + return $cspHeader; + } + } + + return ''; } }