Merge branch 'master' into release

This commit is contained in:
Dan Brown 2020-09-30 22:44:17 +01:00
commit 76e30869e1
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
15 changed files with 80 additions and 58 deletions

View File

@ -238,9 +238,9 @@ DISABLE_EXTERNAL_SERVICES=false
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon # Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL= AVATAR_URL=
# Enable draw.io integration # Enable diagrams.net integration
# Can simply be true/false to enable/disable the integration. # Can simply be true/false to enable/disable the integration.
# Alternatively, It can be URL to the draw.io instance you want to use. # Alternatively, It can be URL to the diagrams.net instance you want to use.
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1 # For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
DRAWIO=true DRAWIO=true

View File

@ -2,7 +2,6 @@
use BookStack\Entities\Page; use BookStack\Entities\Page;
use DOMDocument; use DOMDocument;
use DOMElement;
use DOMNodeList; use DOMNodeList;
use DOMXPath; use DOMXPath;
@ -44,18 +43,24 @@ class PageContent
$container = $doc->documentElement; $container = $doc->documentElement;
$body = $container->childNodes->item(0); $body = $container->childNodes->item(0);
$childNodes = $body->childNodes; $childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes // Set ids on top-level nodes
$idMap = []; $idMap = [];
foreach ($childNodes as $index => $childNode) { foreach ($childNodes as $index => $childNode) {
$this->setUniqueId($childNode, $idMap); [$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
} }
// Ensure no duplicate ids within child items // Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]'); $idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) { foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap); [$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
} }
// Generate inner html as a string // Generate inner html as a string
@ -67,23 +72,34 @@ class PageContent
return $html; return $html;
} }
/**
* Update the all links to the $old location to instead point to $new.
*/
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
}
}
/** /**
* Set a unique id on the given DOMElement. * Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence. * A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element * Returns a pair of strings in the format [old_id, new_id]
* @param array $idMap
*/ */
protected function setUniqueId($element, array &$idMap) protected function setUniqueId(\DOMNode $element, array &$idMap): array
{ {
if (get_class($element) !== 'DOMElement') { if (get_class($element) !== 'DOMElement') {
return; return ['', ''];
} }
// Overwrite id if not a BookStack custom id // Stop if there's an existing valid id that has not already been used.
$existingId = $element->getAttribute('id'); $existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) { if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true; $idMap[$existingId] = true;
return; return [$existingId, $existingId];
} }
// Create an unique id for the element // Create an unique id for the element
@ -100,6 +116,7 @@ class PageContent
$element->setAttribute('id', $newId); $element->setAttribute('id', $newId);
$idMap[$newId] = true; $idMap[$newId] = true;
return [$existingId, $newId];
} }
/** /**

24
package-lock.json generated
View File

@ -253,9 +253,9 @@
} }
}, },
"esbuild": { "esbuild": {
"version": "0.6.30", "version": "0.7.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.6.30.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
"integrity": "sha512-ZSZY461UPzTYYC3rqy1QiMtngk2WyXf+58MgC7tC22jkI90FXNgEl0hN3ipfn/UgZYzTW2GBcHiO7t0rSbHT7g==", "integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
"dev": true "dev": true
}, },
"escape-string-regexp": { "escape-string-regexp": {
@ -496,9 +496,9 @@
"dev": true "dev": true
}, },
"markdown-it": { "markdown-it": {
"version": "11.0.0", "version": "11.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.0.tgz", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
"integrity": "sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==", "integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"entities": "~2.0.0", "entities": "~2.0.0",
@ -730,9 +730,9 @@
} }
}, },
"sass": { "sass": {
"version": "1.26.10", "version": "1.26.11",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.10.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
"integrity": "sha512-bzN0uvmzfsTvjz0qwccN1sPm2HxxpNI/Xa+7PlUEMS+nQvbyuEK7Y0qFqxlPHhiNHb1Ze8WQJtU31olMObkAMw==", "integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
"dev": true, "dev": true,
"requires": { "requires": {
"chokidar": ">=2.0.0 <4.0.0" "chokidar": ">=2.0.0 <4.0.0"
@ -777,9 +777,9 @@
"dev": true "dev": true
}, },
"sortablejs": { "sortablejs": {
"version": "1.10.2", "version": "1.12.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.12.0.tgz",
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==" "integrity": "sha512-bPn57rCjBRlt2sC24RBsu40wZsmLkSo2XeqG8k6DC1zru5eObQUIPPZAQG7W2SJ8FZQYq+BEJmvuw1Zxb3chqg=="
}, },
"spdx-correct": { "spdx-correct": {
"version": "3.1.1", "version": "3.1.1",

View File

@ -4,9 +4,9 @@
"build:css:dev": "sass ./resources/sass:./public/dist", "build:css:dev": "sass ./resources/sass:./public/dist",
"build:css:watch": "sass ./resources/sass:./public/dist --watch", "build:css:watch": "sass ./resources/sass:./public/dist --watch",
"build:css:production": "sass ./resources/sass:./public/dist -s compressed", "build:css:production": "sass ./resources/sass:./public/dist -s compressed",
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2020", "build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
"build:js:watch": "chokidar \"./resources/**/*.js\" -c \"npm run build:js:dev\"", "build:js:watch": "chokidar \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --minify", "build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
"build": "npm-run-all --parallel build:*:dev", "build": "npm-run-all --parallel build:*:dev",
"production": "npm-run-all --parallel build:*:production", "production": "npm-run-all --parallel build:*:production",
"dev": "npm-run-all --parallel watch livereload", "dev": "npm-run-all --parallel watch livereload",
@ -16,18 +16,18 @@
}, },
"devDependencies": { "devDependencies": {
"chokidar-cli": "^2.1.0", "chokidar-cli": "^2.1.0",
"esbuild": "0.6.30", "esbuild": "0.7.8",
"livereload": "^0.9.1", "livereload": "^0.9.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"sass": "^1.26.10" "sass": "^1.26.11"
}, },
"dependencies": { "dependencies": {
"clipboard": "^2.0.6", "clipboard": "^2.0.6",
"codemirror": "^5.58.1", "codemirror": "^5.58.1",
"dropzone": "^5.7.2", "dropzone": "^5.7.2",
"markdown-it": "^11.0.0", "markdown-it": "^11.0.1",
"markdown-it-task-lists": "^2.1.1", "markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.10.2" "sortablejs": "^1.12.0"
} }
} }

View File

@ -11,9 +11,10 @@
# Redirect Trailing Slashes If Not A Folder... # Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301] RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Handle Front Controller... # Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L] RewriteRule ^ index.php [L]

View File

@ -168,6 +168,6 @@ These are the great open-source projects used to help build BookStack:
* [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy) * [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper) * [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html) * [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
* [Draw.io](https://github.com/jgraph/drawio) * [diagrams.net](https://github.com/jgraph/drawio)
* [Laravel Stats](https://github.com/stefanzweifel/laravel-stats) * [Laravel Stats](https://github.com/stefanzweifel/laravel-stats)
* [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml) * [OneLogin's SAML PHP Toolkit](https://github.com/onelogin/php-saml)

View File

@ -1,4 +1,4 @@
import {Sortable, MultiDrag} from "sortablejs"; import Sortable from "sortablejs";
// Auto sort control // Auto sort control
const sortOperations = { const sortOperations = {
@ -43,7 +43,6 @@ class BookSort {
this.input = elem.querySelector('[book-sort-input]'); this.input = elem.querySelector('[book-sort-input]');
const initialSortBox = elem.querySelector('.sort-box'); const initialSortBox = elem.querySelector('.sort-box');
Sortable.mount(new MultiDrag());
this.setupBookSortable(initialSortBox); this.setupBookSortable(initialSortBox);
this.setupSortPresets(); this.setupSortPresets();

View File

@ -141,10 +141,14 @@ async function request(url, options = {}) {
/** /**
* Get the content from a fetch response. * Get the content from a fetch response.
* Checks the content-type header to determine the format. * Checks the content-type header to determine the format.
* @param response * @param {Response} response
* @returns {Promise<Object|String>} * @returns {Promise<Object|String>}
*/ */
async function getResponseContent(response) { async function getResponseContent(response) {
if (response.status === 204) {
return null;
}
const responseContentType = response.headers.get('Content-Type'); const responseContentType = response.headers.get('Content-Type');
const subType = responseContentType.split('/').pop(); const subType = responseContentType.split('/').pop();

View File

@ -1,6 +1,7 @@
<div component="ajax-form" <div component="ajax-form"
option:ajax-form:url="/attachments/{{ $attachment->id }}" option:ajax-form:url="/attachments/{{ $attachment->id }}"
option:ajax-form:method="put" option:ajax-form:method="put"
option:ajax-form:response-container=".attachment-edit-container"
option:ajax-form:success-message="{{ trans('entities.attachments_updated_success') }}"> option:ajax-form:success-message="{{ trans('entities.attachments_updated_success') }}">
<h5>{{ trans('entities.attachments_edit_file') }}</h5> <h5>{{ trans('entities.attachments_edit_file') }}</h5>

View File

@ -4,6 +4,7 @@
<div component="ajax-form" <div component="ajax-form"
option:ajax-form:url="/attachments/link" option:ajax-form:url="/attachments/link"
option:ajax-form:method="post" option:ajax-form:method="post"
option:ajax-form:response-container=".link-form-container"
option:ajax-form:success-message="{{ trans('entities.attachments_link_attached') }}"> option:ajax-form:success-message="{{ trans('entities.attachments_link_attached') }}">
<input type="hidden" name="attachment_link_uploaded_to" value="{{ $pageId }}"> <input type="hidden" name="attachment_link_uploaded_to" value="{{ $pageId }}">
<p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p> <p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>

View File

@ -24,14 +24,14 @@
'successMessage' => trans('entities.attachments_file_uploaded'), 'successMessage' => trans('entities.attachments_file_uploaded'),
]) ])
</div> </div>
<div refs="tabs@contentLinks" class="hidden"> <div refs="tabs@contentLinks" class="hidden link-form-container">
@include('attachments.manager-link-form', ['pageId' => $page->id]) @include('attachments.manager-link-form', ['pageId' => $page->id])
</div> </div>
</div> </div>
</div> </div>
<div refs="attachments@editContainer" class="hidden"> <div refs="attachments@editContainer" class="hidden attachment-edit-container">
</div> </div>

View File

@ -1,7 +1,7 @@
<div component="page-editor" class="page-editor flex-fill flex" <div component="page-editor" class="page-editor flex-fill flex"
option:page-editor:drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}" option:page-editor:drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
@if(config('services.drawio')) @if(config('services.drawio'))
drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://www.draw.io/?embed=1&proto=json&spin=1' }}" drawio-url="{{ is_string(config('services.drawio')) ? config('services.drawio') : 'https://embed.diagrams.net/?embed=1&proto=json&spin=1' }}"
@endif @endif
@if($model->name === trans('entities.pages_initial_name')) @if($model->name === trans('entities.pages_initial_name'))
option:page-editor:has-default-title="true" option:page-editor:has-default-title="true"

View File

@ -196,24 +196,6 @@ class Saml2Test extends TestCase
}); });
} }
public function test_user_registration_with_existing_email()
{
config()->set([
'saml2.onelogin.strict' => false,
]);
$viewer = $this->getViewer();
$viewer->email = 'user@example.com';
$viewer->save();
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
$acsPost = $this->post('/saml2/acs');
$acsPost->assertRedirect('/');
$errorMessage = session()->get('error');
$this->assertEquals('A user with the email user@example.com already exists but with different credentials.', $errorMessage);
});
}
public function test_saml_routes_are_only_active_if_saml_enabled() public function test_saml_routes_are_only_active_if_saml_enabled()
{ {
config()->set(['auth.method' => 'standard']); config()->set(['auth.method' => 'standard']);

View File

@ -262,6 +262,23 @@ class PageContentTest extends TestCase
$this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1); $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
} }
public function test_anchors_referencing_non_bkmrk_ids_rewritten_after_save()
{
$this->asEditor();
$page = Page::first();
$content = '<h1 id="non-standard-id">test</h1><p><a href="#non-standard-id">link</a></p>';
$this->put($page->getUrl(), [
'name' => $page->name,
'html' => $content,
'summary' => ''
]);
$updatedPage = Page::where('id', '=', $page->id)->first();
$this->assertStringContainsString('id="bkmrk-test"', $updatedPage->html);
$this->assertStringContainsString('href="#bkmrk-test"', $updatedPage->html);
}
public function test_get_page_nav_sets_correct_properties() public function test_get_page_nav_sets_correct_properties()
{ {
$content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>'; $content = '<h1 id="testa">Hello</h1><h2 id="testb">There</h2><h3 id="testc">Donkey</h3>';

View File

@ -69,7 +69,7 @@ class DrawioTest extends TestCase
$editor = $this->getEditor(); $editor = $this->getEditor();
$resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));
$resp->assertSee('drawio-url="https://www.draw.io/?embed=1&amp;proto=json&amp;spin=1"'); $resp->assertSee('drawio-url="https://embed.diagrams.net/?embed=1&amp;proto=json&amp;spin=1"');
config()->set('services.drawio', false); config()->set('services.drawio', false);
$resp = $this->actingAs($editor)->get($page->getUrl('/edit')); $resp = $this->actingAs($editor)->get($page->getUrl('/edit'));