]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'master' into release
authorDan Brown <redacted>
Sun, 10 Mar 2019 13:44:29 +0000 (13:44 +0000)
committerDan Brown <redacted>
Sun, 10 Mar 2019 13:44:29 +0000 (13:44 +0000)
36 files changed:
.env.example.complete
app/Auth/Access/LdapService.php
app/Auth/Permissions/PermissionService.php
app/Entities/EntityProvider.php
app/Entities/Repos/PageRepo.php
app/Exceptions/HttpFetchException.php
app/Exceptions/UserUpdateException.php
app/Http/Controllers/PageController.php
app/Http/Middleware/Localization.php
app/Notifications/MailNotification.php
app/Notifications/ResetPassword.php
app/Providers/TranslationServiceProvider.php
app/Settings/SettingService.php
app/Translation/Translator.php
app/Uploads/HttpFetcher.php
app/helpers.php
config/database.php
config/services.php
readme.md
resources/assets/js/components/markdown-editor.js
resources/assets/js/components/wysiwyg-editor.js
resources/assets/js/services/code.js
resources/assets/js/vues/components/dropzone.js
resources/assets/sass/_components.scss
resources/assets/sass/_pages.scss
resources/lang/de/entities.php
resources/lang/de_informal/entities.php
resources/lang/en/validation.php
resources/lang/nl/auth.php
resources/views/base.blade.php
resources/views/components/code-editor.blade.php
resources/views/pages/show.blade.php
resources/views/users/edit.blade.php
tests/Auth/LdapTest.php
tests/Entity/BookShelfTest.php
tests/Entity/SortTest.php

index 8851bd26834e75373c4c011928056321a3d8b14c..911d924df75f4c4fa6b6c2892529e154f2b78a0d 100644 (file)
@@ -75,6 +75,12 @@ CACHE_PREFIX=bookstack
 # For multiple servers separate with a comma
 MEMCACHED_SERVERS=127.0.0.1:11211:100
 
+# Redis server configuration
+# This follows the following format: HOST:PORT:DATABASE
+# or, if using a password: HOST:PORT:DATABASE:PASSWORD
+# For multiple servers separate with a comma. These will be clustered.
+REDIS_SERVERS=127.0.0.1:6379:0
+
 # Queue driver to use
 # Queue not really currently used but may be configurable in the future.
 # Would advise not to change this for now.
@@ -171,6 +177,7 @@ LDAP_USER_FILTER=false
 LDAP_VERSION=false
 LDAP_TLS_INSECURE=false
 LDAP_EMAIL_ATTRIBUTE=mail
+LDAP_DISPLAY_NAME_ATTRIBUTE=cn
 LDAP_FOLLOW_REFERRALS=true
 
 # LDAP group sync configuration
index 1e95ac513a340705d251d394045d39f1d0cdbe93..9ffbbfbb75b57862c5543c4e4002013bd6485d28 100644 (file)
@@ -80,20 +80,40 @@ class LdapService
     public function getUserDetails($userName)
     {
         $emailAttr = $this->config['email_attribute'];
-        $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
+        $displayNameAttr = $this->config['display_name_attribute'];
+
+        $user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
 
         if ($user === null) {
             return null;
         }
 
+        $userCn = $this->getUserResponseProperty($user, 'cn', null);
         return [
-            'uid'   => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
-            'name'  => $user['cn'][0],
+            'uid'   => $this->getUserResponseProperty($user, 'uid', $user['dn']),
+            'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
             'dn'    => $user['dn'],
-            'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
+            'email' => $this->getUserResponseProperty($user, $emailAttr, null),
         ];
     }
 
+    /**
+     * Get a property from an LDAP user response fetch.
+     * Handles properties potentially being part of an array.
+     * @param array $userDetails
+     * @param string $propertyKey
+     * @param $defaultValue
+     * @return mixed
+     */
+    protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
+    {
+        if (isset($userDetails[$propertyKey])) {
+            return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
+        }
+
+        return $defaultValue;
+    }
+
     /**
      * @param Authenticatable $user
      * @param string          $username
@@ -176,8 +196,8 @@ class LdapService
          * 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);
+        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);
index d0e6ccceab56640ddbee6b48a5b99b4cf14f815f..8fc70e916dbd5dec3c5e878222de11861dfd9b45 100644 (file)
@@ -190,10 +190,10 @@ class PermissionService
     {
         return $this->entityProvider->book->newQuery()
             ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
-            $query->select(['id', 'restricted', 'created_by', 'book_id']);
-        }, 'pages'  => function ($query) {
-            $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
-        }]);
+                $query->select(['id', 'restricted', 'created_by', 'book_id']);
+            }, 'pages'  => function ($query) {
+                $query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
+            }]);
     }
 
     /**
@@ -556,6 +556,39 @@ class PermissionService
         return $q;
     }
 
+    /**
+     * Checks if a user has the given permission for any items in the system.
+     * Can be passed an entity instance to filter on a specific type.
+     * @param string $permission
+     * @param string $entityClass
+     * @return bool
+     */
+    public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
+    {
+        $userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
+        $userId = $this->currentUser()->id;
+
+        $permissionQuery = $this->db->table('joint_permissions')
+            ->where('action', '=', $permission)
+            ->whereIn('role_id', $userRoleIds)
+            ->where(function ($query) use ($userId) {
+                $query->where('has_permission', '=', 1)
+                    ->orWhere(function ($query2) use ($userId) {
+                        $query2->where('has_permission_own', '=', 1)
+                            ->where('created_by', '=', $userId);
+                    });
+        }) ;
+
+        if (!is_null($entityClass)) {
+            $entityInstance = app()->make($entityClass);
+            $permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
+        }
+
+        $hasPermission = $permissionQuery->count() > 0;
+        $this->clean();
+        return $hasPermission;
+    }
+
     /**
      * Check if an entity has restrictions set on itself or its
      * parent tree.
@@ -612,13 +645,13 @@ class PermissionService
         $entities = $this->entityProvider;
         $pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
             ->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
-            $query->where('draft', '=', 0);
-            if (!$filterDrafts) {
-                $query->orWhere(function ($query) {
-                    $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
-                });
-            }
-        });
+                $query->where('draft', '=', 0);
+                if (!$filterDrafts) {
+                    $query->orWhere(function ($query) {
+                        $query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
+                    });
+                }
+            });
         $chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
         $query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
             ->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
index 46a883ec408927661479c5d84fc8bab7cc5d5fed..04939a14a411d9b344faad349ba8dd59195273cf 100644 (file)
@@ -84,6 +84,4 @@ class EntityProvider
         $type = strtolower($type);
         return $this->all()[$type];
     }
-
-
-}
\ No newline at end of file
+}
index 3558b29b3d2e109d6b901ee7adc641856c8876fc..148ae8459e088f15f0a55ad483add20acc77004a 100644 (file)
@@ -505,4 +505,4 @@ class PageRepo extends EntityRepo
 
         return $this->publishPageDraft($copyPage, $pageData);
     }
-}
\ No newline at end of file
+}
index 48e30e1e6a2fd564c3a7b55790fefa77b9259995..2a34bbc626b7c91c44b1bd8d17f6e39db4fcb5df 100644 (file)
@@ -2,4 +2,6 @@
 
 use Exception;
 
-class HttpFetchException extends Exception {}
+class HttpFetchException extends Exception
+{
+}
index eb41dece6d626a93c13c85b72ff2b7ac35f31d50..81e95b16fd201a3ae7ccd1d58ea8dac242823323 100644 (file)
@@ -1,3 +1,5 @@
 <?php namespace BookStack\Exceptions;
 
-class UserUpdateException extends NotifyException {}
+class UserUpdateException extends NotifyException
+{
+}
index b68655241485c572a0ea277f400ffd98e880f461..d95e02470bee10c63d436421ac7a53cdcff6577c 100644 (file)
@@ -643,7 +643,7 @@ class PageController extends Controller
     public function showCopy($bookSlug, $pageSlug)
     {
         $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('page-view', $page);
         session()->flashInput(['name' => $page->name]);
         return view('pages/copy', [
             'book' => $page->book,
@@ -662,7 +662,7 @@ class PageController extends Controller
     public function copy($bookSlug, $pageSlug, Request $request)
     {
         $page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
-        $this->checkOwnablePermission('page-update', $page);
+        $this->checkOwnablePermission('page-view', $page);
 
         $entitySelection = $request->get('entity_selection', null);
         if ($entitySelection === null || $entitySelection === '') {
index e65b417d5079f43535930ab77d86a915d80ca8b8..ff5526cc70d61f6fbdcde2aa35a63c034d74ff1a 100644 (file)
@@ -51,6 +51,7 @@ class Localization
     public function handle($request, Closure $next)
     {
         $defaultLang = config('app.locale');
+        config()->set('app.default_locale', $defaultLang);
 
         if (user()->isDefault() && config('app.auto_detect_locale')) {
             $locale = $this->autoDetectLocale($request, $defaultLang);
@@ -63,8 +64,6 @@ class Localization
             config()->set('app.rtl', true);
         }
 
-
-
         app()->setLocale($locale);
         Carbon::setLocale($locale);
         $this->setSystemDateLocale($locale);
index 413ac6d73f3cb32e13152211ebd1b1b967e0264d..5aa9b1e4a6296de1fe9557649a2d7401d877c52c 100644 (file)
@@ -31,5 +31,4 @@ class MailNotification extends Notification implements ShouldQueue
             'text' => 'vendor.notifications.email-plain'
         ]);
     }
-
-}
\ No newline at end of file
+}
index 282aa335a07aed35b52c4ad2520565d567ca217f..305a7da72dc50236dd82cc8bbd935f891cf8fb27 100644 (file)
@@ -1,6 +1,5 @@
 <?php namespace BookStack\Notifications;
 
-
 class ResetPassword extends MailNotification
 {
     /**
index 0e628c7da746a825188f351248649cce0fa74533..7d51cc73d0a7af9b303a8112292d3ac18a81f432 100644 (file)
@@ -1,6 +1,5 @@
 <?php namespace BookStack\Providers;
 
-
 use BookStack\Translation\Translator;
 
 class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
@@ -29,4 +28,4 @@ class TranslationServiceProvider extends \Illuminate\Translation\TranslationServ
             return $trans;
         });
     }
-}
\ No newline at end of file
+}
index c903bd60a5dc4b811bcf2604965aa3692d020382..c9491e3eeb4b4c949248680b9d43b1a0e45c3f59 100644 (file)
@@ -41,6 +41,7 @@ class SettingService
         if ($default === false) {
             $default = config('setting-defaults.' . $key, false);
         }
+
         if (isset($this->localCache[$key])) {
             return $this->localCache[$key];
         }
index 2edfecf7368413559303884081c9d455ba17219d..032f43ff7255162f8c942939657f87a7868e13b9 100644 (file)
@@ -1,6 +1,5 @@
 <?php namespace BookStack\Translation;
 
-
 class Translator extends \Illuminate\Translation\Translator
 {
 
@@ -70,5 +69,4 @@ class Translator extends \Illuminate\Translation\Translator
     {
         return $this->baseLocaleMap[$locale] ?? null;
     }
-
-}
\ No newline at end of file
+}
index 3ebe17eee4c25f86839e6de47128db17881f16f9..5e8115637dbbe23abf78dd5a9eb52959d9e75fe8 100644 (file)
@@ -30,5 +30,4 @@ class HttpFetcher
 
         return $data;
     }
-
-}
\ No newline at end of file
+}
index b0886d02b9762c27383e37f5b7989014607ed03c..d9533645de6e31ae1c464b7466f1ee71a498a721 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Entity;
 use BookStack\Ownable;
 
 /**
@@ -50,21 +52,34 @@ function signedInUser()
  * Check if the current user has a permission.
  * If an ownable element is passed in the jointPermissions are checked against
  * that particular item.
- * @param $permission
+ * @param string $permission
  * @param Ownable $ownable
  * @return mixed
  */
-function userCan($permission, Ownable $ownable = null)
+function userCan(string $permission, Ownable $ownable = null)
 {
     if ($ownable === null) {
         return user() && user()->can($permission);
     }
 
     // Check permission on ownable item
-    $permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
+    $permissionService = app(PermissionService::class);
     return $permissionService->checkOwnableUserAccess($ownable, $permission);
 }
 
+/**
+ * Check if the current user has the given permission
+ * on any item in the system.
+ * @param string $permission
+ * @param string|null $entityClass
+ * @return bool
+ */
+function userCanOnAny(string $permission, string $entityClass = null)
+{
+    $permissionService = app(PermissionService::class);
+    return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
+}
+
 /**
  * Helper to access system settings.
  * @param $key
index 6ca902944ed14644d8ded1631ac4bc3ae0749746..93a44854f092a8d166b9ce994a41da9dacb67a99 100644 (file)
@@ -8,23 +8,39 @@
  * Do not edit this file unless you're happy to maintain any changes yourself.
  */
 
-// REDIS - Split out configuration into an array
+// REDIS
+// Split out configuration into an array
 if (env('REDIS_SERVERS', false)) {
-    $redisServerKeys = ['host', 'port', 'database'];
+
+    $redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
     $redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
-    $redisConfig = [
-        'cluster' => env('REDIS_CLUSTER', false)
-    ];
+    $redisConfig = [];
+    $cluster = count($redisServers) > 1;
+
+    if ($cluster) {
+        $redisConfig['clusters'] = ['default' => []];
+    }
+
     foreach ($redisServers as $index => $redisServer) {
-        $redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
         $redisServerDetails = explode(':', $redisServer);
-        if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
-        if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
-        $redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
+
+        $serverConfig = [];
+        $configIndex = 0;
+        foreach ($redisDefaults as $configKey => $configDefault) {
+            $serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
+            $configIndex++;
+        }
+
+        if ($cluster) {
+            $redisConfig['clusters']['default'][] = $serverConfig;
+        } else {
+            $redisConfig['default'] = $serverConfig;
+        }
     }
 }
 
-// MYSQL - Split out port from host if set
+// MYSQL
+// Split out port from host if set
 $mysql_host = env('DB_HOST', 'localhost');
 $mysql_host_exploded = explode(':', $mysql_host);
 $mysql_port = env('DB_PORT', 3306);
index f713f9d3897df3caf051805bdb64bd0f25ae9a4a..97cb71ddc73f0cf623907936fe43b02356496269 100644 (file)
@@ -141,6 +141,7 @@ return [
         'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
         'version' => env('LDAP_VERSION', false),
         'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
+        'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
         'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
                'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
                'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
index e6fd59e98fffc0393dc5c53fc1378e3b4c1d4ba4..037fbedb5eced2af6de1350b3f0643e4ab6c193c 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -9,8 +9,7 @@ A platform for storing and organising information and documentation. General inf
 * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
 * [Documentation](https://www.bookstackapp.com/docs)
 * [Demo Instance](https://demo.bookstackapp.com)
-  * *Username: `[email protected]`*
-  * *Password: `password`*
+    * [Admin Login](https://demo.bookstackapp.com/[email protected]&password=password)
 * [BookStack Blog](https://www.bookstackapp.com/blog)
 
 ## Project Definition
@@ -19,7 +18,30 @@ BookStack is an opinionated wiki system that provides a pleasant and simple out
 
 BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
 
-In regards to development philosophy, BookStack has a relaxed, open & positive approach. Put simply, At the end of the day this is free software developed and maintained by people donating their own free time.
+In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
+
+## Road Map
+
+Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
+
+- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
+    - *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
+- **Platform REST API**
+    - *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
+- **Editor Alignment & Review**
+    - *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.*
+- **Installation & Deployment Process Revamp**
+    - *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
+
+## Release Versioning & Process
+
+BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
+
+Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. 
+
+For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
 
 ## Development & Testing
 
@@ -85,15 +107,15 @@ PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr
 
 ### Pull Requests
 
-Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
+Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
 
-Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
+Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
 
 If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
 
 ## Website, Docs & Blog
 
-The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
+The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
 
 ## License
 
@@ -117,7 +139,6 @@ These are the great open-source projects used to help build BookStack:
 * [clipboard.js](https://clipboardjs.com/)
 * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
 * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
-* [Moment.js](http://momentjs.com/)
 * [BarryVD](https://github.com/barryvdh)
     * [Debugbar](https://github.com/barryvdh/laravel-debugbar)
     * [Dompdf](https://github.com/barryvdh/laravel-dompdf)
index 9228cfe2c49445caa7aebe79c0859e2748fdc8cf..b8e2bc040c8fe4db7795259cbcbaf515ff3830f1 100644 (file)
@@ -8,7 +8,11 @@ class MarkdownEditor {
 
     constructor(elem) {
         this.elem = elem;
-        this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
+
+        const pageEditor = document.getElementById('page-editor');
+        this.pageId = pageEditor.getAttribute('page-id');
+        this.textDirection = pageEditor.getAttribute('text-direction');
+
         this.markdown = new MarkdownIt({html: true});
         this.markdown.use(mdTasksLists, {label: true});
 
@@ -98,7 +102,9 @@ class MarkdownEditor {
     }
 
     codeMirrorSetup() {
-        let cm = this.cm;
+        const cm = this.cm;
+        const context = this;
+
         // Text direction
         // cm.setOption('direction', this.textDirection);
         cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
@@ -266,17 +272,18 @@ class MarkdownEditor {
             }
 
             // Insert image into markdown
-            let id = "image-" + Math.random().toString(16).slice(2);
-            let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
-            let selectedText = cm.getSelection();
-            let placeHolderText = `![${selectedText}](${placeholderImage})`;
-            let cursor = cm.getCursor();
+            const id = "image-" + Math.random().toString(16).slice(2);
+            const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
+            const selectedText = cm.getSelection();
+            const placeHolderText = `![${selectedText}](${placeholderImage})`;
+            const cursor = cm.getCursor();
             cm.replaceSelection(placeHolderText);
             cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
 
-            let remoteFilename = "image-" + Date.now() + "." + ext;
-            let formData = new FormData();
+            const remoteFilename = "image-" + Date.now() + "." + ext;
+            const formData = new FormData();
             formData.append('file', file, remoteFilename);
+            formData.append('uploaded_to', context.pageId);
 
             window.$http.post('/images/gallery/upload', formData).then(resp => {
                 const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
@@ -302,7 +309,7 @@ class MarkdownEditor {
     }
 
     actionInsertImage() {
-        let cursorPos = this.cm.getCursor('from');
+        const cursorPos = this.cm.getCursor('from');
         window.ImageManager.show(image => {
             let selectedText = this.cm.getSelection();
             let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
@@ -313,7 +320,7 @@ class MarkdownEditor {
     }
 
     actionShowImageManager() {
-        let cursorPos = this.cm.getCursor('from');
+        const cursorPos = this.cm.getCursor('from');
         window.ImageManager.show(image => {
             this.insertDrawing(image, cursorPos);
         }, 'drawio');
@@ -321,7 +328,7 @@ class MarkdownEditor {
 
     // Show the popup link selector and insert a link when finished
     actionShowLinkSelector() {
-        let cursorPos = this.cm.getCursor('from');
+        const cursorPos = this.cm.getCursor('from');
         window.EntitySelectorPopup.show(entity => {
             let selectedText = this.cm.getSelection() || entity.name;
             let newText = `[${selectedText}](${entity.link})`;
@@ -357,7 +364,7 @@ class MarkdownEditor {
     }
 
     insertDrawing(image, originalCursor) {
-        let newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
+        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
         this.cm.focus();
         this.cm.replaceSelection(newText);
         this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
@@ -365,9 +372,13 @@ class MarkdownEditor {
 
     // Show draw.io if enabled and handle save.
     actionEditDrawing(imgContainer) {
-        if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
-        let cursorPos = this.cm.getCursor('from');
-        let drawingId = imgContainer.getAttribute('drawio-diagram');
+        const drawingDisabled = document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true';
+        if (drawingDisabled) {
+            return;
+        }
+
+        const cursorPos = this.cm.getCursor('from');
+        const drawingId = imgContainer.getAttribute('drawio-diagram');
 
         DrawIO.show(() => {
             return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {
index 39bce49591e490a90559c8f8e34a95d6aafe66f5..9deb1d0a7a0f4e2a9c16945d919fe6e9b61148ff 100644 (file)
@@ -4,22 +4,24 @@ import DrawIO from "../services/drawio";
 /**
  * Handle pasting images from clipboard.
  * @param {ClipboardEvent} event
+ * @param {WysiwygEditor} wysiwygComponent
  * @param editor
  */
-function editorPaste(event, editor) {
+function editorPaste(event, editor, wysiwygComponent) {
     if (!event.clipboardData || !event.clipboardData.items) return;
-    let items = event.clipboardData.items;
 
-    for (let i = 0; i < items.length; i++) {
-        if (items[i].type.indexOf("image") === -1) continue;
+    for (let clipboardItem of event.clipboardData.items) {
+        if (clipboardItem.type.indexOf("image") === -1) continue;
         event.preventDefault();
 
-        let id = "image-" + Math.random().toString(16).slice(2);
-        let loadingImage = window.baseUrl('/loading.gif');
-        let file = items[i].getAsFile();
+        const id = "image-" + Math.random().toString(16).slice(2);
+        const loadingImage = window.baseUrl('/loading.gif');
+        const file = clipboardItem.getAsFile();
+
         setTimeout(() => {
             editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
-            uploadImageFile(file).then(resp => {
+
+            uploadImageFile(file, wysiwygComponent).then(resp => {
                 editor.dom.setAttrib(id, 'src', resp.thumbs.display);
             }).catch(err => {
                 editor.dom.remove(id);
@@ -33,9 +35,12 @@ function editorPaste(event, editor) {
 /**
  * Upload an image file to the server
  * @param {File} file
+ * @param {WysiwygEditor} wysiwygComponent
  */
-function uploadImageFile(file) {
-    if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
+async function uploadImageFile(file, wysiwygComponent) {
+    if (file === null || file.type.indexOf('image') !== 0) {
+        throw new Error(`Not an image file`);
+    }
 
     let ext = 'png';
     if (file.name) {
@@ -43,11 +48,13 @@ function uploadImageFile(file) {
         if (fileNameMatches.length > 1) ext = fileNameMatches[1];
     }
 
-    let remoteFilename = "image-" + Date.now() + "." + ext;
-    let formData = new FormData();
+    const remoteFilename = "image-" + Date.now() + "." + ext;
+    const formData = new FormData();
     formData.append('file', file, remoteFilename);
+    formData.append('uploaded_to', wysiwygComponent.pageId);
 
-    return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
+    const resp = await window.$http.post(window.baseUrl('/images/gallery/upload'), formData);
+    return resp.data;
 }
 
 function registerEditorShortcuts(editor) {
@@ -370,7 +377,10 @@ class WysiwygEditor {
 
     constructor(elem) {
         this.elem = elem;
-        this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
+
+        const pageEditor = document.getElementById('page-editor');
+        this.pageId = pageEditor.getAttribute('page-id');
+        this.textDirection = pageEditor.getAttribute('text-direction');
 
         this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
         this.loadPlugins();
@@ -397,6 +407,9 @@ class WysiwygEditor {
     }
 
     getTinyMceConfig() {
+
+        const context = this;
+
         return {
             selector: '#html-editor',
             content_css: [
@@ -586,7 +599,7 @@ class WysiwygEditor {
                 });
 
                 // Paste image-uploads
-                editor.on('paste', event => editorPaste(event, editor));
+                editor.on('paste', event => editorPaste(event, editor, context));
             }
         };
     }
index cfeabd3be0ce32aa84a5fe4ea6a74f3c38a3915e..bd749033d60b7702e6d3c0130842ad4b13496693 100644 (file)
@@ -8,6 +8,7 @@ import 'codemirror/mode/diff/diff';
 import 'codemirror/mode/go/go';
 import 'codemirror/mode/htmlmixed/htmlmixed';
 import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/lua/lua';
 import 'codemirror/mode/markdown/markdown';
 import 'codemirror/mode/nginx/nginx';
 import 'codemirror/mode/php/php';
@@ -38,12 +39,13 @@ const modeMap = {
     javascript: 'javascript',
     json: {name: 'javascript', json: true},
     js: 'javascript',
-    php: 'php',
+    lua: 'lua',
     md: 'markdown',
     mdown: 'markdown',
     markdown: 'markdown',
     nginx: 'nginx',
     powershell: 'powershell',
+    php: 'php',
     py: 'python',
     python: 'python',
     ruby: 'ruby',
index 31a84a267d958831996406bf7a717f9ec85e432b..9d3d22b4dd2fdf7668299a1c65a1799db0e46366 100644 (file)
@@ -16,6 +16,7 @@ function mounted() {
         addRemoveLinks: true,
         dictRemoveFile: trans('components.image_upload_remove'),
         timeout: Number(window.uploadTimeout) || 60000,
+        maxFilesize: Number(window.uploadLimit) || 256,
         url: function() {
             return _this.uploadUrl;
         },
index d00d1fe9a433f5497795d5fc765e577683f91d0a..1f34166c61ffda83e9f72434bdf0875c6d2804f6 100644 (file)
@@ -210,7 +210,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 
 .image-manager-sidebar {
   width: 300px;
-  margin-left: 1px;
   overflow-y: auto;
   overflow-x: hidden;
   border-left: 1px solid #DDD;
@@ -524,8 +523,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   font-size: 12px;
   line-height: 1.2;
   top: 88px;
-  left: -26px;
-  width: 148px;
+  left: -12px;
+  width: 160px;
   background: $negative;
   padding: $-xs;
   color: white;
@@ -535,7 +534,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   content: '';
   position: absolute;
   top: -6px;
-  left: 64px;
+  left: 44px;
   width: 0;
   height: 0;
   border-left: 6px solid transparent;
index 21fdf90dea234ad25c95365f6a45e89a20045ca0..84280319debff1d04fd30d4633ca0448b66d6399 100755 (executable)
     margin: $-xs $-s $-xs 0;
   }
   .align-right {
-    float: right !important;
+    text-align: right !important;
   }
   img.align-right, table.align-right {
-    text-align: right;
+    float: right !important;
     margin: $-xs 0 $-xs $-s;
   }
   .align-center {
     text-align: center;
   }
+  img.align-center {
+    display: block;
+  }
+  img.align-center, table.align-center {
+    margin-left: auto;
+    margin-right: auto;
+  }
   img {
     max-width: 100%;
     height:auto;
index 7c27be17b175c029aac83edc20d1c5818e2878c5..07a92e2c7aac8acc1e6bfe12a9ab66ecc676b6b5 100644 (file)
@@ -60,6 +60,39 @@ return [
     'search_created_after' => 'Erstellt nach',
     'search_set_date' => 'Datum auswählen',
     'search_update' => 'Suche aktualisieren',
+    
+    /*
+     * Shelves
+     */
+    'shelf' => 'Regal',
+    'shelves' => 'Regale',
+    'shelves_long' => 'Bücherregal',
+    'shelves_empty' => 'Es wurden noch keine Regale angelegt',
+    'shelves_create' => 'Erzeuge ein Regal',
+    'shelves_popular' => 'Beliebte Regale',
+    'shelves_new' => 'Kürzlich erstellte Regale',
+    'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',
+    'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',
+    'shelves_save' => 'Regal speichern',
+    'shelves_books' => 'Bücher in diesem Regal',
+    'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',
+    'shelves_drag_books' => 'Bücher hier hin ziehen um sie dem Regal hinzuzufügen',
+    'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',
+    'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',
+    'shelves_edit_named' => 'Bücherregal :name bearbeiten',
+    'shelves_edit' => 'Bücherregal bearbeiten',
+    'shelves_delete' => 'Bücherregal löschen',
+    'shelves_delete_named' => 'Bücherregal :name löschen',
+    'shelves_delete_explain' => "Sie sind im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
+    'shelves_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Bücherregal löschen wollen?',
+    'shelves_permissions' => 'Regal-Berechtigungen',
+    'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
+    'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
+    'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
+    'shelves_copy_permissions' => 'Berechtigungen kopieren',
+    'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher Ã¼bertragen. Ãœberprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
+    'shelves_copy_permission_success' => 'Regal-Berechtigungen wurden zu :count Büchern kopiert',
+    
     /**
      * Books
      */
index 21fdbb13d8ff7651254a896cfcf405f007da265b..1decdd7b78bcd593c160e1d5003670da3c4e603c 100644 (file)
@@ -9,6 +9,13 @@ return [
     'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',
     'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',
 
+    /**
+     * Shelves
+     */
+    'shelves_delete_explain' => "Du bist im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
+    'shelves_delete_confirmation' => 'Bist du sicher, dass du dieses Bücherregal löschen willst?',
+    'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher Ã¼bertragen. Ãœberprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
+    
     /**
      * Books
      */
index 0b6a1c170a5fb31d7d470eedf61bb7a89bd96dbb..e05cfca9dbe13222fdbc1f5ee492fb57fa5036d6 100644 (file)
@@ -69,6 +69,7 @@ return [
     'timezone'             => 'The :attribute must be a valid zone.',
     'unique'               => 'The :attribute has already been taken.',
     'url'                  => 'The :attribute format is invalid.',
+    'is_image'             => 'The :attribute must be a valid image.',
 
     // Custom validation lines
     'custom' => [
index d8813f07b8398c289ef07bb876e2036884349ebb..31bd330cc3d5ae28acb9663883cfce9f775d3800 100644 (file)
@@ -27,7 +27,7 @@ return [
     'email' => 'Email',
     'password' => 'Wachtwoord',
     'password_confirm' => 'Wachtwoord Bevestigen',
-    'password_hint' => 'Minimaal 5 tekens',
+    'password_hint' => 'Minimaal 6 tekens',
     'forgot_password' => 'Wachtwoord vergeten?',
     'remember_me' => 'Mij onthouden',
     'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
@@ -73,4 +73,4 @@ return [
     'email_not_confirmed_click_link' => 'Klik op de link in de e-mail die vlak na je registratie is verstuurd.',
     'email_not_confirmed_resend' => 'Als je deze e-mail niet kunt vinden kun je deze met onderstaande formulier opnieuw verzenden.',
     'email_not_confirmed_resend_button' => 'Bevestigingsmail Opnieuw Verzenden',
-];
\ No newline at end of file
+];
index c7a5acca80a0398fdfe5bb58fedf9c88f7bf4ef5..fdd248091d9dfcf29ddd3d819f27c75daa63d597 100644 (file)
@@ -48,7 +48,7 @@
                             </form>
                         </div>
                         <div class="links text-center">
-                            @if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
+                            @if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-own'))
                                 <a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
                             @endif
                             <a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
index cd235f1d2a519558d073245e0d20c5c667242ef1..67e7b77737049504ee3e38a4f00b939c66d1e619 100644 (file)
@@ -21,7 +21,9 @@
                             <a @click="updateLanguage('Java')">Java</a>
                             <a @click="updateLanguage('JavaScript')">JavaScript</a>
                             <a @click="updateLanguage('JSON')">JSON</a>
+                            <a @click="updateLanguage('Lua')">Lua</a>
                             <a @click="updateLanguage('PHP')">PHP</a>
+                            <a @click="updateLanguage('Powershell')">Powershell</a>
                             <a @click="updateLanguage('MarkDown')">MarkDown</a>
                             <a @click="updateLanguage('Nginx')">Nginx</a>
                             <a @click="updateLanguage('Python')">Python</a>
@@ -48,4 +50,4 @@
 
         </div>
     </div>
-</div>
\ No newline at end of file
+</div>
index afe007d45fe057df7dc24f58065d40a1985b41a0..db2f1462e3d05bedbea27fef66c9677c5efc599c 100644 (file)
             @if(userCan('page-update', $page))
                 <a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" >@icon('edit'){{ trans('common.edit') }}</a>
             @endif
-            @if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
+            @if((userCan('page-view', $page) && userCanOnAny('page-create')) || userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
                 <div dropdown class="dropdown-container">
                     <a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
                     <ul>
-                        @if(userCan('page-update', $page))
+                        @if(userCanOnAny('page-create'))
                             <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
-                            @if(userCan('page-delete', $page))
-                                <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
-                            @endif
+                        @endif
+                        @if(userCan('page-delete', $page) && userCan('page-update', $page))
+                            <li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
+                        @endif
+                        @if(userCan('page-update', $page))
                             <li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
                         @endif
                         @if(userCan('restrictions-manage', $page))
index c4b02f9b9714f9e30ea17ed4ab78af4a7a198eb2..1b0514f9cdc4de07a3cfcee151e3abb62c2ff44e 100644 (file)
@@ -38,8 +38,9 @@
                             <div class="form-group">
                                 <label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
                                 <select name="setting[language]" id="user-language">
+
                                     @foreach(trans('settings.language_select') as $lang => $label)
-                                        <option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
+                                        <option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
                                     @endforeach
                                 </select>
                             </div>
index 16ba113587e772d61a545a0cadcaafd6286ff4ca..5ccb1415e6fed57e2bced5e33e6d04b966cadf2a 100644 (file)
@@ -23,6 +23,7 @@ class LdapTest extends BrowserKitTest
             'auth.method' => 'ldap',
             'services.ldap.base_dn' => 'dc=ldap,dc=local',
             'services.ldap.email_attribute' => 'mail',
+            'services.ldap.display_name_attribute' => 'cn',
             'services.ldap.user_to_groups' => false,
             'auth.providers.users.driver' => 'ldap',
         ]);
@@ -45,6 +46,15 @@ class LdapTest extends BrowserKitTest
         });
     }
 
+    protected function mockUserLogin()
+    {
+        return $this->visit('/login')
+            ->see('Username')
+            ->type($this->mockUser->name, '#username')
+            ->type($this->mockUser->password, '#password')
+            ->press('Log In');
+    }
+
     public function test_login()
     {
         $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
@@ -60,11 +70,7 @@ class LdapTest extends BrowserKitTest
         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
         $this->mockEscapes(4);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
+        $this->mockUserLogin()
             ->seePageIs('/login')->see('Please enter an email to use for this account.');
 
         $this->type($this->mockUser->email, '#email')
@@ -90,11 +96,7 @@ class LdapTest extends BrowserKitTest
         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
         $this->mockEscapes(2);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
+        $this->mockUserLogin()
             ->seePageIs('/')
             ->see($this->mockUser->name)
             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
@@ -115,11 +117,7 @@ class LdapTest extends BrowserKitTest
         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
         $this->mockEscapes(2);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
+        $this->mockUserLogin()
             ->seePageIs('/login')->see('These credentials do not match our records.')
             ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
     }
@@ -196,12 +194,7 @@ class LdapTest extends BrowserKitTest
         $this->mockEscapes(5);
         $this->mockExplodes(6);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/');
+        $this->mockUserLogin()->seePageIs('/');
 
         $user = User::where('email', $this->mockUser->email)->first();
         $this->seeInDatabase('role_user', [
@@ -249,12 +242,7 @@ class LdapTest extends BrowserKitTest
         $this->mockEscapes(4);
         $this->mockExplodes(2);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/');
+        $this->mockUserLogin()->seePageIs('/');
 
         $user = User::where('email', $this->mockUser->email)->first();
         $this->seeInDatabase('role_user', [
@@ -303,12 +291,7 @@ class LdapTest extends BrowserKitTest
         $this->mockEscapes(4);
         $this->mockExplodes(2);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/');
+        $this->mockUserLogin()->seePageIs('/');
 
         $user = User::where('email', $this->mockUser->email)->first();
         $this->seeInDatabase('role_user', [
@@ -354,12 +337,7 @@ class LdapTest extends BrowserKitTest
         $this->mockEscapes(5);
         $this->mockExplodes(6);
 
-        $this->visit('/login')
-            ->see('Username')
-            ->type($this->mockUser->name, '#username')
-            ->type($this->mockUser->password, '#password')
-            ->press('Log In')
-            ->seePageIs('/');
+        $this->mockUserLogin()->seePageIs('/');
 
         $user = User::where('email', $this->mockUser->email)->first();
         $this->seeInDatabase('role_user', [
@@ -372,4 +350,63 @@ class LdapTest extends BrowserKitTest
         ]);
     }
 
+    public function test_login_uses_specified_display_name_attribute()
+    {
+        app('config')->set([
+            'services.ldap.display_name_attribute' => 'displayName'
+        ]);
+
+        $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
+        $this->mockLdap->shouldReceive('setVersion')->once();
+        $this->mockLdap->shouldReceive('setOption')->times(4);
+        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
+            ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
+            ->andReturn(['count' => 1, 0 => [
+                'uid' => [$this->mockUser->name],
+                'cn' => [$this->mockUser->name],
+                'dn' => ['dc=test' . config('services.ldap.base_dn')],
+                'displayName' => 'displayNameAttribute'
+            ]]);
+        $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
+        $this->mockEscapes(4);
+
+        $this->mockUserLogin()
+            ->seePageIs('/login')->see('Please enter an email to use for this account.');
+
+        $this->type($this->mockUser->email, '#email')
+            ->press('Log In')
+            ->seePageIs('/')
+            ->see('displayNameAttribute')
+            ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
+    }
+
+    public function test_login_uses_default_display_name_attribute_if_specified_not_present()
+    {
+        app('config')->set([
+            'services.ldap.display_name_attribute' => 'displayName'
+        ]);
+
+        $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
+        $this->mockLdap->shouldReceive('setVersion')->once();
+        $this->mockLdap->shouldReceive('setOption')->times(4);
+        $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
+            ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
+            ->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(6)->andReturn(true);
+        $this->mockEscapes(4);
+
+        $this->mockUserLogin()
+            ->seePageIs('/login')->see('Please enter an email to use for this account.');
+
+        $this->type($this->mockUser->email, '#email')
+            ->press('Log In')
+            ->seePageIs('/')
+            ->see($this->mockUser->name)
+            ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
+    }
+
 }
index 5d71ec6f6a15f12c5c5e1cd7e057f70ccd5de6bb..bdba812d59a16593516e96f99e0a6a78e64393a8 100644 (file)
@@ -1,5 +1,7 @@
 <?php namespace Tests;
 
+use BookStack\Auth\Role;
+use BookStack\Auth\User;
 use BookStack\Entities\Book;
 use BookStack\Entities\Bookshelf;
 
@@ -27,6 +29,22 @@ class BookShelfTest extends TestCase
         $resp->assertElementContains('header', 'Shelves');
     }
 
+    public function test_shelves_shows_in_header_if_have_any_shelve_view_permission()
+    {
+        $user = factory(User::class)->create();
+        $this->giveUserPermissions($user, ['image-create-all']);
+        $shelf = Bookshelf::first();
+        $userRole = $user->roles()->first();
+
+        $resp = $this->actingAs($user)->get('/');
+        $resp->assertElementNotContains('header', 'Shelves');
+
+        $this->setEntityRestrictions($shelf, ['view'], [$userRole]);
+
+        $resp = $this->get('/');
+        $resp->assertElementContains('header', 'Shelves');
+    }
+
     public function test_shelves_page_contains_create_link()
     {
         $resp = $this->asEditor()->get('/shelves');
index 11294f7dfdcd236dc7b0f6f8508650a09656a5e6..a3c20e84c5b517bede1e3b3488516a299c1e853a 100644 (file)
@@ -1,5 +1,6 @@
 <?php namespace Tests;
 
+use BookStack\Auth\Role;
 use BookStack\Entities\Book;
 use BookStack\Entities\Chapter;
 use BookStack\Entities\Page;
@@ -239,4 +240,35 @@ class SortTest extends TestCase
         $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
     }
 
+    public function test_page_can_be_copied_without_edit_permission()
+    {
+        $page = Page::first();
+        $currentBook = $page->book;
+        $newBook = Book::where('id', '!=', $currentBook->id)->first();
+        $viewer = $this->getViewer();
+
+        $resp = $this->actingAs($viewer)->get($page->getUrl());
+        $resp->assertDontSee($page->getUrl('/copy'));
+
+        $newBook->created_by = $viewer->id;
+        $newBook->save();
+        $this->giveUserPermissions($viewer, ['page-create-own']);
+        $this->regenEntityPermissions($newBook);
+
+        $resp = $this->actingAs($viewer)->get($page->getUrl());
+        $resp->assertSee($page->getUrl('/copy'));
+
+        $movePageResp = $this->post($page->getUrl('/copy'), [
+            'entity_selection' => 'book:' . $newBook->id,
+            'name' => 'My copied test page'
+        ]);
+        $movePageResp->assertRedirect();
+
+        $this->assertDatabaseHas('pages', [
+            'name' => 'My copied test page',
+            'created_by' => $viewer->id,
+            'book_id' => $newBook->id,
+        ]);
+    }
+
 }
\ No newline at end of file