Compare commits

..

18 Commits

Author SHA1 Message Date
Dan Brown
11a1a6fb16 Updated version and assets for release v22.02.3 2022-03-07 15:12:22 +00:00
Dan Brown
882c609296 Merge branch 'development' into release 2022-03-07 15:12:09 +00:00
Dan Brown
77ad819970 Updated translation attribution before v22.02.3 release 2022-03-07 15:06:44 +00:00
Dan Brown
2835e5be93 New Crowdin updates (#3312) 2022-03-07 15:06:21 +00:00
Dan Brown
856fca8289 Updated CSP with frame-src rules
- Configurable via 'ALLOWED_IFRAME_SOURCES' .env option.
- Also updated how CSP rules are set, with a single header being used
  instead of many.
- Also applied CSP rules to HTML export outputs.
- Updated tests to cover.

For #3314
2022-03-07 14:27:41 +00:00
Dan Brown
48d0095aa2 Added mysql-ssl-ca option to complete .env 2022-03-02 21:51:18 +00:00
Dan Brown
176a0dcd59 Updated version and assets for release v22.02.2 2022-03-01 22:45:41 +00:00
Dan Brown
94b0f70bfa Merge branch 'development' into release 2022-03-01 22:45:12 +00:00
Dan Brown
36d7ff77a9 New translations editor.php (Italian) (#3301) 2022-03-01 22:32:43 +00:00
Dan Brown
fb16ac326f Reduced dynamic fade in dark mode
For #3203
2022-03-01 22:29:31 +00:00
Dan Brown
5947f59a04 Updated strategy for empty newline sections
- For some reason, TinyMCE would handle empty paragraphs with a '&nbsp'
  by default but this would be removed when the paragraph had an
  attribute. This was fine in the old editor.
- This changes the approach to use '<br>' tags within elements
  for "spaced emptiness".
- For compatbility with any existing empty paragraphs, I updated the
  styles to show default height for empty paragraph sections.
- This also makes changes to help preserve encoded &nbsp; html tags
  since they were getting converted along the journey.

Related to #3302
2022-03-01 17:26:06 +00:00
Dan Brown
1843d80fb7 Added cache breaker to tinymce loading systems
Takes the version from BookStack app.js paths instead of tinyMCE version
since things external from TinyMCE could be loaded using this.
2022-03-01 13:41:53 +00:00
Dan Brown
08b2a77d41 Updated version and assets for release v22.02.1 2022-02-27 17:46:06 +00:00
Dan Brown
3e8e9a23cf Merge branch 'development' into release 2022-02-27 17:45:49 +00:00
Dan Brown
1253711c7d New translations editor.php (Chinese Simplified) (#3291) 2022-02-27 17:44:58 +00:00
Dan Brown
963d8f4693 Updated issue templates, readme and dev version
- Updated bug report template to capture browser.
- Updated readme roadmap.
- Bumped dev version.
2022-02-27 17:26:27 +00:00
Dan Brown
0de4d6d223 Improved WYSIWYG code block behaviour via range of fixes
- Fixed issues with new code blocks breaking or acting odd due to
  misnamed contenteditable attribute.
- Helped fix issue where code blocks may show in a strage blank state
  due to timing within shadow dom loading.
- Fixed some function timing issues where some functions required their
  async predecessor to have finished.

Tested rather heavily in firefox and brave.
Fixes #3292
2022-02-27 17:21:24 +00:00
Dan Brown
06f694bad2 Updated tinymce link query to break caches
Fixes #3293
2022-02-27 16:03:18 +00:00
33 changed files with 472 additions and 314 deletions

View 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

View File

@@ -35,6 +35,15 @@ body:
description: Provide any additional context and screenshots here to help us solve this issue
validations:
required: false
- type: input
id: browserdetails
attributes:
label: Browser Details
description: |
If this is an issue that occurs when using the BookStack interface, please provide details of the browser used which presents the reported issue.
placeholder: (eg. Firefox 97 (64-bit) on Windows 11)
validations:
required: false
- type: input
id: bsversion
attributes:

View File

@@ -33,8 +33,7 @@ body:
attributes:
label: Have you searched for an existing open/closed issue?
description: |
To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue)
for any existing issues that cover the fundemental benefit/goal of your request.
To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundemental benefit/goal of your request.
options:
- label: I have searched for existing issues and none cover my fundemental request
required: true

View File

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

View File

@@ -57,6 +57,13 @@ return [
// Space separated if multiple. BookStack host domain is auto-inferred.
'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'),

View 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);

View File

@@ -239,6 +239,9 @@ class PageContent
$html .= $doc->saveHTML($childNode);
}
// Perform required string-level tweaks
$html = str_replace(' ', '&nbsp;', $html);
return $html;
}

View 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;
}

View 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,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;
}
}

6
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -63,7 +63,7 @@ Below is a high-level road map view for BookStack to provide a sense of directio
- **Platform REST API** - *(Most actions implemented, maturing)*
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
- **Editor Alignment & Review** - *(Started)*
- **Editor Alignment & Review** - *(In Progress)*
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
- **Permission System Review**
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*

View File

@@ -59,12 +59,10 @@ class CodeEditor {
this.languageInput.value = language;
this.callback = callback;
this.show();
this.updateEditorMode(language);
window.importVersioned('code').then(Code => {
Code.setContent(this.editor, code);
});
this.show()
.then(() => this.updateEditorMode(language))
.then(() => window.importVersioned('code'))
.then(Code => Code.setContent(this.editor, code));
}
async show() {

View File

@@ -221,11 +221,15 @@ export function build(options) {
// Build toolbar content
const {toolbar, groupButtons: toolBarGroupButtons} = buildToolbar(options);
// 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'),
],
@@ -239,6 +243,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,

View File

@@ -91,15 +91,35 @@ function defineCodeBlockCustomElement(editor) {
}
connectedCallback() {
const connectedTime = Date.now();
if (this.cm) {
return;
}
this.cleanChildContent();
const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
importVersioned('code').then(Code => {
const renderCodeMirror = (Code) => {
this.cm = Code.wysiwygView(container, this.getContent(), this.getLanguage());
Code.updateLayout(this.cm);
};
window.importVersioned('code').then((Code) => {
const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
setTimeout(() => renderCodeMirror(Code), timeout);
});
}
cleanChildContent() {
const pre = this.querySelector('pre');
if (!pre) return;
for (const preChild of pre.childNodes) {
if (preChild.nodeName === '#text' && preChild.textContent === '') {
preChild.remove();
}
}
}
}
win.customElements.define('code-block', CodeBlockElement);
@@ -130,15 +150,13 @@ function register(editor, url) {
} else {
const textContent = editor.selection.getContent({format: 'text'});
showPopup(editor, textContent, '', (newCode, newLang) => {
const wrap = doc.createElement('code-block');
const pre = doc.createElement('pre');
const code = doc.createElement('code');
code.classList.add(`language-${newLang}`);
code.innerText = newCode;
pre.append(code);
wrap.append(pre);
editor.insertContent(wrap.outerHTML);
editor.insertContent(pre.outerHTML);
});
}
});
@@ -168,7 +186,7 @@ function register(editor, url) {
editor.parser.addNodeFilter('code-block', function(elms) {
for (const el of elms) {
el.attr('content-editable', 'false');
el.attr('contenteditable', 'false');
}
});

View 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',

View 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',

View File

@@ -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' => 'テンプレートの内容',
];

View 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より大きな値である必要があります。',

View 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' => 'прокомментировал',

View 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' => 'Крупный заголовок',
'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' => 'Следующие сочетания клавиш доступны в редакторе:',

View File

@@ -57,7 +57,7 @@ return [
'list_numbered' => '有序列表',
'indent_increase' => '增加缩进',
'indent_decrease' => '减少缩进',
'table' => '表',
'table' => '表',
'insert_image' => '插入图片',
'insert_image_title' => '插入/编辑图片',
'insert_link' => '插入/编辑链接',

View File

@@ -238,13 +238,13 @@
}
.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;
}
}

View 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;
}
}

View File

@@ -193,7 +193,7 @@
}
}
.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;

View File

@@ -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;

View 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>

View File

@@ -1,7 +1,7 @@
@extends('layouts.base')
@section('head')
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=4.9.4') }}" nonce="{{ $cspNonce }}"></script>
<script src="{{ url('/libs/tinymce/tinymce.min.js?ver=5.10.2') }}" nonce="{{ $cspNonce }}"></script>
@stop
@section('body-class', 'flexbox')

View 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 "]');
}
}
}

View 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);
}
}

View 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'));
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 '';
}
}

View File

@@ -1 +1 @@
v22.02
v22.02.3