]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'fix/registraion-form-validation' of git://github.com/cw1998/BookStack...
authorDan Brown <redacted>
Sun, 21 Apr 2019 11:24:39 +0000 (12:24 +0100)
committerDan Brown <redacted>
Sun, 21 Apr 2019 11:24:39 +0000 (12:24 +0100)
app/Auth/Access/LdapService.php
app/Entities/Repos/PageRepo.php
resources/assets/js/components/markdown-editor.js
resources/assets/js/services/code.js
resources/lang/en/entities.php
resources/views/form/role-checkboxes.blade.php
resources/views/pages/revisions.blade.php
routes/web.php
tests/Auth/LdapTest.php
tests/Entity/PageContentTest.php
tests/Entity/PageRevisionTest.php

index 9ffbbfbb75b57862c5543c4e4002013bd6485d28..c7415e1f738e723bd18e98ea9a3f5c6a217bb697 100644 (file)
@@ -91,7 +91,7 @@ class LdapService
         $userCn = $this->getUserResponseProperty($user, 'cn', null);
         return [
             'uid'   => $this->getUserResponseProperty($user, 'uid', $user['dn']),
-            'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
+            'name'  => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
             'dn'    => $user['dn'],
             'email' => $this->getUserResponseProperty($user, $emailAttr, null),
         ];
@@ -116,8 +116,8 @@ class LdapService
 
     /**
      * @param Authenticatable $user
-     * @param string          $username
-     * @param string          $password
+     * @param string $username
+     * @param string $password
      * @return bool
      * @throws LdapException
      */
@@ -182,25 +182,14 @@ class LdapService
             throw new LdapException(trans('errors.ldap_extension_not_installed'));
         }
 
-        // Get port from server string and protocol if specified.
-        $ldapServer = explode(':', $this->config['server']);
-        $hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
-        if (!$hasProtocol) {
-            array_unshift($ldapServer, '');
-        }
-        $hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
-        $defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
-
-        /*
-         * Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
-         * the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
-         * per handle.
-         */
+         // Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
+         // the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
         if ($this->config['tls_insecure']) {
             $this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
         }
 
-        $ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
+        $serverDetails = $this->parseServerString($this->config['server']);
+        $ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
 
         if ($ldapConnection === false) {
             throw new LdapException(trans('errors.ldap_cannot_connect'));
@@ -215,6 +204,27 @@ class LdapService
         return $this->ldapConnection;
     }
 
+    /**
+     * Parse a LDAP server string and return the host and port for
+     * a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
+     * @param $serverString
+     * @return array
+     */
+    protected function parseServerString($serverString)
+    {
+        $serverNameParts = explode(':', $serverString);
+
+        // If we have a protocol just return the full string since PHP will ignore a separate port.
+        if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
+            return ['host' => $serverString, 'port' => 389];
+        }
+
+        // Otherwise, extract the port out
+        $hostName = $serverNameParts[0];
+        $ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
+        return ['host' => $hostName, 'port' => $ldapPort];
+    }
+
     /**
      * Build a filter string by injecting common variables.
      * @param string $filterString
@@ -319,10 +329,10 @@ class LdapService
         $count = 0;
 
         if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
-            $count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
+            $count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
         }
 
-        for ($i=0; $i<$count; $i++) {
+        for ($i = 0; $i < $count; $i++) {
             $dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
             if (!in_array($dnComponents[0], $ldapGroups)) {
                 $ldapGroups[] = $dnComponents[0];
index 148ae8459e088f15f0a55ad483add20acc77004a..1aeee8daeab9bc66c694f112995f0074ecee74fb 100644 (file)
@@ -7,6 +7,7 @@ use BookStack\Entities\Page;
 use BookStack\Entities\PageRevision;
 use Carbon\Carbon;
 use DOMDocument;
+use DOMElement;
 use DOMXPath;
 
 class PageRepo extends EntityRepo
@@ -129,8 +130,7 @@ class PageRepo extends EntityRepo
     }
 
     /**
-     * Formats a page's html to be tagged correctly
-     * within the system.
+     * Formats a page's html to be tagged correctly within the system.
      * @param string $htmlText
      * @return string
      */
@@ -139,6 +139,7 @@ class PageRepo extends EntityRepo
         if ($htmlText == '') {
             return $htmlText;
         }
+
         libxml_use_internal_errors(true);
         $doc = new DOMDocument();
         $doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@@ -147,37 +148,17 @@ class PageRepo extends EntityRepo
         $body = $container->childNodes->item(0);
         $childNodes = $body->childNodes;
 
-        // Ensure no duplicate ids are used
-        $idArray = [];
-
+        // Set ids on top-level nodes
+        $idMap = [];
         foreach ($childNodes as $index => $childNode) {
-            /** @var \DOMElement $childNode */
-            if (get_class($childNode) !== 'DOMElement') {
-                continue;
-            }
-
-            // Overwrite id if not a BookStack custom id
-            if ($childNode->hasAttribute('id')) {
-                $id = $childNode->getAttribute('id');
-                if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
-                    $idArray[] = $id;
-                    continue;
-                };
-            }
-
-            // Create an unique id for the element
-            // Uses the content as a basis to ensure output is the same every time
-            // the same content is passed through.
-            $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
-            $newId = urlencode($contentId);
-            $loopIndex = 0;
-            while (in_array($newId, $idArray)) {
-                $newId = urlencode($contentId . '-' . $loopIndex);
-                $loopIndex++;
-            }
+            $this->setUniqueId($childNode, $idMap);
+        }
 
-            $childNode->setAttribute('id', $newId);
-            $idArray[] = $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);
         }
 
         // Generate inner html as a string
@@ -189,6 +170,41 @@ class PageRepo extends EntityRepo
         return $html;
     }
 
+    /**
+     * 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
+     */
+    protected function setUniqueId($element, array &$idMap)
+    {
+        if (get_class($element) !== 'DOMElement') {
+            return;
+        }
+
+        // Overwrite id if not a BookStack custom id
+        $existingId = $element->getAttribute('id');
+        if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
+            $idMap[$existingId] = true;
+            return;
+        }
+
+        // Create an unique id for the element
+        // Uses the content as a basis to ensure output is the same every time
+        // the same content is passed through.
+        $contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
+        $newId = urlencode($contentId);
+        $loopIndex = 0;
+
+        while (isset($idMap[$newId])) {
+            $newId = urlencode($contentId . '-' . $loopIndex);
+            $loopIndex++;
+        }
+
+        $element->setAttribute('id', $newId);
+        $idMap[$newId] = true;
+    }
+
     /**
      * Get the plain text version of a page's content.
      * @param \BookStack\Entities\Page $page
index 96cc1e6d10d228a9e8047d53da4e8969abc07238..55cf67813e0d51e156a533e073ca337f94eff1c6 100644 (file)
@@ -64,7 +64,7 @@ class MarkdownEditor {
             let action = button.getAttribute('data-action');
             if (action === 'insertImage') this.actionInsertImage();
             if (action === 'insertLink') this.actionShowLinkSelector();
-            if (action === 'insertDrawing' && event.ctrlKey) {
+            if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
                 this.actionShowImageManager();
                 return;
             }
index bd749033d60b7702e6d3c0130842ad4b13496693..1e0e48289d595132a5ef0e08d006d3c249352d3f 100644 (file)
@@ -8,13 +8,17 @@ import 'codemirror/mode/diff/diff';
 import 'codemirror/mode/go/go';
 import 'codemirror/mode/htmlmixed/htmlmixed';
 import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/julia/julia';
 import 'codemirror/mode/lua/lua';
+import 'codemirror/mode/haskell/haskell';
 import 'codemirror/mode/markdown/markdown';
+import 'codemirror/mode/mllike/mllike';
 import 'codemirror/mode/nginx/nginx';
 import 'codemirror/mode/php/php';
 import 'codemirror/mode/powershell/powershell';
 import 'codemirror/mode/python/python';
 import 'codemirror/mode/ruby/ruby';
+import 'codemirror/mode/rust/rust';
 import 'codemirror/mode/shell/shell';
 import 'codemirror/mode/sql/sql';
 import 'codemirror/mode/toml/toml';
@@ -35,21 +39,29 @@ const modeMap = {
     csharp: 'text/x-csharp',
     diff: 'diff',
     go: 'go',
+    haskell: 'haskell',
+    hs: 'haskell',
     html: 'htmlmixed',
     javascript: 'javascript',
     json: {name: 'javascript', json: true},
     js: 'javascript',
+    jl: 'julia',
+    julia: 'julia',
     lua: 'lua',
     md: 'markdown',
     mdown: 'markdown',
     markdown: 'markdown',
+    ml: 'mllike',
     nginx: 'nginx',
     powershell: 'powershell',
+    ocaml: 'mllike',
     php: 'php',
     py: 'python',
     python: 'python',
     ruby: 'ruby',
+    rust: 'rust',
     rb: 'ruby',
+    rs: 'rust',
     shell: 'shell',
     sh: 'shell',
     bash: 'shell',
@@ -265,4 +277,4 @@ export default {
     setContent: setContent,
     markdownEditor: markdownEditor,
     getMetaKey: getMetaKey,
-};
\ No newline at end of file
+};
index 8a3aeb022a9a0929a7cfd55eb948ed0950907020..abcd2cf2327efb66683f16222ece0fa4b94a5cbe 100644 (file)
@@ -299,6 +299,7 @@ return [
 
     // Revision
     'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
+    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
     'revision_delete_success' => 'Revision deleted',
     'revision_cannot_delete_latest' => 'Cannot delete the latest revision.'
 ];
\ No newline at end of file
index ab62652640c10fd89a6ec1daf42432c340f7ec16..580b332f39c7e1fc993c670693ba08a8ad818192 100644 (file)
@@ -3,10 +3,10 @@
     @foreach($roles as $role)
         <div>
             @include('components.custom-checkbox', [
-                'name' => $name . '[' . $role->name . ']',
+                'name' => $name . '[' . str_replace('.', 'DOT', $role->name) . ']',
                 'label' => $role->display_name,
                 'value' => $role->id,
-                'checked' => old($name . '.' . $role->name) || (!old('name') && isset($model) && $model->hasRole($role->name))
+                'checked' => old($name . '.' . str_replace('.', 'DOT', $role->name)) || (!old('name') && isset($model) && $model->hasRole($role->name))
             ])
         </div>
     @endforeach
index 9d968c016d3f1affe00ecf0afd5cf49dc387e1bb..e5515a7c9d20fe514a61bdccdaca4d89949bc05d 100644 (file)
                                 @else
                                     <a href="{{ $revision->getUrl() }}" target="_blank">{{ trans('entities.pages_revisions_preview') }}</a>
                                     <span class="text-muted">&nbsp;|&nbsp;</span>
-                                    <a href="{{ $revision->getUrl('restore') }}">{{ trans('entities.pages_revisions_restore') }}</a>
+                                    <a href="{{ $revision->getUrl('restore') }}"></a>
+                                    <div dropdown class="dropdown-container">
+                                        <a dropdown-toggle>{{ trans('entities.pages_revisions_restore') }}</a>
+                                        <ul>
+                                            <li class="px-m py-s"><small class="text-muted">{{trans('entities.revision_restore_confirm')}}</small></li>
+                                            <li>
+                                                <form action="{{ $revision->getUrl('/restore') }}" method="POST">
+                                                    {!! csrf_field() !!}
+                                                    <input type="hidden" name="_method" value="PUT">
+                                                    <button type="submit" class="text-button text-primary">@icon('history'){{ trans('entities.pages_revisions_restore') }}</button>
+                                                </form>
+                                            </li>
+                                        </ul>
+                                    </div>
                                     <span class="text-muted">&nbsp;|&nbsp;</span>
                                     <div dropdown class="dropdown-container">
                                         <a dropdown-toggle>{{ trans('common.delete') }}</a>
index d1a8b79695ff59ff64731377593751952c6d0ab9..695f61654a13c97b68f0dba8bf84a2226971ca80 100644 (file)
@@ -77,7 +77,7 @@ Route::group(['middleware' => 'auth'], function () {
         Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
         Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
         Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', 'PageController@showRevisionChanges');
-        Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
+        Route::put('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
         Route::delete('/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', 'PageController@destroyRevision');
 
         // Chapters
index 5ccb1415e6fed57e2bced5e33e6d04b966cadf2a..5923ef377700734f931b6a56098bfe14a18e255d 100644 (file)
@@ -409,4 +409,41 @@ class LdapTest extends BrowserKitTest
             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
     }
 
+    protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
+    {
+        app('config')->set([
+            'services.ldap.server' => $serverString
+        ]);
+
+        // Standard mocks
+        $this->mockLdap->shouldReceive('setVersion')->once();
+        $this->mockLdap->shouldReceive('setOption')->times(2);
+        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)->andReturn(['count' => 1, 0 => [
+            'uid' => [$this->mockUser->name],
+            'cn' => [$this->mockUser->name],
+            'dn' => ['dc=test' . config('services.ldap.base_dn')]
+        ]]);
+        $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
+        $this->mockEscapes(2);
+
+        $this->mockLdap->shouldReceive('connect')->once()
+            ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
+        $this->mockUserLogin();
+    }
+
+    public function test_ldap_port_provided_on_host_if_host_is_full_uri()
+    {
+        $hostName = 'ldaps://bookstack:8080';
+        $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
+    }
+
+    public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
+    {
+        $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
+    }
+
+    public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
+    {
+        $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
+    }
 }
index 003d07d4e19c754d2703e3f77409673a0c1d2280..88169c50da486c13ef710213d83b79609e021911 100644 (file)
@@ -71,51 +71,6 @@ class PageContentTest extends TestCase
         $pageResp->assertSee($content);
     }
 
-    public function test_page_revision_views_viewable()
-    {
-        $this->asEditor();
-
-        $pageRepo = app(PageRepo::class);
-        $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
-        $pageRevision = $page->revisions->last();
-
-        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
-        $revisionView->assertStatus(200);
-        $revisionView->assertSee('new content');
-
-        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes');
-        $revisionView->assertStatus(200);
-        $revisionView->assertSee('new content');
-    }
-
-    public function test_page_revision_restore_updates_content()
-    {
-        $this->asEditor();
-
-        $pageRepo = app(PageRepo::class);
-        $page = Page::first();
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
-        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
-        $page =  Page::find($page->id);
-
-
-        $pageView = $this->get($page->getUrl());
-        $pageView->assertDontSee('abc123');
-        $pageView->assertDontSee('def456');
-
-        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
-        $restoreReq = $this->get($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
-        $page =  Page::find($page->id);
-
-        $restoreReq->assertStatus(302);
-        $restoreReq->assertRedirect($page->getUrl());
-
-        $pageView = $this->get($page->getUrl());
-        $pageView->assertSee('abc123');
-        $pageView->assertSee('def456');
-    }
-
     public function test_page_content_scripts_escaped_by_default()
     {
         $this->asEditor();
@@ -159,4 +114,21 @@ class PageContentTest extends TestCase
         $pageView = $this->get($pageB->getUrl());
         $pageView->assertSuccessful();
     }
+
+    public function test_duplicate_ids_fixed_on_page_save()
+    {
+        $this->asEditor();
+        $page = Page::first();
+
+        $content = '<ul id="bkmrk-test"><li>test a</li><li><ul id="bkmrk-test"><li>test b</li></ul></li></ul>';
+        $pageSave = $this->put($page->getUrl(), [
+            'name' => $page->name,
+            'html' => $content,
+            'summary' => ''
+        ]);
+        $pageSave->assertRedirect();
+
+        $updatedPage = Page::where('id', '=', $page->id)->first();
+        $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
+    }
 }
index 015320dd7b1208e7c2ae4aa74d49f13f5498b457..521ea79a4760ee6bcfa30c3497b8882f1a267ddb 100644 (file)
@@ -1,11 +1,55 @@
 <?php namespace Entity;
 
-
 use BookStack\Entities\Page;
+use BookStack\Entities\Repos\PageRepo;
 use Tests\TestCase;
 
 class PageRevisionTest extends TestCase
 {
+    public function test_page_revision_views_viewable()
+    {
+        $this->asEditor();
+
+        $pageRepo = app(PageRepo::class);
+        $page = Page::first();
+        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $pageRevision = $page->revisions->last();
+
+        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id);
+        $revisionView->assertStatus(200);
+        $revisionView->assertSee('new content');
+
+        $revisionView = $this->get($page->getUrl() . '/revisions/' . $pageRevision->id . '/changes');
+        $revisionView->assertStatus(200);
+        $revisionView->assertSee('new content');
+    }
+
+    public function test_page_revision_restore_updates_content()
+    {
+        $this->asEditor();
+
+        $pageRepo = app(PageRepo::class);
+        $page = Page::first();
+        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page abc123', 'html' => '<p>new contente def456</p>', 'summary' => 'initial page revision testing']);
+        $pageRepo->updatePage($page, $page->book_id, ['name' => 'updated page again', 'html' => '<p>new content</p>', 'summary' => 'page revision testing']);
+        $page =  Page::find($page->id);
+
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertDontSee('abc123');
+        $pageView->assertDontSee('def456');
+
+        $revToRestore = $page->revisions()->where('name', 'like', '%abc123')->first();
+        $restoreReq = $this->put($page->getUrl() . '/revisions/' . $revToRestore->id . '/restore');
+        $page =  Page::find($page->id);
+
+        $restoreReq->assertStatus(302);
+        $restoreReq->assertRedirect($page->getUrl());
+
+        $pageView = $this->get($page->getUrl());
+        $pageView->assertSee('abc123');
+        $pageView->assertSee('def456');
+    }
 
     public function test_page_revision_count_increments_on_update()
     {