# 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.
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
# 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
goegol :: Dutch
msevgen :: Turkish
Khroners :: French
+MASOUD HOSSEINY (masoudme) :: Persian
+Thomerson Roncally (roncallyt) :: Portuguese, Brazilian
// 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'),
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;
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;
}
/**
{
$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);
$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);
'book' => $book,
'bookChildren' => $bookTree,
'format' => 'html',
+ 'cspContent' => $this->cspService->getCspMetaTagValue(),
])->render();
return $this->containHtml($html);
$html .= $doc->saveHTML($childNode);
}
+ // Perform required string-level tweaks
+ $html = str_replace(' ', ' ', $html);
+
return $html;
}
class ApplyCspRules
{
- /**
- * @var CspService
- */
- protected $cspService;
+ protected CspService $cspService;
public function __construct(CspService $cspService)
{
$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;
}
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 = '')
{
}
/**
- * 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 = [
'\'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
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;
+ }
}
// 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'),
],
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,
*/
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',
'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',
*/
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' => 'テンプレートの内容',
];
'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より大きな値である必要があります。',
'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' => 'прокомментировал',
'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' => 'Подчёркнутый',
'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' => 'По правому краю',
'clear_formatting' => 'Очистить форматирование',
'source_code' => 'Исходный код',
'source_code_title' => 'Исходный код',
- 'fullscreen' => 'Полный экран',
+ 'fullscreen' => 'Полноэкранный режим',
'image_options' => 'Параметры изображения',
// Tables
'cell_properties_title' => 'Свойства ячейки',
'cell_type' => 'Тип ячейки',
'cell_type_cell' => 'Ячейка',
- 'cell_type_header' => 'Header cell',
+ 'cell_type_header' => 'Заголовок ячейки',
'table_row_group' => 'Объединить строки',
'table_column_group' => 'Объединить столбцы',
'horizontal_align' => 'Выровнять по горизонтали',
'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' => 'Следующие сочетания клавиш доступны в редакторе:',
}
.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;
}
}
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;
}
}
}
}
.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;
clear: both;
}
+ p:empty {
+ min-height: 1.6em;
+ }
+
&.page-revision {
pre code {
white-space: pre-wrap;
<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>
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; }');
}
}
$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 "]');
+ }
+ }
}
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 ',
]);
- $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> </p>';
+ $this->asEditor()->put($page->getUrl(), [
+ 'name' => $page->name,
+ 'html' => $content,
+ ]);
+
+ $this->assertStringContainsString('<p id="bkmrk-%C2%A0"> </p>', $page->refresh()->html);
}
}
$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();
*/
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 '';
}
}