]> BookStack Code Mirror - bookstack/commitdiff
Merge pull request #3298 from BookStackApp/wysiwyg_links
authorDan Brown <redacted>
Wed, 9 Mar 2022 14:29:03 +0000 (14:29 +0000)
committerGitHub <redacted>
Wed, 9 Mar 2022 14:29:03 +0000 (14:29 +0000)
WYSIWYG editor link updates

22 files changed:
.env.example.complete
.github/translators.txt
app/Config/app.php
app/Entities/Tools/ExportFormatter.php
app/Entities/Tools/PageContent.php
app/Http/Middleware/ApplyCspRules.php
app/Util/CspService.php
resources/js/wysiwyg/config.js
resources/lang/fa/editor.php
resources/lang/it/editor.php
resources/lang/ja/editor.php
resources/lang/ja/validation.php
resources/lang/ru/activities.php
resources/lang/ru/editor.php
resources/sass/_blocks.scss
resources/sass/_layout.scss
resources/sass/_lists.scss
resources/sass/_pages.scss
resources/views/layouts/export.blade.php
tests/Entity/ExportTest.php
tests/Entity/PageContentTest.php
tests/SecurityHeaderTest.php

index 9d24fceeb1ef4f086f65d65ed5e9ce704febd54f..fb947408dff96c79f5a2136034d2b6521f31479d 100644 (file)
@@ -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
index 1e1d2c2015bcb9f4399b29511efffde89db94d58..93a9c21f64a63077a388ecbcd2abd13eb5df4da6 100644 (file)
@@ -230,3 +230,5 @@ roncallyt :: Portuguese, Brazilian
 goegol :: Dutch
 msevgen :: Turkish
 Khroners :: French
+MASOUD HOSSEINY (masoudme) :: Persian
+Thomerson Roncally (roncallyt) :: Portuguese, Brazilian
index 39bfa7134f9e441e1b0686b750e72a05c1bdafdf..2329043b64270dccebb8f3ee9990439929ba9867 100644 (file)
@@ -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'),
 
index 7edd1b50f99d27b74448cee27dee0b46b7527bb4..9029d7270dc64d8767afdeb6f8755a6888d1ea4e 100644 (file)
@@ -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);
index dbb62021cae18c18e5233bb9a04e27da0874c4e3..b1c750adbdd6a3a3645c75e91b0f44c9e940bc6c 100644 (file)
@@ -239,6 +239,9 @@ class PageContent
             $html .= $doc->saveHTML($childNode);
         }
 
+        // Perform required string-level tweaks
+        $html = str_replace(' ', '&nbsp;', $html);
+
         return $html;
     }
 
index 6c9d14e7b6594af29c61d675533db0a78f2df271..9f3a8d1d84f157b69c95e8758e3f8b26f0d90d66 100644 (file)
@@ -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;
     }
index 812e1a4bed1cc2a1d937028e2f614ccf89cfa5ba..ba927c93b29556b8949fd365aca6892f8fa10eb4 100644 (file)
@@ -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,13 +22,51 @@ 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
+    {
+        $headers = [
+            $this->getFrameAncestors(),
+            $this->getFrameSrc(),
+            $this->getScriptSrc(),
+            $this->getObjectSrc(),
+            $this->getBaseUri(),
+        ];
+
+        return implode('; ', array_filter($headers));
+    }
+
+    /**
+     * Get the CSP rules for the application for a HTML meta tag.
+     */
+    public function getCspMetaTagValue(): string
+    {
+        $headers = [
+            $this->getFrameSrc(),
+            $this->getScriptSrc(),
+            $this->getObjectSrc(),
+            $this->getBaseUri(),
+        ];
+
+        return implode('; ', array_filter($headers));
+    }
+
+    /**
+     * Check if the user has configured some allowed iframe hosts.
+     */
+    public function allowedIFrameHostsConfigured(): bool
+    {
+        return count($this->getAllowedIframeHosts()) > 0;
+    }
+
+    /**
+     * Create CSP 'script-src' rule to restrict the forms of script that can run on the page.
+     */
+    protected function getScriptSrc(): string
     {
         if (config('app.allow_content_scripts')) {
-            return;
+            return '';
         }
 
         $parts = [
@@ -40,51 +76,50 @@ class CspService
             '\'strict-dynamic\'',
         ];
 
-        $value = 'script-src ' . implode(' ', $parts);
-        $response->headers->set('Content-Security-Policy', $value, false);
+        return 'script-src ' . implode(' ', $parts);
     }
 
     /**
-     * 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.
+     * Create CSP "frame-ancestors" rule to restrict the hosts that BookStack can be iframed within.
      */
-    public function setFrameAncestors(Response $response)
+    protected function getFrameAncestors(): string
     {
         $iframeHosts = $this->getAllowedIframeHosts();
         array_unshift($iframeHosts, "'self'");
-        $cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts);
-        $response->headers->set('Content-Security-Policy', $cspValue, false);
+        return 'frame-ancestors ' . implode(' ', $iframeHosts);
     }
 
     /**
-     * Check if the user has configured some allowed iframe hosts.
+     * 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.
      */
-    public function allowedIFrameHostsConfigured(): bool
+    protected function getFrameSrc(): string
     {
-        return count($this->getAllowedIframeHosts()) > 0;
+        $iframeHosts = $this->getAllowedIframeSources();
+        array_unshift($iframeHosts, "'self'");
+        return 'frame-src ' . implode(' ', $iframeHosts);
     }
 
     /**
-     * Sets CSP 'object-src' headers to restrict the types of dynamic content
+     * Creates CSP 'object-src' rule to restrict the types of dynamic content
      * that can be embedded on the page.
      */
-    public function setObjectSrc(Response $response)
+    protected function getObjectSrc(): string
     {
         if (config('app.allow_content_scripts')) {
-            return;
+            return '';
         }
 
-        $response->headers->set('Content-Security-Policy', 'object-src \'self\'', false);
+        return "object-src 'self'";
     }
 
     /**
-     * Sets CSP 'base-uri' headers to restrict what base tags can be set on
+     * 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;
+    }
 }
index 2da1e2c989d704bb8961bc330494b66c360c47a7..965b14d083699d6c5bb1ce6adb1b38c3eeee4ea2 100644 (file)
@@ -178,11 +178,15 @@ export function build(options) {
     // Set language
     window.tinymce.addI18n(options.language, options.translationMap);
 
+    // BookStack Version
+    const version = document.querySelector('script[src*="/dist/app.js"]').getAttribute('src').split('?version=')[1];
+
     // Return config object
     return {
         width: '100%',
         height: '100%',
         selector: '#html-editor',
+        cache_suffix: '?version=' + version,
         content_css: [
             window.baseUrl('/dist/styles.css'),
         ],
@@ -196,6 +200,7 @@ export function build(options) {
         remove_script_host: false,
         document_base_url: window.baseUrl('/'),
         end_container_on_empty_block: true,
+        remove_trailing_brs: false,
         statusbar: false,
         menubar: false,
         paste_data_images: false,
index 76a9f7fca07fd590f758ea99b6086aabf1d0b994..22d463365a7443a719bbc0547121ed8b495beab6 100644 (file)
@@ -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',
index 3e1a2c3499fb282e52619c7f47757502ac1370dc..937ff6f5de6c5ce18fcaec0498e4b56735ab2870 100644 (file)
@@ -131,12 +131,12 @@ return [
     'open_link' => 'Apri collegamento in...',
     'open_link_current' => 'Finestra corrente',
     'open_link_new' => 'Nuova finestra',
-    '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',
+    'insert_collapsible' => 'Inserisci blocco collassabile',
+    'collapsible_unwrap' => 'Espandi',
+    'edit_label' => 'Modifica etichetta',
+    'toggle_open_closed' => 'Espandi/Comprimi',
+    'collapsible_edit' => 'Modifica blocco collassabile',
+    'toggle_label' => 'Attiva/Disattiva etichetta',
 
     // About view
     'about_title' => 'Informazioni sull\'editor di WYSIWYG',
index 76a9f7fca07fd590f758ea99b6086aabf1d0b994..de38b0cbe53bb3acae89ca81c5989d04d616ed8d 100644 (file)
  */
 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' => 'テンプレートの内容',
 ];
index 4c96087f912c5cc513c50940f3d8aedf200cba08..96ae7dfff249a102b407ffa85543e4939efdcdb5 100644 (file)
@@ -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より大きな値である必要があります。',
index 47a589f3f7d3dc009c72d6ae66949c3c1f05c8d4..a015e183d9c41804a9347023353a0f078a962f54 100644 (file)
@@ -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'                => 'прокомментировал',
index 44021900a290240b66bea879ea41555cc77c70f9..45a8d1e90c0af87fe5d86e28f53c76071d43f493 100644 (file)
@@ -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' => 'Ð\9aÑ\80Ñ\83пнÑ\8bй Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²Ð¾Ðº',
-    'header_medium' => 'Средний заголовок',
-    'header_small' => 'Ð\9dеболÑ\8cÑ\88ой Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²Ð¾Ðº',
-    'header_tiny' => 'Ð\9cаленÑ\8cкий Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²Ð¾Ðº',
-    'paragraph' => 'Ð\90бзаÑ\86',
+    'formats' => 'Форматы',
+    'header_large' => 'Ð\91олÑ\8cÑ\88ой',
+    'header_medium' => 'Средний',
+    'header_small' => 'Ð\9cаленÑ\8cкий',
+    'header_tiny' => 'Ð\9aÑ\80оÑ\88еÑ\87нÑ\8bй',
+    'paragraph' => 'Ð\9eбÑ\8bÑ\87нÑ\8bй Ñ\82екÑ\81Ñ\82',
     'blockquote' => 'Цитата',
     'inline_code' => 'Встроенный код',
     'callouts' => 'Выноска',
     'callout_information' => 'Информация',
-    'callout_success' => 'УÑ\81пеÑ\88но',
+    'callout_success' => 'УÑ\81пеÑ\85',
     'callout_warning' => 'Предупреждение',
-    'callout_danger' => 'Опасность',
+    'callout_danger' => 'Ошибка',
     'bold' => 'Жирный',
     'italic' => 'Курсив',
     'underline' => 'Подчёркнутый',
@@ -47,8 +47,8 @@ return [
     'subscript' => 'Подстрочный',
     'text_color' => 'Цвет текста',
     'custom_color' => 'Пользовательский цвет',
-    'remove_color' => 'УбÑ\80ать цвет',
-    'background_color' => 'ФоновÑ\8bй Ñ\86веÑ\82',
+    'remove_color' => 'Удалить цвет',
+    'background_color' => 'ЦвеÑ\82 Ñ\84она',
     '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' => 'Ð\9dовое Ð¾ÐºÐ½Ð¾',
+    'open_link_new' => 'Ð\92 Ð½Ð¾Ð²Ð¾Ð¼ Ð¾ÐºÐ½Ðµ',
     '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' => 'Ð\90вÑ\82оÑ\80Ñ\81кие Ð¿Ñ\80ава Ð¸ Ð¿Ð¾Ð´Ñ\80обноÑ\81Ñ\82и Ð»Ð¸Ñ\86ензии TinyMCE Ð\92ы можете найти здесь.',
+    'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под лицензией LGPL v2.1.',
+    'editor_tiny_license_link' => 'Ð\90вÑ\82оÑ\80Ñ\81кие Ð¿Ñ\80ава Ð¸ Ð¿Ð¾Ð´Ñ\80обноÑ\81Ñ\82и Ð»Ð¸Ñ\86ензии TinyMCE Ð²ы можете найти здесь.',
     'save_continue' => 'Сохранить страницу и продолжить',
-    'callouts_cycle' => '(Keep pressing to toggle through types)',
+    'callouts_cycle' => '(Держите нажатым для переключения типов)',
     'shortcuts' => 'Сочетания клавиш',
     'shortcut' => 'Сочетания клавиш',
     'shortcuts_intro' => 'Следующие сочетания клавиш доступны в редакторе:',
index 0a7a689f7dec53043ffbf576ba3c418f06eb6d43..7d408cd1b5f24fdcb41f42e18e8b680bf9baa899 100644 (file)
 }
 
 .fade-in-when-active {
-  opacity: 0.6;
+  @include lightDark(opacity, 0.6, 0.7);
   transition: opacity ease-in-out 120ms;
   &:hover, &:focus-within {
-    opacity: 1;
+    opacity: 1 !important;
   }
   @media (prefers-contrast: more) {
-    opacity: 1;
+    opacity: 1 !important;
   }
 }
 
index 783ccc8f9476cb455e71c6186c346e2695ba1b9e..69882d40deb4c5eafe551c8cd0835dfc066b3ad1 100644 (file)
@@ -361,16 +361,13 @@ body.flexbox {
     display: none;
   }
   .tri-layout-left-contents > *, .tri-layout-right-contents > * {
-    opacity: 0.6;
+    @include lightDark(opacity, 0.6, 0.7);
     transition: opacity ease-in-out 120ms;
-    &:hover {
-      opacity: 1;
-    }
-    &:focus-within {
-      opacity: 1;
+    &:hover, &:focus-within {
+      opacity: 1 !important;
     }
     @media (prefers-contrast: more) {
-      opacity: 1;
+      opacity: 1 !important;
     }
   }
 
index c46ac84f35e45f09ebd89cf31482fb12230af8ea..8febdcffcd4f681d4ba5b2b57ccc2ea37b56608c 100644 (file)
     }
   }
   .entity-list-item.selected {
-    background-color: rgba(0, 0, 0, 0.08);
+    @include lightDark(background-color, rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.08));
   }
   .entity-list-item.no-hover {
     margin-top: -$-xs;
index af5bea0f1770822ceb3d22dfe9321cd59082bcb6..8103ca20d1c8ceb67b2626230ce79ae2c5b3664c 100755 (executable)
@@ -164,6 +164,10 @@ body.tox-fullscreen, body.markdown-fullscreen {
     clear: both;
   }
 
+  p:empty {
+    min-height: 1.6em;
+  }
+
   &.page-revision {
     pre code {
       white-space: pre-wrap;
index a951e262de7b368c821f3b3b5c3b4bd0fca095ed..36568fef4dad22efa7876f9825ebd90336b9d6bb 100644 (file)
@@ -4,6 +4,10 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
     <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-custom-head')
 </head>
index 445cd24f3848b2bc77e3165d610c4238f49d9232..fc15bb8f3b3a916d3530cd8ba879866518a48b90 100644 (file)
@@ -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('<script', false);
             $resp->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 "]');
+        }
+    }
 }
index 20cde049a1938d46e4aa55fe48a6da33fd90bb80..b9680d23fc80598746bff0f096255b9083ceda1d 100644 (file)
@@ -692,35 +692,43 @@ class PageContentTest extends TestCase
 
     public function test_base64_images_within_markdown_blanked_if_not_supported_extension_for_extract()
     {
-        $this->asEditor();
         $page = Page::query()->first();
 
-        $this->put($page->getUrl(), [
+        $this->asEditor()->put($page->getUrl(), [
             'name'     => $page->name, 'summary' => '',
             'markdown' => 'test ![test](data:image/jiff;base64,' . $this->base64Jpeg . ')',
         ]);
 
-        $page->refresh();
-        $this->assertStringContainsString('<img src=""', $page->html);
+        $this->assertStringContainsString('<img src=""', $page->refresh()->html);
     }
 
     public function test_nested_headers_gets_assigned_an_id()
     {
-        $this->asEditor();
         $page = Page::query()->first();
 
         $content = '<table><tbody><tr><td><h5>Simple Test</h5></td></tr></tbody></table>';
-        $this->put($page->getUrl(), [
+        $this->asEditor()->put($page->getUrl(), [
             'name'    => $page->name,
             'html'    => $content,
-            'summary' => '',
         ]);
 
-        $updatedPage = Page::query()->where('id', '=', $page->id)->first();
-
         // The top level <table> node will get assign the bkmrk-simple-test id because the system will
         // take the node value of h5
         // So the h5 should get the bkmrk-simple-test-1 id
-        $this->assertStringContainsString('<h5 id="bkmrk-simple-test-1">Simple Test</h5>', $updatedPage->html);
+        $this->assertStringContainsString('<h5 id="bkmrk-simple-test-1">Simple Test</h5>', $page->refresh()->html);
+    }
+
+    public function test_non_breaking_spaces_are_preserved()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+
+        $content = '<p>&nbsp;</p>';
+        $this->asEditor()->put($page->getUrl(), [
+            'name'    => $page->name,
+            'html'    => $content,
+        ]);
+
+        $this->assertStringContainsString('<p id="bkmrk-%C2%A0">&nbsp;</p>', $page->refresh()->html);
     }
 }
index 78691badbbc5eb3e2b26ee140024636f98726450..1a0a6c9b3a1ad4b1fd964d547b2218848de4f984 100644 (file)
@@ -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'));
+
+        foreach ($cspHeaders as $cspHeader) {
+            if (strpos($cspHeader, $type) === 0) {
+                return $cspHeader;
+            }
+        }
 
-        return $cspHeaders->filter(function ($val) use ($type) {
-            return strpos($val, $type) === 0;
-        })->first() ?? '';
+        return '';
     }
 }