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
AVATAR_URL=
# Enable draw.io integration
# Enable diagrams.net 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
DRAWIO=true

View File

@ -2,7 +2,6 @@
use BookStack\Entities\Page;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use DOMXPath;
@ -44,18 +43,24 @@ class PageContent
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes
$idMap = [];
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
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
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
@ -67,23 +72,34 @@ class PageContent
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.
* A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element
* @param array $idMap
* Returns a pair of strings in the format [old_id, new_id]
*/
protected function setUniqueId($element, array &$idMap)
protected function setUniqueId(\DOMNode $element, array &$idMap): array
{
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');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return;
return [$existingId, $existingId];
}
// Create an unique id for the element
@ -100,6 +116,7 @@ class PageContent
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
return [$existingId, $newId];
}
/**

24
package-lock.json generated
View File

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

View File

@ -4,9 +4,9 @@
"build:css:dev": "sass ./resources/sass:./public/dist",
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
"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: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",
"production": "npm-run-all --parallel build:*:production",
"dev": "npm-run-all --parallel watch livereload",
@ -16,18 +16,18 @@
},
"devDependencies": {
"chokidar-cli": "^2.1.0",
"esbuild": "0.6.30",
"esbuild": "0.7.8",
"livereload": "^0.9.1",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.26.10"
"sass": "^1.26.11"
},
"dependencies": {
"clipboard": "^2.0.6",
"codemirror": "^5.58.1",
"dropzone": "^5.7.2",
"markdown-it": "^11.0.0",
"markdown-it": "^11.0.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...
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} !-f
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)
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [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)
* [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
const sortOperations = {
@ -43,7 +43,6 @@ class BookSort {
this.input = elem.querySelector('[book-sort-input]');
const initialSortBox = elem.querySelector('.sort-box');
Sortable.mount(new MultiDrag());
this.setupBookSortable(initialSortBox);
this.setupSortPresets();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<div component="page-editor" class="page-editor flex-fill flex"
option:page-editor:drafts-enabled="{{ $draftsEnabled ? 'true' : 'false' }}"
@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
@if($model->name === trans('entities.pages_initial_name'))
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()
{
config()->set(['auth.method' => 'standard']);

View File

@ -262,6 +262,23 @@ class PageContentTest extends TestCase
$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()
{
$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();
$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);
$resp = $this->actingAs($editor)->get($page->getUrl('/edit'));