Compare commits

...

448 Commits

Author SHA1 Message Date
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
d2a9b312e9 Fixed LDAP group sync using wrong user filter
LDAP group sync was trying to find users based on the external_auth_id
which is not garunteed to match the username entered so somtimes
the search for a user would fail.

This passes the username to the group sync.
Picked up by @yoyokko in #959.
2018-08-19 15:24:42 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
01260d95f3 Merge pull request #957 from moucho/master
Updated Spanish translation
2018-08-12 14:20:53 +01:00
Dan Brown
d69ba6b47a Updated composer dependancies 2018-08-12 13:42:17 +01:00
Dan Brown
098128aafb Added test to cover new language autodetect config option 2018-08-12 13:34:14 +01:00
Dan Brown
92c9837157 Fixed incorrect type error in LDAP group sync
Should fix #951
2018-08-12 13:28:40 +01:00
Marcos
18e5f86ffa Updated Spanish translation 2018-08-12 14:14:56 +02:00
Dan Brown
c860645a5a Tweaked bug report template to request hosting method 2018-08-12 13:12:47 +01:00
Dan Brown
fcb93dc7c8 Added option to disable public lang autodetect
Also cleaned up localization middleware a little.
Closes #944
2018-08-12 13:10:55 +01:00
Dan Brown
fcdb39e428 Merge pull request #942 from marcusforsberg/master
Updated Swedish translation
2018-08-12 12:51:00 +01:00
Dan Brown
1b3e1863f4 Merge pull request #948 from houbaron/fix/Chinese_translation
Fix/Chinese translation
2018-08-12 12:37:35 +01:00
Dan Brown
fbc2175789 Merge pull request #952 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-08-12 12:36:28 +01:00
Leonardo Martinez
8099c431bb Updated 'Spanish Argentina' translation. 2018-08-06 10:46:53 -03:00
Baron Hou
efbfe0f7af Update Traditional Chinese 2018-08-05 17:07:13 +08:00
Baron Hou
66402b474c Update Simplified Chinese 2018-08-05 17:05:41 +08:00
marcusforsberg
f47f0e05d6 Updated Swedish translation 2018-07-30 09:35:34 +03:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
c83a51f7e2 Merge pull request #904 from lommes/903-socialite-discord
add everything needed to use discord as social login provider
2018-07-29 16:18:10 +01:00
Dan Brown
b922c8029e Merge pull request #933 from nicobubulle/master
French translation update
2018-07-29 16:04:39 +01:00
Dan Brown
653761e67d Merge pull request #925 from alex2702/fix/835
Fixed German translations for notifications
2018-07-29 16:03:29 +01:00
Dan Brown
d59ff132ab Delete ISSUE_TEMPLATE.md 2018-07-29 15:55:13 +01:00
Dan Brown
e6e740b2a1 Update issue templates 2018-07-29 15:54:53 +01:00
Dan Brown
af6f4e6c8c Updated pagination to use theme colour 2018-07-29 15:44:10 +01:00
Dan Brown
69a0f8d502 Prevented error notification being visible on load
Fixes #897

Also made design a little more compact
2018-07-29 15:34:54 +01:00
Dan Brown
6d35fb5237 Updated packages via npm audit 2018-07-28 15:03:29 +01:00
nicobubulle
79d0f707e6 French translation update 2018-07-22 18:20:09 +02:00
alex2702
369dc02e78 Fixed German translations for notifications 2018-07-15 21:26:55 +02:00
Dan Brown
9d2e65b73d Merge branch 'brennanmurphy-master' 2018-07-15 19:36:28 +01:00
Dan Brown
f421d83627 Added ability to set custom ldap group -> role mapping
Added input in role form to allow matching against custom names.
Changed default mapping to use role display name instead of the hidden
DB name.
2018-07-15 19:34:42 +01:00
Dan Brown
be2ca9d4bb Refactored out the LDAP repo 2018-07-15 18:21:45 +01:00
Dan Brown
17bca662a7 Added tests to cover ldap group mapping
Also updated .env.example formatting.
Updated how LdapRepo uses Ldap so can be mocked by testing.
2018-07-15 17:57:25 +01:00
Dan Brown
1776204870 Merge branch 'master' of git://github.com/brennanmurphy/BookStack into brennanmurphy-master 2018-07-14 14:17:55 +01:00
Dan Brown
985e214d94 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-07-14 14:14:37 +01:00
Dan Brown
2bcc159fd6 Allowed creating pages in visible chapters in invisible books
Fixes permissions with test to cover in the event a page is created,
with permission, in a chapter but the user does not have permission to
see the parent book.

Fixes #912
2018-07-14 14:12:29 +01:00
Dan Brown
fb7c12438d Merge pull request #918 from DeehSlash/fix/pt_br_locale
Adds missing pt_BR strings
2018-07-14 10:31:18 +01:00
Dan Brown
b2cd363539 Added browserlist, Tweaked md scrollToText ot use ES6 2018-07-14 10:20:49 +01:00
Dan Brown
f668bee88b Merge branch 'master' into feature/edit-link-headers 2018-07-14 09:36:14 +01:00
André Luiz da Silva
642f2760cc Improves and adds missing pt_BR strings 2018-07-10 15:10:21 -03:00
Brennan Murphy
37aa8b05f8 Update files to PSR-2 standards 2018-07-02 17:27:43 +00:00
Brennan Murphy
d640cc1eee LDAP groups sync to Bookstack roles.
Closes #75
2018-07-02 17:09:39 +00:00
Abijeet Patro
c2d6e98985 Merge pull request #907 from BookStackApp/fix/date-image-manager
Changes the way the date is displayed in image-manager.
2018-07-02 00:34:30 +05:30
Dan Brown
84b4fe6176 Merge pull request #886 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-07-01 16:21:38 +01:00
Dan Brown
decdf5714b Merge pull request #865 from moucho/master
New strings from 0.22 release for Spanish translation
2018-07-01 16:20:47 +01:00
Dan Brown
9da600caf9 Merge pull request #906 from BookStackApp/bug/revision-wrap
Fixes issue with code not wrapping on revision page.
2018-07-01 16:18:49 +01:00
Dan Brown
45aee2a1c1 Merge pull request #874 from BookStackApp/fix/gototext
Fixes undefined error when clicking on link under page navigation.
2018-07-01 16:13:10 +01:00
Abijeet
f5df5ac7d5 Changes the way the date is displayed in image-manager.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-30 11:04:12 +05:30
Abijeet
fb29f4119d Fixes issue with code not wrapping on revision page.
Closes #888

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-30 09:49:55 +05:30
Timo B
93795b6eda add everything needed to use discord as social login provider 2018-06-28 09:01:36 +02:00
Leonardo Martinez
f7b808a9e6 Merge remote-tracking branch 'upstream/master' 2018-06-26 09:37:20 -03:00
Abijeet Patro
448068e318 Merge pull request #892 from BookStackApp/fix/884
Fixes issue with having to click the delete icon for attachment twice.
2018-06-17 18:29:32 +05:30
Abijeet
7d81a95156 Fixes issue with having to click the delete icon for attachment twice.
Fixes #884

This is happening because -

Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.

Source: https://vuejs.org/v2/guide/reactivity.html

Also added padding to the icons in the attachment section.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-17 14:21:31 +05:30
Leonardo Martinez
a9bf2ed398 Updated 'Spanish Argentina' translation. 2018-06-13 10:12:36 -03:00
Abijeet
771f781e7f Fixes a corner case with exclamation in the ID.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:29:30 +05:30
Abijeet
78be8535f7 Removed previous code that is now unneeded
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:19:03 +05:30
Abijeet
6c4c1ccb58 Changed the way we were displaying the edit icon.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:06:23 +05:30
Abijeet
562225a77b Added code to set the cursor at end of line while scrolling.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:04:54 +05:30
Abijeet
b936e1f403 Added code to handle scroll for markdown.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 13:11:10 +05:30
Dan Brown
b3cc3130f0 Added copy button to codemirror-rendered code blocks
Closes #858
2018-06-09 10:41:01 +01:00
Abijeet
0363fc4ea1 Fixes undefined error when clicking on page navigation links.
Fixes #873

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-03 14:24:55 +05:30
Abijeet
134a96fa32 Adds edit icon to each header in the page.
Towards #618

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-03 13:47:07 +05:30
Marcos
56f444a8a7 New strings 2018-05-30 01:41:25 +02:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
86f43c8a65 Fixed incorrect tag from removing ng tags 2018-05-28 11:06:11 +01:00
Dan Brown
d886c6a32e Removed old ng tags, Fixed header spacing
Also prevent pointer error on custom home page
2018-05-28 10:33:38 +01:00
Dan Brown
f399e60910 Made header link spacing a little more even 2018-05-27 20:32:06 +01:00
Dan Brown
173eaf1c98 Made comments section more subtle
Also removed spacing from within details above active restrictions
2018-05-27 20:20:13 +01:00
Dan Brown
64eabaf882 Fixed search icon overalapping input in header
Fixes #859
2018-05-27 19:51:32 +01:00
Dan Brown
6b84a76af1 Merge branch 'drawing_updates' 2018-05-27 19:42:25 +01:00
Dan Brown
2bd6ba9895 Added maintenance view with image-cleanup 2018-05-27 19:40:07 +01:00
Dan Brown
1df0bcaf85 Made image cleanup safer
Also fixed drawing update in markdown editor.
Added shortcut for MD editor to view drawing manager.
2018-05-27 14:33:50 +01:00
Dan Brown
c31e6a03ce Added command to clean-up old images, Unfinished 2018-05-20 18:16:01 +01:00
Dan Brown
61c9324229 Removed old image versions test 2018-05-20 17:12:44 +01:00
Dan Brown
8c4c8cd95b Updated secure-images option to not effect image name
Instead only the image path is altered.
Also fixed image manger mode not changing on button press.
2018-05-20 16:47:53 +01:00
Dan Brown
0c9c1e4c6b Reverted work on revisions
Improved linkage of drawings and image manager.
Updated image updates to create new versions.
2018-05-20 16:41:14 +01:00
Dan Brown
9ec114641c Merge pull request #846 from moucho/master
Updated Spanish translation
2018-05-20 15:20:33 +01:00
Dan Brown
295c7918a4 Merge pull request #851 from vriic/master
Update german translation
2018-05-20 12:06:44 +01:00
Dan Brown
3ac34b5849 Merge pull request #802 from marcusforsberg/master
Updated Swedish translation
2018-05-20 12:05:11 +01:00
Dan Brown
6e7adcc095 Embedded SVG icons in css/js files
Allows removal of hacky /icon endpoint solution.
Fixes PDF exports with WKHTML and allows the icon to show in HTML
exports.

Fixes #796
2018-05-20 11:55:23 +01:00
Dan Brown
a1ecdcacba Fixed attachment error handling, Allowed all link types
Related to #812
2018-05-20 11:06:10 +01:00
Dan Brown
019b8196ad Merge branch 'feature/615' 2018-05-20 10:13:34 +01:00
Dan Brown
63f96c1c6f Reorganised home and robots views
Extracted home view sidebar into own view.
Moved home and robot views into 'common' folder so that we only have
layouts in the top-level views folder.
2018-05-20 10:11:56 +01:00
Dan Brown
8df9dab80a Merge branch 'master' into feature/615 2018-05-20 09:51:45 +01:00
Dan Brown
93147f4340 Prevented back-to-top showing on flexbox-body pages
Fixes #824
2018-05-20 09:48:11 +01:00
Dan Brown
77727e7e50 Update session config to match laravel
Includes option to set secure cookies via env.
Closes #817
2018-05-20 09:38:27 +01:00
Dan Brown
9f4c64a676 Codemirror mode now correct for c-like langs
Fixes #849
2018-05-20 09:32:15 +01:00
Nikolai Nikolajevic
e0ebae19aa Update: Übersetzung 2018-05-20 03:00:55 +02:00
Dan Brown
6cdb943916 Started work on revisions in image manager 2018-05-19 18:44:40 +01:00
Dan Brown
d3d8ddbe52 Improved 404 handling and fixed editor error
404 handling now not a hack-around and uses Laravel 'fallback' routes
instead. Prevents errors with the session when you have mulitple errors
on a page where a post/put/delete is made.
2018-05-19 17:01:33 +01:00
Marcos
57c312ec3f Updated Spanish translation 2018-05-18 03:10:49 +02:00
Dan Brown
13ad0031d6 Drawings now generate revisions, not replace
Updated drawing update test to accomodate.
Image deletion system now takes revisions into account.
2018-05-13 17:41:35 +01:00
Dan Brown
d5b922aa50 Started work on drawing revisions
Improved sidebar and selection styling of image manager.
Allowed image manager imageType to be changed on open.
Created models for image revisions.
2018-05-13 12:07:38 +01:00
Abijeet
28823c4fae Changed the location of the "view-toggle" to be under the books views.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 18:26:35 +05:30
Abijeet
b6bb078e0a removed some added CSS as it was causing unintended sideffects.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 17:28:10 +05:30
Abijeet
8254c3be8d Added the book view toggle option on the homepage.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 14:16:05 +05:30
Abijeet
47cb99a2d6 Added test cases.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 13:07:28 +05:30
Abijeet
86b2ddbd28 Implemented displaying of the books list on home page. 2018-05-10 09:05:18 +05:30
Abijeet
2e4863edb1 Added an option to set books as the default homepage.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-09 08:26:49 +05:30
Dan Brown
b0d027a4a9 Repaired other uses of entity-meta view 2018-04-30 15:12:10 +01:00
Dan Brown
0c3c8fc9c3 Updated npm dependancies 2018-04-30 14:54:54 +01:00
Dan Brown
624c568008 Revamped tag styling 2018-04-30 14:35:15 +01:00
Dan Brown
58a0a59d7e Cleaned details sidebar box and merged with permissions 2018-04-30 13:53:04 +01:00
Dan Brown
3d0d7f8be2 Updated version for next block of development 2018-04-30 13:52:22 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
eb5069ca66 Attempted to fix failing time-based test 2018-04-22 20:06:46 +01:00
Dan Brown
0306253c45 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-04-22 12:26:21 +01:00
Dan Brown
71b6f09128 Applied phpcs findings 2018-04-22 12:25:32 +01:00
Dan Brown
67e0c3d2a5 Improved export base64 encoding of images
Now will use set storage mechanism to find image files.
Fixes #786

Added test to cover
2018-04-22 12:23:43 +01:00
Dan Brown
6aeb1387aa Fixed licence badge 2018-04-22 11:26:11 +01:00
Dan Brown
fa83e6bda4 Merge pull request #806 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-04-21 13:46:07 +01:00
Leonardo Martinez
ae89e05a25 Updated 'Spanish Argentina' translation. 2018-04-17 09:45:57 -03:00
Dan Brown
a50153d221 Slimmed down testing DB sized and improved permission caching 2018-04-14 22:17:47 +01:00
Dan Brown
cdb1c7ef88 Added destination permission checking to entity move 2018-04-14 18:47:13 +01:00
Dan Brown
0f7b0ad45a Added ability to copy a page
In 'More' menu alongside move.
Allows you to move if you have permission to create within the new
target parent.
Closes #673
2018-04-14 18:00:16 +01:00
marcusforsberg
6c5304a3de Updated Swedish translation 2018-04-14 18:09:09 +02:00
Dan Brown
d34b91f2c9 Updated move card width and made sidebar order more consistent 2018-04-14 16:23:16 +01:00
Dan Brown
dfadaa28f6 Updated reset-password flow design
Fixes #800
2018-04-14 16:16:29 +01:00
Dan Brown
fae564ff32 Merge pull request #798 from abno85/loc-de_DE
Update German localization
2018-04-14 16:02:48 +01:00
Dan Brown
502b22a0f2 Merge pull request #783 from moucho/master
Completely overhaul of the Spanish translation,  added missing strings
2018-04-14 16:01:25 +01:00
Dan Brown
f9feeef5c9 Merge pull request #780 from jasoncheng7115/master
Add Language zh_TW
2018-04-14 16:00:03 +01:00
Dan Brown
a6674a5a5e Merge pull request #767 from msaus/update_japanese_translation
update japanese translation
2018-04-14 15:58:35 +01:00
Dan Brown
fb18576259 Merge pull request #768 from BookStackApp/feature/tinymce-insert-video
Adds the media plugin to TinyMCE to allow insertion of videos.
2018-04-14 15:57:08 +01:00
Abijeet
7238a01f89 Moved the code to the wysiwyg-editor file.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:41:35 +05:30
Abijeet
93f92e9e16 Updated the TinyMCE to version 4.7.9.
Added some code to remove the box-shadow around the TinyMCE toolbar.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:30:34 +05:30
Abijeet
d92efd4edc Adds the media plugin to TinyMCE to allow insertion of videos.
Fixes #266

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:30:28 +05:30
abno85
448f7d091b Add comment_add
Missed 'comment_add' the last time
2018-04-13 08:44:54 +02:00
abno85
11470b85f9 Update German localization
Added a few missing strings and (hopefully) fixed my borked first commit.
2018-04-12 13:48:07 +02:00
Marcos
9c07619099 orthography 2018-04-05 03:57:35 +02:00
Marcos
60a224f7a1 Missing comma 2018-04-05 02:58:32 +02:00
Marcos
e392e1fd8b Completely overhaul of the Spanish translation, added missing strings 2018-04-03 15:58:04 +02:00
Jason Cheng
64d5763d08 Add zh_TW Locales.
Add zh_TW Locales.
2018-04-02 16:09:23 +08:00
Jason Cheng
007059273e Add translate.
Add translate.
2018-04-02 15:54:06 +08:00
Jason Cheng
106432ee4e Added Language zh_TW
Added Language zh_TW
2018-04-02 15:03:07 +08:00
Dan Brown
0ade9b5b9b Refactored moment.js out of app
Reduces bundle size by 25%
2018-04-01 14:10:44 +01:00
Dan Brown
736d7118b0 Refactored js file structure to be standard throughout app
Still work to be done but a good start in standardisation.
2018-04-01 13:21:11 +01:00
Dan Brown
b612cf9e4c Refactored out page-display system 2018-04-01 12:46:27 +01:00
Dan Brown
1a72208d27 Added configurable robots.txt file.
Deleted old static file.
Default output depends on app-public setting.
Otherwise can be overidden in `.env` file via `ALLOW_ROBOTS`
Otherwise view file can be customized.

Fixes #779
2018-03-31 12:41:40 +01:00
Dan Brown
7f437c2e3c Fixed issue where cover images don't save on older books
Ensured an existing ID is always provided to image-picker.js.
Fixes #773
2018-03-31 11:21:22 +01:00
Dan Brown
cfdf5b93d9 Merge branch 'v0.20' to gain export fix 2018-03-30 15:45:34 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
3cd08382e9 Fixed export style paths 2018-03-30 15:31:39 +01:00
Dan Brown
58a6b2df7d Merge branch 'master' of github.com:BookStackApp/BookStack 2018-03-30 14:10:36 +01:00
Dan Brown
582158f70e Added tags to chapters and books
Closes #121
2018-03-30 14:09:51 +01:00
msaus
03ee3d21ba Merge branch 'master' into update_japanese_translation 2018-03-28 11:58:14 +09:00
Abijeet Patro
b99229a5c3 Merge pull request #769 from BookStackApp/psr-2-fixes
PSR2 fixes after running `./vendor/bin/phpcbf`
2018-03-28 01:09:09 +05:30
Abijeet
2fc513984d PSR2 fixes after running ./vendor/bin/phpcbf
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-28 01:07:01 +05:30
Soseki Masao
34499658d5 update japanese translation 2018-03-27 16:18:38 +09:00
Dan Brown
a8f18c0102 Updated COC with criticism point 2018-03-25 18:16:53 +01:00
Dan Brown
c85cace48b Updated readme, license and added code of conduct 2018-03-25 18:12:49 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
4f788384f0 Updated icons with height
Fixes issues within IE
2018-03-25 15:52:48 +01:00
Dan Brown
23f90ed6b4 Ensured uploaded system images remain public
Also added tests to cover local_secure image storage.

Fixes #725
2018-03-25 12:41:52 +01:00
Dan Brown
f1586be516 Removed invalid bracket from view 2018-03-25 11:35:58 +01:00
Dan Brown
1a9f676416 Updated create routes to prevent slug clashes
Fixes #758
2018-03-25 11:34:42 +01:00
Dan Brown
df1a3a0715 Properly escaped search results
Prevents vue-like syntax in results causing errors.
Related to #748
2018-03-25 11:06:21 +01:00
Dan Brown
1e015af3c9 Fixed incorrect search logic in last commit
Incorrect cross-entity pagination could lead to hidden entities.
2018-03-24 19:05:56 +00:00
Dan Brown
f101c1a622 Made search more efficient and tweaked weighting
Added per-entity weighting changes.
Now Books score higher than chapters which score higher than pages.

Reduced queries required on search by only searching once but at a
higher count to see if there's another page.
2018-03-24 18:46:31 +00:00
Dan Brown
3df7d828eb Fixed failing tests
Fixed syntax error in french translations.
Removed 'required' on image validation which was breaking tests
2018-03-24 15:25:13 +00:00
Dan Brown
5ad9c5d319 Merge branch 'bug/gif-image-740' of git://github.com/Abijeet/BookStack
Also removed console.logs in dropzone.js
2018-03-24 14:54:50 +00:00
Dan Brown
9fead9890b Merge branch 'Abijeet-bug/image-upload' 2018-03-24 14:45:10 +00:00
Dan Brown
746684ec8c Merge branch 'bug/image-upload' of git://github.com/Abijeet/BookStack into Abijeet-bug/image-upload 2018-03-24 14:39:57 +00:00
Dan Brown
2ede273ef3 Merge pull request #753 from Alwaysin/master
Update french language
2018-03-24 14:36:12 +00:00
Dan Brown
6882bd3c62 Merge pull request #752 from Alwaysin/patch-1
Update entities.php for french language
2018-03-24 14:35:04 +00:00
Dan Brown
1061946858 Merge pull request #761 from msaus/hotfix/japanese_translation
update japanese translation
2018-03-24 14:32:34 +00:00
Soseki Masao
696ef3ff33 fix entities.php 2018-03-23 18:20:44 +09:00
Soseki Masao
2d1567ea30 update japanese translation 2018-03-23 17:35:21 +09:00
Abijeet
2cfcbe0a3c Fixes an issue with handling of large image files.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-19 02:09:01 +05:30
Abijeet
bf8dddd99c Not resizing gif images.
See - https://github.com/Intervention/image/issues/176

Fixes #223

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-19 01:44:33 +05:30
Abijeet Patro
0335f58478 Merge branch 'master' into bug/image-upload 2018-03-18 23:44:33 +05:30
Abijeet
3a5c20c17e Removing the selected image and clearing the dropdzone on dialog close.
Towards #741

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-18 23:38:37 +05:30
Dan Brown
380e2ff668 Updated outline button styles for svg icons 2018-03-18 15:35:04 +00:00
Dan Brown
c6844324d0 Use autodiscover for dev packages
Allows installation with `composer install --no-dev`
Fixed #742
2018-03-18 15:27:15 +00:00
Dan Brown
ecdeb545e0 Cleaned some form styling
Removed uppercasing of labels to make a little friendlier.
Extracted out toggleswitch JS into own component.
Improved basic code input for html-head-input area.
2018-03-18 15:13:46 +00:00
Dan Brown
2c8d7da885 Updated webpack SCSS extract to provide sourcemaps 2018-03-18 14:47:43 +00:00
Alwaysin
35c7e00203 Update entities.php 2018-03-18 14:46:56 +01:00
Abijeet
83d830fd7d Fixes the icons not being aligned properly in attached items section for the page.
Also formatting.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-18 18:58:04 +05:30
Alwaysin
accd936781 Update settings.php 2018-03-18 14:24:39 +01:00
Alwaysin
880987f15c Update components.php 2018-03-18 14:23:42 +01:00
Alwaysin
018084a951 Update common.php 2018-03-18 14:23:08 +01:00
Alwaysin
098b594104 Update auth.php 2018-03-18 14:21:11 +01:00
Alwaysin
bb7fab1dc0 Update activities.php 2018-03-18 14:20:20 +01:00
Abijeet
d859be3a12 Fixes a number of issues with the image uploader. Read below,
- Added a remove link to remove files that have an error.
- Error will appear below the progress bar.
- Hovering on dz-image or dz-details will display the error message. Otherwise error message was covering the remove link as well.
- Removed styling around the file size.
- Removed gradient effect in accordance with BookStack styling.
- Dropzone filenae will not overflow the container element. Also done for page attachments
- Added a 'uploaded' error message. this error was being thrown when the file size exceeded the server configured file size. (https://stackoverflow.com/a/42934387/903324)

Towards #741

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-18 18:44:11 +05:30
Dan Brown
8b27ce3296 Fixed large content previews and improved mobile styles
Listing content previews no longer pre-wrap and instead simply break
correctly.
Updated titles to ensure they break on mobile devices.
Reduced spacing and font sizes on mobile to better adjust content to
screen size.

Fixes #739
2018-03-18 12:23:48 +00:00
Dan Brown
8828adfc9c Fixed up notification styling a little 2018-03-18 11:58:45 +00:00
Dan Brown
d44e0b7964 Prevented markdown editor pushing out toolbar 2018-03-18 11:46:08 +00:00
Dan Brown
0372efa89a Merge branch 'patch-1' of git://github.com/BackwardSpy/BookStack into BackwardSpy-patch-1 2018-03-18 11:40:38 +00:00
Dan Brown
d2eec4fbce Markdown editor image paste sets cursor correctly
Now sets cursor to alt text rather than end of placeholder image.
Fixed #751
2018-03-18 11:33:30 +00:00
Dan Brown
b42b07179f Updated exports to use DejaVu font
Provides potentially better language font coverage.
2018-03-17 17:12:01 +00:00
Dan Brown
1ad6fe1cbd Added togglable script escaping to page content
Configurable via 'ALLOW_CONTENT_SCRIPTS' env variable.
Fixes #575
2018-03-17 15:52:42 +00:00
Dan Brown
0a1546daea Moved jQuery to use NPM and fixed some asset refs 2018-03-17 15:20:15 +00:00
Dan Brown
b64940be82 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-03-17 13:05:37 +00:00
Dan Brown
2ff2c0b257 Merge branch 'webpack-2018' 2018-03-17 13:05:25 +00:00
Dan Brown
ced4e58137 Finished off intitial conversion to webpack 2018-03-17 13:03:13 +00:00
Abijeet
f42d355fd7 Fixes issue with the validation message not being translated.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-13 09:10:23 +05:30
Dan Brown
28a5bd24b0 Merge pull request #743 from cipi1965/master
Update Italian translation
2018-03-12 19:33:21 +00:00
Matteo Piccina
85605ab570 Update Italian translation 2018-03-12 16:43:19 +01:00
Dan Brown
5e41933773 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-03-11 16:35:14 +00:00
Dan Brown
f6f44c9de8 Reorganised dev-deps and updated moment 2018-03-11 16:34:58 +00:00
Dan Brown
e52bfc0c24 Attempted move to webpack again 2018-03-11 16:16:30 +00:00
Dan Brown
c44c42103c Merge pull request #711 from duncanbarnes/master
Added ability to configure email sender name
2018-03-10 17:45:16 +00:00
Dan Brown
1e98759722 Merge pull request #717 from owcz/patch-1
typo in readme.md
2018-03-10 17:43:10 +00:00
Dan Brown
bf4b95f929 Merge pull request #714 from pataar/patch-1
Add CACHE_PREFIX to the .env.example file
2018-03-10 17:42:39 +00:00
Dan Brown
a4072365e3 Merge pull request #718 from artur-trzesiok/master
add missing polish translations for comments
2018-03-05 20:26:26 +00:00
Dan Brown
2bd977b7d7 Merge pull request #709 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-03-05 20:22:26 +00:00
Pieter
496289ad94 Update .env.example 2018-03-05 13:10:59 +01:00
Artur Trzęsiok
98a3c815cf add missing polish translations for comments 2018-02-26 23:49:58 +01:00
Wolf
01e03f5a0f typo in readme.md 2018-02-26 09:10:43 -05:00
Chris Latham
1c8a8acb3d fix markdown editor resizing with long strings 2018-02-26 11:31:11 +00:00
Pieter
70cfb6624d Add CACHE_PREFIX to the .env.example file
We had some problems with multiple BookStack instances using the same caching server. Perhaps it's a good idea to have this available in the `.env.example` file.
2018-02-26 09:51:53 +01:00
Leonardo Martinez
fb48b025f3 Updated 'Spanish Argentina' translation. 2018-02-21 13:20:12 -03:00
Duncan Barnes
9a88b2cd0c Added ability to configure email sender name
Added env variable MAIL_FROM_NAME to allow the email sender name to be
customised. Also added MAIL_FROM to .env.example
2018-02-21 18:24:19 +09:00
Leonardo Martinez
395d02ef81 Updated 'Spanish Argentina' translation. 2018-02-19 10:15:24 -03:00
Dan Brown
67332a2f1b Merge pull request #704 from BookStackApp/svg_icons
Override-able SVG Icons
2018-02-17 19:51:59 +00:00
Dan Brown
81fa021083 Finished migrated from icon-font to SVG 2018-02-17 19:49:00 +00:00
Dan Brown
5ab39bfd5a Started migration to SVG icons 2018-02-17 13:30:52 +00:00
Dan Brown
dc1a16be4c Made it possible to override icons via custom theme 2018-02-17 12:36:24 +00:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
981d215155 Tweaked some comments 2018-02-11 18:18:16 +00:00
Dan Brown
2d43ab8a1b Fixed text overflow in various views
Fixes #669
2018-02-11 14:28:26 +00:00
Dan Brown
548dcd4db1 Fixed error when accessing non-authed attachment
Also updated attachment tests to use standard test-case.
Fixes #681
2018-02-11 12:37:02 +00:00
Dan Brown
2d41a4f064 Updated twitch SVG icon with vector SVG 2018-02-11 12:01:07 +00:00
Dan Brown
110f32a16d Merge branch 'master' of git://github.com/moutonnoireu/BookStack into moutonnoireu-master
Also updated composer deps
2018-02-11 11:44:09 +00:00
Dan Brown
bed7ba78d3 Updated grid view to use CSS grid and flexbox
Provides a cleaner height-matched design.
Closes #701
2018-02-11 11:36:51 +00:00
Dan Brown
2533db260d Merge branch 'master' of github.com:BookStackApp/BookStack 2018-02-04 18:14:41 +00:00
Dan Brown
87a45edde9 Merge branch 'pixwell-dev-support_for_gitlub_auth' 2018-02-04 18:14:16 +00:00
Dan Brown
9becc8055b Merge branch 'support_for_gitlub_auth' of git://github.com/pixwell-dev/BookStack into pixwell-dev-support_for_gitlub_auth 2018-02-04 17:51:30 +00:00
Dan Brown
d84f75c257 Merge pull request #695 from Yoginth/patch-2
Added Search Permission for not logged in Users
2018-02-04 17:48:25 +00:00
Dan Brown
1d49b65c2e Fixed code block wrapping on export
Now wraps instead of running off the page.

Fixed #676
2018-02-04 17:35:01 +00:00
Dan Brown
7c44f5462c Prevent image manager search from reloading page
Fixes #697
2018-02-04 17:18:55 +00:00
Dan Brown
b7e5cc6763 Merge pull request #696 from yuezhihan/master
Added simplified Chinese(zh-CN) language
2018-02-04 17:15:31 +00:00
Yue Zhihan
ab3231b550 Added 'zh_CN' to app.locales 2018-02-04 22:05:29 +08:00
Yue Zhihan
d65cd53c99 Added simplified Chinese(zh-CN) language 2018-02-04 21:42:19 +08:00
Dan Brown
46ea90c36e Merge pull request #692 from lommes/master
Corrected the keys for okta auth
2018-02-04 11:41:51 +00:00
Dan Brown
a45922616f Made default books view configurable in .env
Under 'APP_VIEWS_BOOKS' key.
Closes #675
2018-02-04 11:36:58 +00:00
Yoginth
ecf68b6365 Update all.blade.php 2018-02-03 20:15:36 +05:30
Jozef Balún
194bb0f042 add missing icon, fix name conventions 2018-02-01 18:26:19 +01:00
BlackSheep
addfb96002 reduced icon size 2018-02-01 09:55:37 +01:00
BlackSheep
6f7cfe7206 Update .env.example 2018-02-01 08:53:08 +01:00
BlackSheep
f51e0e9eb9 Update services.php 2018-02-01 08:51:35 +01:00
Timo Bartholomes
3cf2c6a027 Corrected the keys for okta auth 2018-01-31 21:11:17 +01:00
Jozef Balún
8b125be8f6 add missing lock file 2018-01-31 16:08:39 +01:00
Jozef Balún
44d8f39037 add support for gitlab authentification 2018-01-31 16:02:07 +01:00
BlackSheep
1651c807cb Update... 2018-01-30 09:59:56 +01:00
BlackSheep
5e2bf7c3e4 Add twitch socialite auth provider 2018-01-29 09:28:56 +01:00
Dan Brown
1d5440493c Set markdown editor preview width to 100%
Fixes #658
2018-01-28 18:14:02 +00:00
Dan Brown
59e809be16 Added command to add a new admin user
Closes #609
2018-01-28 18:09:26 +00:00
Dan Brown
ec050a5eef Fixed validation issue on register post
Added test to cover and also cleaned up RegisterController comments.

Fixes #670
2018-01-28 17:15:30 +00:00
Dan Brown
62342433f4 Set /app PHP code to PSR-2 standard
Also adde draw.io to attribution list.

Closes #649
2018-01-28 16:58:52 +00:00
Dan Brown
30b4f81fc6 Merge branch 'Abijeet-bug-650' 2018-01-28 14:20:35 +00:00
Dan Brown
bd711d69e4 Adapted code insert area and language select for mobile 2018-01-28 14:19:54 +00:00
Dan Brown
98d4bf4486 Merge branch 'bug-650' of git://github.com/Abijeet/BookStack into Abijeet-bug-650 2018-01-28 14:15:31 +00:00
Dan Brown
ead4b14d94 Updated user profile image delete to delete all uploads
Also moved test and made more comprehensive
2018-01-28 14:08:14 +00:00
Sampath Kumar
35e00ddb95 #630: Deleting user's profile pics on deleting of user account (#646)
* Issue-630: Fixed issue with deleting user profile pics when deleting a user.

* Issue #630: Deleting user's profile pics on deleting of user account

* Issue-630: Added test case for deleting user
2018-01-28 13:50:24 +00:00
Dan Brown
4eb5205070 Merge pull request #679 from marcusforsberg/master
Added Swedish translation
2018-01-28 13:40:01 +00:00
Dan Brown
1d1cc19596 Merge pull request #632 from BookStackApp/draw.io
draw.io integration
2018-01-28 13:39:14 +00:00
Dan Brown
faf7c55fdd Actually fixed the BaseURL this time 🤦 2018-01-28 13:33:50 +00:00
Dan Brown
ba6eb6727a Fixed test failing from missing baseURL
Also updated image upload test to delete before upload to prevent failed
tests breaking subsequent tests.
2018-01-28 13:27:41 +00:00
Dan Brown
88d09a2a3b Added drawing endpoint tests
Also refactored ImageTests away from BrowserKit
Also added image upload type validation.
2018-01-28 13:18:28 +00:00
marcusforsberg
daa11c3f13 Added Swedish locale to config 2018-01-26 20:27:28 +01:00
marcusforsberg
682bc9f896 Added Swedish translation 2018-01-26 20:16:35 +01:00
Dan Brown
9bbef3a3dd Added drawio abilities to markdown editor 2018-01-20 20:40:21 +00:00
Dan Brown
1411ee86b3 Extracted draw.io functionality to own file 2018-01-20 16:32:13 +00:00
Dan Brown
56264551e7 Added drawing icon and made drawio disablable 2018-01-20 15:00:54 +00:00
Dan Brown
0c383eee5b Merge branch 'master' into draw.io to fetch auth image changes 2018-01-20 14:06:44 +00:00
Dan Brown
f4bfbf91db Merge pull request #665 from BookStackApp/authed_images
Adds ability to secure images behind auth
2018-01-20 14:05:03 +00:00
Dan Brown
34782fbc91 Merge branch 'master' into draw.io 2018-01-20 14:01:56 +00:00
Dan Brown
1bfd77e7a1 Added drawing update ability 2018-01-20 14:01:35 +00:00
Dan Brown
5b075aa9bd Merge branch 'Abijeet-bug-638' 2018-01-13 16:45:28 +00:00
Dan Brown
281da59bae Refactored book sort using collections 2018-01-13 16:44:47 +00:00
Dan Brown
0afa417b0a Added ability to secure images behind auth
Still in testing.
Adds STORAGE_TYPE=local_secure option for setting images to be behind
auth. Stores images alongside attachments in /storage/uploads/images.
2018-01-13 11:11:23 +00:00
Abijeet
f2c62765ca Adds overflow:auto to popup content to allow it to scroll in lower res.
Fixes #650

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-01-06 01:41:06 +05:30
Abijeet
a77756a2da Refactored the code to first check for the permissions before sorting the book.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-01-06 01:04:48 +05:30
Dan Brown
6988a6ff88 Added view override support
Relates to #652
2017-12-31 16:25:58 +00:00
Abijeet
e269cc7ea7 Adds test case for sorting permissions.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-31 20:17:08 +05:30
Abijeet
e13e71cbe0 Changed the sort view to only show books to which we have an update permission.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-31 16:44:46 +05:30
Abijeet
4a24d1c31b Checks the target and the source book before performing the sort.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-31 16:25:21 +05:30
Dan Brown
96b8c403a8 Fixed failing book view test
Also ensured setting system localcache is cleared correctly
2017-12-30 16:09:27 +00:00
Dan Brown
359b1b40a2 Fixed broken table/ol/ul page includes
Fixes #640
2017-12-30 15:50:33 +00:00
Dan Brown
920964a561 Enabled system-storage of drawings made via draw.io 2017-12-30 15:26:39 +00:00
Dan Brown
7bb336d1a8 Merge pull request #644 from Abijeet/bug-643
Adds font-size to ol to ensure that they are uniform.
2017-12-30 12:41:13 +00:00
Dan Brown
141bf22725 Updated book view change to PATCH + other amends
Moved toggle to right of header bar and added unique text and icon for
each view type.

Removed old profile setting to keep things clean.
2017-12-29 16:49:03 +00:00
Dan Brown
1aa4d0dc59 Merge branch 'feature-613' of git://github.com/Abijeet/BookStack into Abijeet-feature-613 2017-12-29 16:25:15 +00:00
Dan Brown
0c1b1cd435 Standardised admin role check 2017-12-29 16:14:20 +00:00
Dan Brown
3eb2246291 Merge branch 'feature-579' of git://github.com/Abijeet/BookStack into Abijeet-feature-579 2017-12-29 16:03:34 +00:00
Abijeet
937d2cd55c Adds font-size to ol to ensure that they are uniform.
Fixes #643

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-28 22:32:24 +05:30
Dan Brown
afe781bc39 Enabled session in 404 responses
Fixes #634
2017-12-28 13:19:02 +00:00
Abijeet
d5a2529775 Adds test cases and fixes an issue with the permission checking.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-26 15:46:20 +05:30
Abijeet
0d4db603a4 Adds button to allow users to toggle the book view via the books list page.
Closes #613

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-26 12:38:16 +05:30
Abijeet
7da8804753 Adds code to allow deletion of users via cmd line.
Fixes #579

Command:

php artisan bookstack:delete-users

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2017-12-26 02:22:41 +05:30
Dan Brown
8b160b9fb4 Updated pull-request info to be clearer
Also updated dev version
2017-12-24 14:42:31 +00:00
Dan Brown
0dc1f0b07f Started draw.io integration 2017-12-24 14:28:35 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
03eb63ec77 Made it possible to pre-fill login via url
Allows email to be passed to email field.
Also allows password only if in demo mode (Due to security concerns).
2017-12-10 13:56:25 +00:00
Dan Brown
3ed5426315 Moved book cover image input into collapsible section
Prevent extra friction when creating a new book and makes it easier to
skip if grid view is not in use
2017-12-10 13:46:50 +00:00
Dan Brown
90bf13c1ab Updated okta config keys, made SVG fully vector
Also added some additional error handling to login.
2017-12-09 13:32:45 +00:00
Dan Brown
d17eb0f54c Merge branch 'master' of git://github.com/lommes/BookStack into lommes-master 2017-12-09 12:48:08 +00:00
Dan Brown
ac7e3977de Fixed WYSIWYG fullscreen mode on firefox
Prevented overlapping sidebar and collapsed content.
Fixes #605
2017-12-09 12:38:30 +00:00
Dan Brown
d7edc389a6 Enabled custom HTML head content to work within editors
Closes #562
2017-12-08 11:52:43 +00:00
Dan Brown
56d5af1336 Made it possible to configure proxies via env
In reference to #146
2017-12-07 19:46:47 +00:00
Dan Brown
06cf175b08 Prevented page navigation highlighting erroring
This was when no page nav was on the page
2017-12-07 19:27:54 +00:00
Dan Brown
b65abd25e0 Made small var name and formatting tweaks 2017-12-07 19:19:25 +00:00
Dan Brown
a5e49f642b Merge branch 'disable-comments' of git://github.com/Abijeet/BookStack into Abijeet-disable-comments 2017-12-07 19:15:26 +00:00
Dan Brown
91444e83fd Cleaned up some page-show JS 2017-12-07 19:10:31 +00:00
Dan Brown
6063ac4a11 Merge branch 'master' of git://github.com/Abijeet/BookStack 2017-12-07 18:47:07 +00:00
Dan Brown
02fd1c48ed Added meta+enter shortcut for page save
Closes #604
2017-12-07 18:44:20 +00:00
Dan Brown
6ee35f55cc Refactored image picker to js component
Also adjusted default cover image size
2017-12-06 17:32:29 +00:00
Dan Brown
261e57fc4e Converted books view setting to user setting
Also cleaned up/moved new CSS and removed redundant new book methods.
2017-12-06 16:34:26 +00:00
Dan Brown
bc1302a8d8 Merge branch 'BookStackApp-master' of git://github.com/OsmosysSoftware/BookStack into OsmosysSoftware-BookStackApp-master 2017-12-06 15:52:54 +00:00
Dan Brown
eeb2b8cbe5 Prevented finding of check script in lang tests 2017-12-06 11:17:34 +00:00
Dan Brown
b167ae795e Added script to check translation files
Closes #373
2017-12-04 20:25:04 +00:00
Dan Brown
6ebe8bf619 Fixed conflicting PDF facade namespace and corrected php version
Updated composer to have the correct config to install dependancies that
work for 7.0
2017-12-04 17:59:53 +00:00
Timo Bartholomes
009af9736e Add socialite authentication for okta 2017-11-26 16:41:29 +01:00
Dan Brown
7668a999a2 Fixed heavy init breakages made in last commit 2017-11-19 18:31:24 +00:00
Dan Brown
ed88c623d6 Made some further laravel 5.5 cleanup
Removed old bootstrap files that are not needed and
amended composer to laravel upgrade guide
2017-11-19 17:59:12 +00:00
Dan Brown
873b1099f8 Updated to laravel 5.5
Closes #590
2017-11-19 15:56:19 +00:00
Abijeet
6a54733f2b Adding testcases for comments disable / enable setting. 2017-11-16 23:32:36 +05:30
Abijeet
7a5bd23909 Added language translation for the new settings icons. 2017-11-16 00:22:22 +05:30
Abijeet
6bb7b5465f Added code in the settings to disable comments. Based on that hiding the comments section on the page display. 2017-11-16 00:05:24 +05:30
Abijeet Patro
0b967d84ad Merge pull request #2 from BookStackApp/master
Getting the latest code.
2017-11-15 08:05:20 +05:30
Abijeet
2261308415 Removed invalid comments, and formatted the code. 2017-11-15 00:04:35 +05:30
Abijeet
7b5edb4d62 Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-11-15 00:01:48 +05:30
Abijeet
8378f06889 Highlights all headings currently visible. Also fixes extra scrollbar appearing in Firefox. 2017-11-14 23:54:25 +05:30
Abijeet Patro
10dc851697 Merge pull request #1 from BookStackApp/master
Getting the latest changes.
2017-11-13 23:22:45 +05:30
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
65579214e2 Allowed custom session expiry time
Closes #570
2017-11-11 18:30:55 +00:00
Dan Brown
d89440d198 Fixed required email confirmation with domain restriction
Added test to cover scenario.

Closes #573
2017-11-11 18:09:48 +00:00
Dan Brown
08e58bab79 Fixed vue component error 2017-11-11 17:10:15 +00:00
Dan Brown
d29b177c84 Merge pull request #563 from 10bass/master
Fix ajax tag suggestion for subdir installs
2017-11-11 17:03:36 +00:00
Dan Brown
151d72e42c Merge pull request #558 from lbguilherme/ptbr
Update pt_BR translations
2017-11-11 16:30:16 +00:00
Dan Brown
711ba258f1 Prevented mulitple hypens incorrectly in slug
Added test to check slug format.
Fixes #589
2017-11-11 16:27:29 +00:00
Dan Brown
df4d4f30f1 Added package-lock file for npm version locking 2017-11-11 16:19:24 +00:00
Dan Brown
f094837709 Added test to cover multi-byte slugs
Also removed check for 'mb_' functions since mbstring is a dependancy
2017-11-11 16:15:08 +00:00
Dan Brown
e27cbb9dce Merge branch 'wowkaster-patch-1' 2017-11-11 16:07:23 +00:00
Abijeet
bdba25b6f2 Refactored all functionality into one function. Changed margin-top. 2017-11-05 20:23:16 +05:30
Vladimir
6b2581de63 Russian slug and Multibyte String 2017-11-03 14:00:07 +02:00
Abijeet
1031c61d0c Fixes #466. Adds support for header highlighting using intersection observer. 2017-11-02 01:14:06 +05:30
10bass
46fc0e5026 Fix ajax tag suggestion for subdir installs
tag suggest URLs were hardcoded to /ajax in blade template. Wrapped them in baseUrl()
2017-10-16 18:24:47 -04:00
Guilherme Bernal
332f678ed0 Update pt_BR translations 2017-10-15 16:15:53 -03:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
0d5d77d8ab Updated search test to fit with new tokenization 2017-10-15 19:24:06 +01:00
Dan Brown
db51cee2d8 Prevented custom homepage being deleted
Fixes #546
2017-10-15 19:14:46 +01:00
Dan Brown
a988438946 Expanded list of indexing split chars
Expands on #531
2017-10-15 19:14:31 +01:00
Dan Brown
3bf7cac030 Prevented flexbox contains overflowing page
Fixes #552
2017-10-15 18:34:37 +01:00
Dan Brown
79c3a07e9a Fixed include syntax erroring within vue
Fixes #553
2017-10-15 18:20:13 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
9758872baf Updated image fetching in exporting
Added domain check to see if possibly local even when whole url found.
Changed image fetch from file_get_contents to curl for external
resources.

Hopeful solution to #392
2017-10-06 20:49:25 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
b711bc6816 Prevented 'Discard draft' option showing after saving a draft page 2017-10-01 18:11:24 +01:00
Dan Brown
247e6dba85 Fixed some design issues around cards
Reverted drop shadow change.
Fixed header line-height when linked.
Fixed overflowing paragraph text. Fixes #533.
2017-10-01 17:59:51 +01:00
Dan Brown
2b3d6e4e4a Updated search-regen command description 2017-10-01 17:51:59 +01:00
Dan Brown
6b1980c4f3 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-10-01 13:19:41 +01:00
Dan Brown
9ba29770e1 Added azureAD social auth option
Closes #509
2017-10-01 13:19:17 +01:00
Dan Brown
3d375fae55 Merge pull request #529 from cipi1965/master
Updated italian translation
2017-10-01 11:44:45 +01:00
Dan Brown
c99a50de2c Merge pull request #528 from turbotankist/master
russian lang fixes
2017-10-01 11:43:31 +01:00
Dan Brown
1a32b25b5e Merge pull request #523 from sanderdw/master
Update dutch translations
2017-10-01 11:33:22 +01:00
Dan Brown
481aa5b5b0 Added 'last_commented' sort option to search
Closes #440
2017-10-01 11:24:33 +01:00
Dan Brown
c943eb4d0d Removed empty string null middleware as was causing issues 2017-09-30 14:44:52 +01:00
Dan Brown
aca6de49b0 Added missing middleware to trim input 2017-09-30 14:31:27 +01:00
Dan Brown
5fd04fa470 Updated search indexer to split words better
Will now split up words based on more chars than just spaces.
Not takes into account newlines, tabs, periods & commas.

Fixed #531
2017-09-30 14:14:23 +01:00
Dan Brown
87339e4cd0 Added missing codemirror theme class
Fixes #535
2017-09-30 13:48:38 +01:00
Dan Brown
a9eb058dad Updated issue template 2017-09-30 13:41:06 +01:00
Dan Brown
61fad6a665 Finished migration of last angular code 2017-09-30 13:27:08 +01:00
Matteo Piccina
fa4bee2d98 Updated italian translation 2017-09-27 10:44:08 +02:00
alexey
ce63260fa6 russian lang fixes 2017-09-27 11:17:56 +03:00
Dan Brown
a3557d5bb2 Tweaked shadows on cards 2017-09-24 18:47:34 +01:00
Dan Brown
9ca22976c3 Migrated editor toolbox, No more directives! 2017-09-24 18:30:21 +01:00
Dan Brown
9e2934fe17 Migrated editor inputs to non-angular JS 2017-09-23 12:24:06 +01:00
sanderdw
2259263214 Update entities.php 2017-09-23 00:52:08 +02:00
sanderdw
762cf5f183 Update components.php 2017-09-23 00:47:02 +02:00
sanderdw
07175f2b3e Update dutch translations 2017-09-23 00:28:25 +02:00
Bharadwaja G
5034f21394 Added migration file. 2017-09-05 19:53:29 +05:30
Bharadwaja G
e02fcbe983 Added Book cover image description in all languages. 2017-09-05 12:46:31 +05:30
Bharadwaja G
1c88d21abf Fixed books cover image ratio. 2017-09-04 20:50:24 +05:30
Bharadwaja G
c1a1bc0135 Books grid view 2017-09-04 20:27:52 +05:30
Bharadwaja G
6200948eec Merge branch 'master' of git://github.com/BookStackApp/BookStack into BookStackApp-master
Conflicts:
	app/Http/Controllers/BookController.php
	resources/lang/en/common.php
	resources/views/books/create.blade.php
	resources/views/books/form.blade.php
	resources/views/books/index.blade.php
	resources/views/users/edit.blade.php
	tests/Entity/EntityTest.php
2017-08-29 12:19:00 +05:30
Bharadwaja G
7f902e41c7 Resolved conflicts 2017-08-24 12:21:43 +05:30
Nilesh Deepak
3079a9f4de Reverted required changes. 2017-07-15 19:07:32 +05:30
Nilesh Deepak
a7d2cfdee2 Resolving test cases 2017-07-15 19:03:02 +05:30
Nilesh Deepak
a149e87ca7 Resolving test cases 2017-07-15 19:00:23 +05:30
Nilesh Deepak
854fd52a27 Resolving test cases 2017-07-15 18:57:09 +05:30
Nilesh Deepak
3d808ac75f Test for cover image 2017-07-15 18:39:13 +05:30
Nilesh Deepak
39b924f158 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 18:37:55 +05:30
Nilesh Deepak
a488ef6b00 Test for cover image. 2017-07-15 18:36:49 +05:30
abijeetp
6d66c38c12 Fixes issues with the test case, now creating a user with the required profile setting. 2017-07-15 18:00:39 +05:30
Nilesh Deepak
922964ecf2 Changes grid container size 2017-07-15 17:50:09 +05:30
Nilesh Deepak
0c70416b5c Test books display options. 2017-07-15 16:33:52 +05:30
Nilesh Deepak
770f30c3a8 Test books display options. 2017-07-15 16:29:42 +05:30
Nilesh Deepak
b4044e6c3a Resolves heading issues in grid view 2017-07-15 16:22:29 +05:30
Nilesh Deepak
9872767f20 Test for cover image upload 2017-07-15 16:19:35 +05:30
Nilesh Deepak
dd4d2f4696 Resolves book heading issues in grid view. 2017-07-15 16:15:45 +05:30
Nilesh Deepak
e5dc0e6bb8 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 16:13:48 +05:30
Nilesh Deepak
85fbe820c4 Adding getHeadingExcerpt to get heading. 2017-07-15 16:11:10 +05:30
abijeetp
832f8eaa94 Fixes the test case related to UserProfileTest. 2017-07-15 15:50:42 +05:30
Abijeet
3435dcc91e Merge pull request #10 from OsmosysSoftware/test-issue-181
Tests for issue 181
2017-07-15 14:29:38 +05:30
Nilesh Deepak
1ed74b8598 Test for grid and list layout selection. 2017-07-15 13:19:49 +05:30
Nilesh Deepak
fd36978c13 Test for layout selection. 2017-07-15 12:26:57 +05:30
Nilesh Deepak
1278a0b818 Test for layout selection. 2017-07-15 11:40:51 +05:30
Nilesh Deepak
6a6516ddd5 Test for layout selection. 2017-07-15 11:31:43 +05:30
Nilesh Deepak
1fe8f13503 Cover image test case 2017-07-14 18:36:50 +05:30
Nilesh Deepak
8f3adcda5d Cover image test case 2017-07-14 18:02:45 +05:30
Abijeet
21a8df78ee Merge pull request #9 from OsmosysSoftware/feature-181
Feature 181
2017-07-13 15:50:43 +05:30
Nilesh Deepak
7f8351e044 Removed avatar class from form.blade.php 2017-07-13 15:20:53 +05:30
Nilesh Deepak
afc1ecafe9 4. Changed the border color of the gallery item to #ccc 2017-07-13 12:27:14 +05:30
Nilesh Deepak
ab6ff5fda2 3. New default.png 2017-07-13 12:26:01 +05:30
Nilesh Deepak
b0ba1a43a9 2. Added classed col-xs-6 col-sm-4 col-md-4 col-lg-3 in grid-item.blade.php
5. Added <div class="row"> in index.blade.php
2017-07-13 12:24:47 +05:30
Nilesh Deepak
e919cab3d1 1. Thumbnail size when creating or editing book. 2017-07-13 12:22:43 +05:30
Abijeet
f37509062e Merge pull request #8 from OsmosysSoftware/feature-181
Issue 181
2017-07-12 18:41:35 +05:30
Nilesh Deepak
24ee78ccd8 Update. 2017-07-12 18:04:06 +05:30
Nilesh Deepak
d37b398e79 Updates styles. 2017-07-12 13:52:21 +05:30
Nilesh Deepak
7a724f9134 Updated modifications. 2017-07-12 13:44:37 +05:30
Abijeet
f3b2e0fb91 Merge pull request #7 from OsmosysSoftware/revert-3-revert-1-issue-181
Revert "Revert "Bookstack grid view.""
2017-07-12 11:41:01 +05:30
Abijeet
844976c85b Revert "Revert "Bookstack grid view."" 2017-07-12 11:40:50 +05:30
Abijeet
f0d914abbf Merge pull request #5 from BookStackApp/master
Getting latest changes
2017-07-12 11:33:58 +05:30
Abijeet
0ed3023b42 Merge pull request #3 from OsmosysSoftware/revert-1-issue-181
Revert "Bookstack grid view."
2017-07-07 17:28:47 +05:30
Abijeet
3fd61a3600 Revert "Bookstack grid view." 2017-07-07 17:28:34 +05:30
Nilesh Deepak
a663fc8aa8 Merge pull request #1 from OsmosysSoftware/issue-181
Bookstack grid view issue 181.
2017-07-07 17:08:19 +05:30
Nilesh Deepak
d84315fff8 Indentation correction. 2017-07-07 17:06:08 +05:30
Nilesh Deepak
144a6e469d Updated cover image upload and delete function. 2017-07-07 16:29:38 +05:30
Nilesh Deepak
c5f11e4516 Fixed pagination on change of display type. 2017-07-06 10:05:11 +05:30
Nilesh Deepak
16a09e8ff6 Deletion of image file on book deletion. 2017-07-06 10:03:40 +05:30
Nilesh Deepak
f51db4b9f6 Resolved responsiveness issues 2017-07-05 19:58:52 +05:30
Nilesh Deepak
6ad24a6bee Changed public getImageURL function to private. 2017-07-05 18:32:38 +05:30
Nilesh Deepak
5b736c3b36 Updated views to support different languages. 2017-07-05 16:12:29 +05:30
Nilesh Deepak
cc553cc93d Added labels for 'Thumbnail toggle' and 'Cover image' in different languages. 2017-07-05 16:11:15 +05:30
Nilesh Deepak
e88a06291e Updated toggle thumbnails function. 2017-07-05 16:09:20 +05:30
Nilesh Deepak
6eccb3d5b9 Adding new migration. 2017-07-05 16:08:04 +05:30
Nilesh Deepak
026de8c5ca Thumbnail toggle function. 2017-07-05 12:48:41 +05:30
Nilesh Deepak
e10d4b91cf styles.scss 2017-07-05 12:36:26 +05:30
Nilesh Deepak
d089eaf754 Changes in User edit profile page. 2017-07-05 12:32:39 +05:30
Nilesh Deepak
bb2d85965f Removed duplicated styles. 2017-07-05 12:29:16 +05:30
Nilesh Deepak
d99fd1fd65 Applied required changes 2017-07-05 12:26:02 +05:30
Nilesh Deepak
947c58f227 Applied required changes in BookStack. 2017-07-05 12:09:01 +05:30
Nilesh Deepak
bce5fdd5cd Merge branch 'master' into issue-181 2017-07-04 15:16:46 +05:30
Nilesh Deepak
fdf139edb2 Changing column size for responsiveness 2017-07-04 15:04:57 +05:30
Nilesh Deepak
af72f0d490 Bookstack grid view. 2017-06-29 18:54:04 +05:30
Nilesh Deepak
8924618d12 test 2017-06-28 18:56:17 +05:30
Nilesh Deepak
6557fbb666 commit 2017-06-28 18:51:32 +05:30
531 changed files with 33379 additions and 7284 deletions

2
.browserslistrc Normal file
View File

@@ -0,0 +1,2 @@
>0.25%
not op_mini all

View File

@@ -20,6 +20,8 @@ SESSION_DRIVER=file
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# A different prefix is useful when multiple BookStack instances use the same caching server
CACHE_PREFIX=bookstack
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
@@ -46,8 +48,18 @@ GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# External services such as Gravatar
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
@@ -57,6 +69,13 @@ LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Mail settings
MAIL_DRIVER=smtp
@@ -64,4 +83,6 @@ MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_ENCRYPTION=null
MAIL_FROM=null
MAIL_FROM_NAME=null

84
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,84 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Project Maintainer Standards
Project maintainers should generally follow these additional standards:
* Avoid using a negative or harsh tone in communication, Even if the other party
is being negative themselves.
* When providing criticism, try to make it constructive to lead the other person
down the correct path.
* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)
in mind when deciding what's in scope of the Project.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior. In addition, Project
maintainers are responsible for following the standards themselves.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

View File

@@ -1,13 +0,0 @@
### For Feature Requests
Desired Feature:
### For Bug Reports
* BookStack Version:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Actual Behavior

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Your Configuration (please complete the following information):**
- Exact BookStack Version (Found in settings):
- PHP Version:
- Hosting Method (Nginx/Apache/Docker):
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Describe the feature you'd like**
A clear description of the feature you'd like implemented in BookStack.
**Describe the benefits this feature would bring to BookStack users**
Explain the measurable benefits this feature would achieve.
**Additional context**
Add any other context or screenshots about the feature request here.

8
.gitignore vendored
View File

@@ -2,8 +2,10 @@
/node_modules
Homestead.yaml
.env
/public/dist
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/plugins
/public/css/*.map
/public/js/*.map
@@ -18,5 +20,5 @@ yarn.lock
nbproject
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs
.settings/
webpack-stats.json

View File

@@ -2,7 +2,8 @@ dist: trusty
sudo: false
language: php
php:
- 7.0.7
- 7.0.20
- 7.1.9
cache:
directories:
@@ -14,7 +15,6 @@ before_script:
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- mysql -u root -e "FLUSH PRIVILEGES;"
- phpenv config-rm xdebug.ini
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- php artisan clear-compiled -n
- php artisan optimize -n

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 Dan Brown
Copyright (c) 2018 Dan Brown and the BookStack Project contributors
https://github.com/BookStackApp/BookStack/graphs/contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -16,7 +16,9 @@ class Activity extends Model
*/
public function entity()
{
if ($this->entity_type === '') $this->entity_type = null;
if ($this->entity_type === '') {
$this->entity_type = null;
}
return $this->morphTo('entity');
}
@@ -43,8 +45,8 @@ class Activity extends Model
* @param $activityB
* @return bool
*/
public function isSimilarTo($activityB) {
public function isSimilarTo($activityB)
{
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class Attachment extends Ownable
{
protected $fillable = ['name', 'order'];
@@ -11,7 +10,9 @@ class Attachment extends Ownable
*/
public function getFileName()
{
if (str_contains($this->name, '.')) return $this->name;
if (str_contains($this->name, '.')) {
return $this->name;
}
return $this->name . '.' . $this->extension;
}
@@ -30,7 +31,9 @@ class Attachment extends Ownable
*/
public function getUrl()
{
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
return baseUrl('/attachments/' . $this->id);
}
}

View File

@@ -2,8 +2,9 @@
class Book extends Entity
{
public $searchFactor = 2;
protected $fillable = ['name', 'description'];
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the url for this book.
@@ -18,6 +19,35 @@ class Book extends Entity
return baseUrl('/books/' . urlencode($this->slug));
}
/**
* Returns book cover image, if book cover not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/*
* Get the edit url for this book.
* @return string
@@ -64,5 +94,4 @@ class Book extends Entity
{
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -1,11 +1,10 @@
<?php namespace BookStack;
class Chapter extends Entity
{
protected $fillable = ['name', 'description', 'priority', 'book_id'];
public $searchFactor = 1.3;
protected $with = ['book'];
protected $fillable = ['name', 'description', 'priority', 'book_id'];
/**
* Get the book this chapter is within.
@@ -59,5 +58,4 @@ class Chapter extends Entity
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\ImageService;
use Illuminate\Console\Command;
use Symfony\Component\Console\Output\OutputInterface;
class CleanupImages extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:cleanup-images
{--a|all : Include images that are used in page revisions}
{--f|force : Actually run the deletions}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cleanup images and drawings';
protected $imageService;
/**
* Create a new command instance.
* @param ImageService $imageService
*/
public function __construct(ImageService $imageService)
{
$this->imageService = $imageService;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$checkRevisions = $this->option('all') ? false : true;
$dryRun = $this->option('force') ? false : true;
if (!$dryRun) {
$proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
if (!$proceed) {
return;
}
}
$deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($deleted);
if ($dryRun) {
$this->comment('Dry run, No images have been deleted');
$this->comment($deleteCount . ' images found that would have been deleted');
$this->showDeletedImages($deleted);
$this->comment('Run with -f or --force to perform deletions');
return;
}
$this->showDeletedImages($deleted);
$this->comment($deleteCount . ' images deleted');
}
protected function showDeletedImages($paths)
{
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return;
if (count($paths) > 0) {
$this->line('Images to delete:');
}
foreach ($paths as $path) {
$this->line($path);
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Repos\UserRepo;
use Illuminate\Console\Command;
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:create-admin
{--email= : The email address for the new admin user}
{--name= : The name of the new admin user}
{--password= : The password to assign to the new admin user}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new admin user to the system';
protected $userRepo;
/**
* Create a new command instance.
*
* @param UserRepo $userRepo
*/
public function __construct(UserRepo $userRepo)
{
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function handle()
{
$email = trim($this->option('email'));
if (empty($email)) {
$email = $this->ask('Please specify an email address for the new admin user');
}
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Invalid email address provided');
}
if ($this->userRepo->getByEmail($email) !== null) {
return $this->error('A user with the provided email already exists!');
}
$name = trim($this->option('name'));
if (empty($name)) {
$name = $this->ask('Please specify an name for the new admin user');
}
if (strlen($name) < 2) {
return $this->error('Invalid name provided');
}
$password = trim($this->option('password'));
if (empty($password)) {
$password = $this->secret('Please specify a password for the new admin user');
}
if (strlen($password) < 5) {
return $this->error('Invalid password provided, Must be at least 5 characters');
}
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
$this->userRepo->attachSystemRole($user, 'admin');
$this->userRepo->downloadGravatarToUserAvatar($user);
$user->email_confirmed = true;
$user->save();
$this->info("Admin account with email \"{$user->email}\" successfully created!");
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\User;
use BookStack\Repos\UserRepo;
use Illuminate\Console\Command;
class DeleteUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:delete-users';
protected $user;
protected $userRepo;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete users that are not "admin" or system users.';
public function __construct(User $user, UserRepo $userRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
parent::__construct();
}
public function handle()
{
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
$numDeleted = 0;
if (strtolower(trim($confirm)) === 'yes') {
$totalUsers = $this->user->count();
$users = $this->user->where('system_name', '=', null)->with('roles')->get();
foreach ($users as $user) {
if ($user->hasSystemRole('admin')) {
// don't delete users with "admin" role
continue;
}
$this->userRepo->destroy($user);
++$numDeleted;
}
$this->info("Deleted $numDeleted of $totalUsers total users.");
} else {
$this->info('Exiting...');
}
}
}

View File

@@ -19,7 +19,7 @@ class RegenerateSearch extends Command
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Re-index all content for searching';
protected $searchService;

View File

@@ -11,12 +11,7 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
Commands\ClearViews::class,
Commands\ClearActivity::class,
Commands\ClearRevisions::class,
Commands\RegeneratePermissions::class,
Commands\RegenerateSearch::class,
Commands\UpgradeDatabaseEncoding::class
//
];
/**
@@ -29,4 +24,14 @@ class Kernel extends ConsoleKernel
{
//
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
}
}

View File

@@ -1,13 +1,20 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable
{
/**
* @var string - Name of property where the main text content is found
*/
public $textField = 'description';
/**
* @var float - Multiplier for search indexing.
*/
public $searchFactor = 1.0;
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
@@ -28,7 +35,9 @@ class Entity extends Ownable
{
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
if ($matches) return true;
if ($matches) {
return true;
}
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
return $entity->book_id === $this->id;
@@ -159,7 +168,9 @@ class Entity extends Ownable
*/
public function getShortName($length = 25)
{
if (strlen($this->name) <= $length) return $this->name;
if (strlen($this->name) <= $length) {
return $this->name;
}
return substr($this->name, 0, $length - 3) . '...';
}
@@ -176,13 +187,18 @@ class Entity extends Ownable
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery(){return '';}
public function entityRawQuery()
{
return '';
}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path){return '/';}
public function getUrl($path = '/')
{
return $path;
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class EntityPermission extends Model
{

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class AuthException extends PrettyException
{
class AuthException extends PrettyException {}
}

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
class ConfirmationEmailException extends NotifyException {}
}

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class FileUploadException extends PrettyException
{
class FileUploadException extends PrettyException {}
}

View File

@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler
{
@@ -26,10 +27,11 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @return mixed
* @throws Exception
*/
public function report(Exception $e)
{
@@ -60,6 +62,11 @@ class Handler extends ExceptionHandler
return response()->view('errors/' . $code, ['message' => $message], $code);
}
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
return \Route::respondWithRoute('fallback');
}
return parent::render($request, $e);
}
@@ -69,9 +76,12 @@ class Handler extends ExceptionHandler
* @param $type
* @return bool
*/
protected function isExceptionType(Exception $e, $type) {
protected function isExceptionType(Exception $e, $type)
{
do {
if (is_a($e, $type)) return true;
if (is_a($e, $type)) {
return true;
}
} while ($e = $e->getPrevious());
return false;
}
@@ -81,7 +91,8 @@ class Handler extends ExceptionHandler
* @param Exception $e
* @return string
*/
protected function getOriginalMessage(Exception $e) {
protected function getOriginalMessage(Exception $e)
{
do {
$message = $e->getMessage();
} while ($e = $e->getPrevious());
@@ -103,4 +114,16 @@ class Handler extends ExceptionHandler
return redirect()->guest('login');
}
/**
* Convert a validation exception into a JSON response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Validation\ValidationException $exception
* @return \Illuminate\Http\JsonResponse
*/
protected function invalidJson($request, ValidationException $exception)
{
return response()->json($exception->errors(), $exception->status);
}
}

View File

@@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class ImageUploadException extends PrettyException {}
class ImageUploadException extends PrettyException
{
}

View File

@@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class LdapException extends PrettyException {}
class LdapException extends PrettyException
{
}

View File

@@ -1,7 +1,7 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
class NotFoundException extends PrettyException
{
/**
* NotFoundException constructor.
@@ -11,4 +11,4 @@ class NotFoundException extends PrettyException {
{
parent::__construct($message, 404);
}
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Exceptions;
class NotifyException extends \Exception
{
@@ -18,4 +17,4 @@ class NotifyException extends \Exception
$this->redirectLocation = $redirectLocation;
parent::__construct();
}
}
}

View File

@@ -1,6 +1,8 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PermissionsException extends Exception {}
class PermissionsException extends Exception
{
}

View File

@@ -1,3 +1,6 @@
<?php namespace BookStack\Exceptions;
class PrettyException extends \Exception {}
class PrettyException extends \Exception
{
}

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends PrettyException
{
class SocialDriverNotConfigured extends PrettyException {}
}

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
class SocialSignInException extends NotifyException {}
}

View File

@@ -1,4 +1,6 @@
<?php namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
class UserRegistrationException extends NotifyException {}
}

View File

@@ -2,6 +2,7 @@
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Services\AttachmentService;
use Illuminate\Http\Request;
@@ -102,7 +103,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'url|min:1|max:255'
'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
@@ -130,7 +131,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|url|min:1|max:255'
'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
@@ -182,11 +183,17 @@ class AttachmentController extends Controller
* Get an attachment from storage.
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
* @throws NotFoundException
*/
public function get($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
if ($page === null) {
throw new NotFoundException(trans('errors.attachment_not_found'));
}
$this->checkOwnablePermission('page-view', $page);
if ($attachment->external) {
@@ -204,6 +211,7 @@ class AttachmentController extends Controller
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
* @throws \Exception
*/
public function delete($attachmentId)
{

View File

@@ -64,5 +64,4 @@ class ForgotPasswordController extends Controller
['email' => trans($response)]
);
}
}
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\LdapService;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
@@ -36,18 +37,21 @@ class LoginController extends Controller
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $ldapService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param LdapService $ldapService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->ldapService = $ldapService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
@@ -66,24 +70,26 @@ class LoginController extends Controller
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
* @throws \BookStack\Exceptions\LdapException
*/
protected function authenticated(Request $request, Authenticatable $user)
{
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists) {
auth()->logout($user);
}
if (!$user->exists && $user->email === null && !$request->has('email')) {
if (!$user->exists && $user->email === null && !$request->filled('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
if (!$user->exists && $user->email === null && $request->filled('email')) {
$user->email = $request->get('email');
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
@@ -95,6 +101,11 @@ class LoginController extends Controller
auth()->login($user);
}
// Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user, $request->get($this->username()));
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
@@ -102,12 +113,21 @@ class LoginController extends Controller
/**
* Show the application login form.
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function getLogin()
public function getLogin(Request $request)
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
if ($request->has('email')) {
session()->flashInput([
'email' => $request->get('email'),
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
]);
}
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
@@ -115,10 +135,11 @@ class LoginController extends Controller
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}
}

View File

@@ -53,7 +53,7 @@ class RegisterController extends Controller
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
@@ -91,6 +91,7 @@ class RegisterController extends Controller
/**
* Show the application registration form.
* @return Response
* @throws UserRegistrationException
*/
public function getRegister()
{
@@ -102,20 +103,13 @@ class RegisterController extends Controller
/**
* Handle a registration request for the application.
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \Illuminate\Validation\ValidationException
*/
public function postRegister(Request $request)
{
$this->checkRegistrationAllowed();
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->validator($request->all())->validate();
$userData = $request->all();
return $this->registerUser($userData);
@@ -141,7 +135,6 @@ class RegisterController extends Controller
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws ConfirmationEmailException
*/
protected function registerUser(array $userData, $socialAccount = false)
{
@@ -239,6 +232,8 @@ class RegisterController extends Controller
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function socialRegister($socialDriver)
{
@@ -250,18 +245,34 @@ class RegisterController extends Controller
/**
* The callback for social login services.
* @param $socialDriver
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws ConfirmationEmailException
*/
public function socialCallback($socialDriver)
public function socialCallback($socialDriver, Request $request)
{
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
// Check request for error information
if ($request->has('error') && $request->has('error_description')) {
throw new SocialSignInException(trans('errors.social_login_bad_response', [
'socialAccount' => $socialDriver,
'error' => $request->get('error_description'),
]), '/login');
}
$action = session()->pull('social-callback');
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
}
if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
}
return redirect()->back();
}
@@ -280,6 +291,7 @@ class RegisterController extends Controller
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
protected function socialRegisterCallback($socialDriver)
{
@@ -294,5 +306,4 @@ class RegisterController extends Controller
];
return $this->registerUser($userData, $socialAccount);
}
}
}

View File

@@ -46,4 +46,4 @@ class ResetPasswordController extends Controller
return redirect($this->redirectPath())
->with('status', trans($response));
}
}
}

View File

@@ -36,16 +36,18 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 20);
$books = $this->entityRepo->getAllPaginated('book', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->setPageTitle('Books');
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new
'new' => $new,
'booksViewType' => $booksViewType
]);
}
@@ -107,7 +109,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
}
@@ -125,9 +127,9 @@ class BookController extends Controller
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
/**
@@ -153,7 +155,7 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false);
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
@@ -183,47 +185,61 @@ class BookController extends Controller
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->has('sort-tree')) {
if (!$request->filled('sort-tree')) {
return redirect($book->getUrl());
}
// Sort pages and chapters
$sortedBooks = [];
$updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id;
$sortMap = collect(json_decode($request->get('sort-tree')));
$bookIdsInvolved = collect([$book->id]);
// Loop through contents of provided map and update entities accordingly
foreach ($sortMap as $bookChild) {
$priority = $bookChild->sort;
$id = intval($bookChild->id);
$isPage = $bookChild->type == 'page';
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
// Load models into map
$sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
$mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
$mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
// Store source and target books
$bookIdsInvolved->push(intval($mapItem->model->book_id));
$bookIdsInvolved->push(intval($mapItem->book));
});
// Update models only if there's a change in parent chain or ordering.
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
$model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId;
// Get the books involved in the sort
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
// Throw permission error if invalid ids or inaccessible books given.
if (count($bookIdsInvolved) !== count($booksInvolved)) {
$this->showPermissionError();
}
// Check permissions of involved books
$booksInvolved->each(function (Book $book) {
$this->checkOwnablePermission('book-update', $book);
});
// Perform the sort
$sortMap->each(function ($mapItem) {
$model = $mapItem->model;
$priorityChanged = intval($model->priority) !== intval($mapItem->sort);
$bookChanged = intval($model->book_id) !== intval($mapItem->book);
$chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
if ($bookChanged) {
$this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
}
if ($chapterChanged) {
$model->chapter_id = intval($mapItem->parentChapter);
$model->save();
$updatedModels->push($model);
}
// Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId;
if ($priorityChanged) {
$model->priority = intval($mapItem->sort);
$model->save();
}
}
});
// Add activity for books
foreach ($sortedBooks as $bookId) {
/** @var Book $updatedBook */
$updatedBook = $this->entityRepo->getById('book', $bookId);
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) {
$this->entityRepo->buildJointPermissionsForBook($book);
Activity::add($book, 'book_sort', $book->id);
});
return redirect($book->getUrl());
}

View File

@@ -107,17 +107,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
if ($chapter->name !== $request->get('name')) {
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
}
$chapter->fill($request->all());
$chapter->updated_by = user()->id;
$chapter->save();
$this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
@@ -159,7 +156,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
public function showMove($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
@@ -177,7 +175,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
public function move($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);

View File

@@ -51,7 +51,9 @@ abstract class Controller extends BaseController
*/
protected function preventAccessForDemoUsers()
{
if (config('app.env') === 'demo') $this->showPermissionError();
if (config('app.env') === 'demo') {
$this->showPermissionError();
}
}
/**
@@ -100,7 +102,9 @@ abstract class Controller extends BaseController
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
if (userCan($permission, $ownable)) {
return true;
}
return $this->showPermissionError();
}
@@ -113,7 +117,9 @@ abstract class Controller extends BaseController
protected function checkPermissionOr($permissionName, $callback)
{
$callbackResult = $callback();
if ($callbackResult === false) $this->checkPermission($permissionName);
if ($callbackResult === false) {
$this->checkPermission($permissionName);
}
return true;
}
@@ -145,5 +151,4 @@ abstract class Controller extends BaseController
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
}

View File

@@ -2,6 +2,7 @@
use Activity;
use BookStack\Repos\EntityRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -32,30 +33,51 @@ class HomeController extends Controller
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
// Custom homepage
$customHomepage = false;
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
$books = false;
$booksViewType = false;
// Check book homepage
$bookHomepageSetting = setting('app-book-homepage');
if ($bookHomepageSetting) {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
} else {
// Check custom homepage
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
}
}
$view = $customHomepage ? 'home-custom' : 'home';
return view($view, [
$view = 'home';
if ($bookHomepageSetting) {
$view = 'home-book';
} else if ($customHomepage) {
$view = 'home-custom';
}
return view('common/' . $view, [
'activity' => $activity,
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'customHomepage' => $customHomepage
'customHomepage' => $customHomepage,
'books' => $books,
'booksViewType' => $booksViewType
]);
}
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function getTranslations() {
public function getTranslations()
{
$locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
@@ -86,4 +108,36 @@ class HomeController extends Controller
]);
}
/**
* Get custom head HTML, Used in ajax calls to show in editor.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function customHeadContent()
{
return view('partials/custom-head-content');
}
/**
* Show the view for /robots.txt
* @return $this
*/
public function getRobots()
{
$sitePublic = setting('app-public', false);
$allowRobots = config('app.allow_robots');
if ($allowRobots === null) {
$allowRobots = $sitePublic;
}
return response()
->view('common/robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
/**
* Show the route for 404 responses.
*/
public function getNotFound()
{
return response()->view('errors/404', [], 404);
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
@@ -28,6 +29,21 @@ class ImageController extends Controller
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Get all images for a specific type, Paginated
* @param string $type
@@ -47,14 +63,14 @@ class ImageController extends Controller
* @param Request $request
* @return mixed
*/
public function searchByType($type, $page = 0, Request $request)
public function searchByType(Request $request, $type, $page = 0)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
return response()->json($imgData);
}
@@ -76,17 +92,19 @@ class ImageController extends Controller
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getGalleryFiltered($filter, $page = 0, Request $request)
public function getGalleryFiltered(Request $request, $filter, $page = 0)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
if (!$validFilters->contains($filter)) {
return response('Invalid filter', 500);
}
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
return response()->json($imgData);
}
@@ -96,6 +114,7 @@ class ImageController extends Controller
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function uploadByType($type, Request $request)
{
@@ -104,18 +123,64 @@ class ImageController extends Controller
'file' => 'is_image'
]);
if (!$this->imageRepo->isValidType($type)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Upload a drawing to the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawing(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
@@ -123,6 +188,8 @@ class ImageController extends Controller
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
@@ -137,6 +204,8 @@ class ImageController extends Controller
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($imageId, Request $request)
{
@@ -150,29 +219,30 @@ class ImageController extends Controller
}
/**
* Deletes an image and all thumbnail/image files
* Show the usage of an image on pages.
* @param EntityRepo $entityRepo
* @param Request $request
* @param int $id
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(EntityRepo $entityRepo, Request $request, $id)
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
if (!$isForced) {
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {
return response()->json($pageSearch, 400);
}
}
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@@ -5,7 +5,6 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -38,11 +37,18 @@ class PageController extends Controller
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
* @throws NotFoundException
*/
public function create($bookSlug, $chapterSlug = null)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -52,7 +58,7 @@ class PageController extends Controller
return redirect($draft->getUrl());
}
// Otherwise show edit view
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
@@ -71,8 +77,14 @@ class PageController extends Controller
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -93,7 +105,7 @@ class PageController extends Controller
public function editDraft($bookSlug, $pageId)
{
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->checkOwnablePermission('page-create', $draft->parent);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
@@ -119,12 +131,10 @@ class PageController extends Controller
]);
$input = $request->all();
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$book = $draftPage->book;
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$parent = $draftPage->parent;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
@@ -145,6 +155,7 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @throws NotFoundException
*/
public function show($bookSlug, $pageSlug)
{
@@ -152,7 +163,9 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) {
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
if ($page === null) {
throw $e;
}
return redirect($page->getUrl());
}
@@ -161,14 +174,22 @@ class PageController extends Controller
$page->html = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($page->html);
$page->load(['comments.createdBy']);
// check if the comment's are enabled
$commentsEnabled = !setting('app-disable-comments');
if ($commentsEnabled) {
$page->load(['comments.createdBy']);
}
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav]);
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
]);
}
/**
@@ -211,7 +232,9 @@ class PageController extends Controller
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
if (count($warnings) > 0) {
session()->flash('warning', implode("\n", $warnings));
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
@@ -262,11 +285,10 @@ class PageController extends Controller
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $utcUpdateTimestamp
'timestamp' => $updateTime
]);
}
@@ -324,9 +346,10 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$book = $page->book;
$this->checkOwnablePermission('page-delete', $page);
$this->entityRepo->destroyPage($page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', trans('entities.pages_delete_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
@@ -572,6 +595,8 @@ class PageController extends Controller
return redirect()->back();
}
$this->checkOwnablePermission('page-create', $parent);
$this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
@@ -579,12 +604,70 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
* @param string $bookSlug
* @param string $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
'book' => $page->book,
'page' => $page
]);
}
/**
* Create a copy of a page within the requested target destination.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
$parent = $page->chapter ? $page->chapter : $page->book;
} else {
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
try {
$parent = $this->entityRepo->getById($entityType, $entityId);
} catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back();
}
}
$this->checkOwnablePermission('page-create', $parent);
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
session()->flash('success', trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
/**
* Set the permissions for this page.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
@@ -594,5 +677,4 @@ class PageController extends Controller
session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}

View File

@@ -67,7 +67,9 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
return view('settings/roles/edit', ['role' => $role]);
}
@@ -76,6 +78,7 @@ class PermissionController extends Controller
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
*/
public function updateRole($id, Request $request)
{

View File

@@ -36,17 +36,16 @@ class SearchController extends Controller
$searchTerm = $request->get('term');
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
return view('search/all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'hasNextPage' => $hasNextPage,
'hasNextPage' => $results['has_more'],
'nextPageLink' => $nextPageLink
]);
}
@@ -88,23 +87,21 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm)['results'];
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames);
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Services\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Setting;
@@ -13,7 +14,7 @@ class SettingController extends Controller
public function index()
{
$this->checkPermission('settings-manage');
$this->setPageTitle('Settings');
$this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
@@ -33,7 +34,9 @@ class SettingController extends Controller
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
if (strpos($name, 'setting-') !== 0) continue;
if (strpos($name, 'setting-') !== 0) {
continue;
}
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
}
@@ -42,4 +45,47 @@ class SettingController extends Controller
return redirect('/settings');
}
/**
* Show the page for application maintenance.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showMaintenance()
{
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/maintenance', ['version' => $version]);
}
/**
* Action to clean-up images in the system.
* @param Request $request
* @param ImageService $imageService
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function cleanupImages(Request $request, ImageService $imageService)
{
$this->checkPermission('settings-manage');
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
$dryRun = !($request->has('confirm'));
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
}

View File

@@ -37,7 +37,7 @@ class TagController extends Controller
*/
public function getNameSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$searchTerm = $request->get('search', false);
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
return response()->json($suggestions);
}
@@ -49,10 +49,9 @@ class TagController extends Controller
*/
public function getValueSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$tagName = $request->has('name') ? $request->get('name') : false;
$searchTerm = $request->get('search', false);
$tagName = $request->get('name', false);
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}
}

View File

@@ -34,9 +34,9 @@ class UserController extends Controller
{
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->has('order') ? $request->get('order') : 'asc',
'search' => $request->has('search') ? $request->get('search') : '',
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
'order' => $request->get('order', 'asc'),
'search' => $request->get('search', ''),
'sort' => $request->get('sort', 'name'),
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
@@ -88,22 +88,12 @@ class UserController extends Controller
$user->save();
if ($request->has('roles')) {
if ($request->filled('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
}
}
$this->userRepo->downloadGravatarToUserAvatar($user);
return redirect('/settings/users');
}
@@ -155,24 +145,24 @@ class UserController extends Controller
$user->fill($request->all());
// Role updates
if (userCan('users-manage') && $request->has('roles')) {
if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Password updates
if ($request->has('password') && $request->get('password') != '') {
if ($request->filled('password')) {
$password = $request->get('password');
$user->password = bcrypt($password);
}
// External auth id updates
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
if ($request->has('setting')) {
if ($request->filled('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}
@@ -249,4 +239,27 @@ class UserController extends Controller
'assetCounts' => $assetCounts
]);
}
/**
* Update the user's preferred book-list display setting.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function switchBookView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('book_view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'books_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
}
}

View File

@@ -13,8 +13,9 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\BookStack\Http\Middleware\TrimStrings::class,
\BookStack\Http\Middleware\TrustProxies::class,
];
/**
@@ -26,6 +27,8 @@ class Kernel extends HttpKernel
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
@@ -42,10 +45,11 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth' => \BookStack\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
];
}

View File

@@ -30,8 +30,11 @@ class Authenticate
*/
public function handle($request, Closure $next)
{
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect(baseUrl('/register/confirm/awaiting'));
if ($this->auth->check()) {
$requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict'));
if ($requireConfirmation && !$this->auth->user()->email_confirmed) {
return redirect('/register/confirm/awaiting');
}
}
if ($this->auth->guest() && !setting('app-public')) {

View File

@@ -2,9 +2,9 @@
namespace BookStack\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends BaseEncrypter
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.

View File

@@ -2,6 +2,7 @@
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
class Localization
{
@@ -15,19 +16,33 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
if (user()->isDefault()) {
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) continue;
$locale = $lang;
break;
}
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
/**
* Autodetect the visitors locale by matching locales in their headers
* against the locales supported by BookStack.
* @param Request $request
* @param string $default
* @return string
*/
protected function autoDetectLocale(Request $request, string $default)
{
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (in_array($lang, $availableLocales)) {
return $lang;
}
}
return $default;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace BookStack\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
'password-confirm',
];
}

View File

@@ -0,0 +1,47 @@
<?php
namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array
*/
protected $proxies;
/**
* The current proxy header mappings.
*
* @var array
*/
protected $headers = [
Request::HEADER_FORWARDED => 'FORWARDED',
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
];
/**
* Handle the request, Set the correct user-configured proxy information.
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$setProxies = config('app.proxies');
if ($setProxies !== '**' && $setProxies !== '*' && $setProxies !== '') {
$setProxies = explode(',', $setProxies);
}
$this->proxies = $setProxies;
return parent::handle($request, $next);
}
}

View File

@@ -2,9 +2,9 @@
namespace BookStack\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends BaseVerifier
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.

View File

@@ -9,13 +9,15 @@ class Image extends Ownable
/**
* Get a thumbnail for this image.
* @param int $width
* @param int $height
* @param int $width
* @param int $height
* @param bool|false $keepRatio
* @return string
* @throws \Exception
*/
public function getThumb($width, $height, $keepRatio = false)
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
}

View File

@@ -15,5 +15,4 @@ class Model extends EloquentModel
{
return parent::getAttributeFromArray($key);
}
}
}

View File

@@ -49,5 +49,4 @@ class ConfirmEmail extends Notification implements ShouldQueue
->line(trans('auth.email_confirm_text'))
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
abstract class Ownable extends Model
{
/**
@@ -29,5 +28,4 @@ abstract class Ownable extends Model
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
}
}

View File

@@ -1,13 +1,11 @@
<?php namespace BookStack;
class Page extends Entity
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
public $textField = 'text';
/**
@@ -30,6 +28,15 @@ class Page extends Entity
return $this->belongsTo(Book::class);
}
/**
* Get the parent item
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function parent()
{
return $this->chapter_id ? $this->chapter() : $this->book();
}
/**
* Get the chapter that this page is in, If applicable.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@@ -101,8 +108,8 @@ class Page extends Entity
* @return string
*/
public function entityRawQuery($withContent = false)
{ $htmlQuery = $withContent ? 'html' : "'' as html";
{
$htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@@ -31,7 +30,9 @@ class PageRevision extends Model
public function getUrl($path = null)
{
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) return $url . '/' . trim($path, '/');
if ($path) {
return $url . '/' . trim($path, '/');
}
return $url;
}
@@ -58,5 +59,4 @@ class PageRevision extends Model
{
return $type === 'revision';
}
}

View File

@@ -15,12 +15,12 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
\Blade::directive('icon', function($expression) {
\Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";
});
@@ -35,7 +35,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
$this->app->singleton(SettingService::class, function($app) {
$this->app->singleton(SettingService::class, function ($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}

View File

@@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider
*/
public function register()
{
Auth::provider('ldap', function($app, array $config) {
Auth::provider('ldap', function ($app, array $config) {
return new LdapUserProvider($config['model'], $app[LdapService::class]);
});
}

View File

@@ -3,6 +3,7 @@
namespace BookStack\Providers;
use BookStack\Activity;
use BookStack\Image;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use BookStack\Services\ViewService;
@@ -34,29 +35,30 @@ class CustomFacadeProvider extends ServiceProvider
*/
public function register()
{
$this->app->bind('activity', function() {
$this->app->bind('activity', function () {
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('views', function() {
$this->app->bind('views', function () {
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('setting', function() {
$this->app->bind('setting', function () {
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
});
$this->app->bind('images', function() {
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class)

View File

@@ -16,6 +16,11 @@ class EventServiceProvider extends ServiceProvider
protected $listen = [
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
],
];

View File

@@ -2,7 +2,6 @@
namespace BookStack\Providers;
use BookStack\Role;
use BookStack\Services\LdapService;
use BookStack\User;
@@ -102,7 +101,9 @@ class LdapUserProvider implements UserProvider
{
// Get user via LDAP
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
if ($userDetails === null) return null;
if ($userDetails === null) {
return null;
}
// Search current user base by looking up a uid
$model = $this->createModel();
@@ -110,7 +111,9 @@ class LdapUserProvider implements UserProvider
->where('external_auth_id', $userDetails['uid'])
->first();
if ($currentUser !== null) return $currentUser;
if ($currentUser !== null) {
return $currentUser;
}
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
use Illuminate\Pagination\Paginator;
@@ -32,4 +31,4 @@ class PaginationServiceProvider extends IlluminatePaginationServiceProvider
return 1;
});
}
}
}

View File

@@ -7,7 +7,8 @@ use BookStack\Entity;
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo {
class CommentRepo
{
/**
* @var Comment $comment
@@ -39,7 +40,7 @@ class CommentRepo {
* @param array $data
* @return Comment
*/
public function create (Entity $entity, $data = [])
public function create(Entity $entity, $data = [])
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
@@ -81,7 +82,9 @@ class CommentRepo {
protected function getNextLocalId(Entity $entity)
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) return 1;
if ($comments === null) {
return 1;
}
return $comments->local_id + 1;
}
}
}

View File

@@ -4,6 +4,7 @@ use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Page;
use BookStack\PageRevision;
use BookStack\Services\AttachmentService;
@@ -76,11 +77,15 @@ class EntityRepo
* @param SearchService $searchService
*/
public function __construct(
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
ViewService $viewService, PermissionService $permissionService,
TagRepo $tagRepo, SearchService $searchService
)
{
Book $book,
Chapter $chapter,
Page $page,
PageRevision $pageRevision,
ViewService $viewService,
PermissionService $permissionService,
TagRepo $tagRepo,
SearchService $searchService
) {
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
@@ -112,9 +117,9 @@ class EntityRepo
* @param bool $allowDrafts
* @return \Illuminate\Database\Query\Builder
*/
protected function entityQuery($type, $allowDrafts = false)
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
{
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission);
if (strtolower($type) === 'page' && !$allowDrafts) {
$q = $q->where('draft', '=', false);
}
@@ -162,14 +167,16 @@ class EntityRepo
$q = $this->entityQuery($type)->where('slug', '=', $slug);
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
$q = $q->where('book_id', '=', function($query) use ($bookSlug) {
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
$query->select('id')
->from($this->book->getTable())
->where('slug', '=', $bookSlug)->limit(1);
});
}
$entity = $q->first();
if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
if ($entity === null) {
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
}
return $entity;
}
@@ -195,15 +202,18 @@ class EntityRepo
}
/**
* Get all entities of a type limited by count unless count if false.
* Get all entities of a type with the given permission, limited by count unless count is false.
* @param string $type
* @param integer|bool $count
* @param string $permission
* @return Collection
*/
public function getAll($type, $count = 20)
public function getAll($type, $count = 20, $permission = 'view')
{
$q = $this->entityQuery($type)->orderBy('name', 'asc');
if ($count !== false) $q = $q->take($count);
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
if ($count !== false) {
$q = $q->take($count);
}
return $q->get();
}
@@ -230,7 +240,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('created_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@@ -249,7 +261,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('updated_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@@ -346,12 +360,16 @@ class EntityRepo
$parents[$key] = $entities[$index];
$parents[$key]->setAttribute('pages', collect());
}
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
$tree[] = $entities[$index];
}
$entities[$index]->book = $book;
}
foreach ($entities as $entity) {
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
continue;
}
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
if (!isset($parents[$parentKey])) {
$tree[] = $entity;
@@ -430,7 +448,9 @@ class EntityRepo
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
$query = $query->where('book_id', '=', $bookId);
}
if ($currentId) $query = $query->where('id', '!=', $currentId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
@@ -441,9 +461,10 @@ class EntityRepo
*/
public function updateEntityPermissionsFromRequest($request, Entity $entity)
{
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
$entity->restricted = $request->get('restricted', '') === 'true';
$entity->permissions()->delete();
if ($request->has('restrictions')) {
if ($request->filled('restrictions')) {
foreach ($request->get('restrictions') as $roleId => $restrictions) {
foreach ($restrictions as $action => $value) {
$entity->permissions()->create([
@@ -453,6 +474,7 @@ class EntityRepo
}
}
}
$entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
}
@@ -470,14 +492,19 @@ class EntityRepo
public function createFromInput($type, $input = [], $book = false)
{
$isChapter = strtolower($type) === 'chapter';
$entity = $this->getEntity($type)->newInstance($input);
$entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
$entity->created_by = user()->id;
$entity->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entity) : $entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
$this->searchService->indexEntity($entity);
return $entity;
$entityModel = $this->getEntity($type)->newInstance($input);
$entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
$entityModel->created_by = user()->id;
$entityModel->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
/**
@@ -496,6 +523,11 @@ class EntityRepo
$entityModel->fill($input);
$entityModel->updated_by = user()->id;
$entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
@@ -552,12 +584,39 @@ class EntityRepo
*/
protected function nameToSlug($name)
{
$slug = str_replace(' ', '-', strtolower($name));
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
return $slug;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
@@ -586,6 +645,43 @@ class EntityRepo
return $draftPage;
}
/**
* Create a copy of a page in a new location with a new name.
* @param Page $page
* @param Entity $newParent
* @param string $newName
* @return Page
*/
public function copyPage(Page $page, Entity $newParent, $newName = '')
{
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
$newChapter = $newParent->isA('chapter') ? $newParent : null;
$copyPage = $this->getDraftPage($newBook, $newChapter);
$pageData = $page->getAttributes();
// Update name
if (!empty($newName)) {
$pageData['name'] = $newName;
}
// Copy tags from previous page if set
if ($page->tags) {
$pageData['tags'] = [];
foreach ($page->tags as $tag) {
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
}
}
// Set priority
if ($newParent->isA('chapter')) {
$pageData['priority'] = $this->getNewChapterPriority($newParent);
} else {
$pageData['priority'] = $this->getNewBookPriority($newParent);
}
return $this->publishPageDraft($copyPage, $pageData);
}
/**
* Saves a page revision into the system.
* @param Page $page
@@ -595,7 +691,9 @@ class EntityRepo
public function savePageRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -623,7 +721,9 @@ class EntityRepo
*/
protected function formatHtml($htmlText)
{
if ($htmlText == '') return $htmlText;
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@@ -637,7 +737,9 @@ class EntityRepo
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
if (get_class($childNode) !== 'DOMElement') {
continue;
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
@@ -682,14 +784,23 @@ class EntityRepo
public function renderPage(Page $page, $ignorePermissions = false)
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) return $content;
if (count($matches[0]) === 0) {
return $content;
}
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
$pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue;
if (is_nan($pageId)) {
continue;
}
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
@@ -710,8 +821,13 @@ class EntityRepo
continue;
}
$innerContent = '';
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
if ($isTopLevel) {
$innerContent .= $doc->saveHTML($matchingElem);
} else {
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
}
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
@@ -719,6 +835,26 @@ class EntityRepo
return $content;
}
/**
* Escape script tags within HTML content.
* @param string $html
* @return mixed
*/
protected function escapeScripts(string $html)
{
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
return $html;
}
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
}
return $html;
}
/**
* Get the plain text version of a page's content.
* @param Page $page
@@ -730,28 +866,6 @@ class EntityRepo
return strip_tags($html);
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Search for image usage within page content.
* @param $imageString
@@ -775,14 +889,18 @@ class EntityRepo
*/
public function getPageNav($pageContent)
{
if ($pageContent == '') return [];
if ($pageContent == '') {
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) return [];
if (is_null($headers)) {
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
@@ -798,7 +916,7 @@ class EntityRepo
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function($header) use ($minLevel) {
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
@@ -834,7 +952,9 @@ class EntityRepo
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
@@ -896,7 +1016,9 @@ class EntityRepo
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
}
return $message . "\n" . trans('entities.pages_draft_edited_notification');
}
@@ -992,7 +1114,9 @@ class EntityRepo
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
$draft->save();
return $draft;
@@ -1073,6 +1197,7 @@ class EntityRepo
/**
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
*/
public function destroyPage(Page $page)
{
@@ -1084,6 +1209,12 @@ class EntityRepo
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
@@ -1092,17 +1223,4 @@ class EntityRepo
$page->delete();
}
}

View File

@@ -1,12 +1,9 @@
<?php namespace BookStack\Repos;
use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
@@ -95,7 +92,7 @@ class ImageRepo
* @param string $searchTerm
* @return array
*/
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
@@ -104,13 +101,13 @@ class ImageRepo
/**
* Get gallery images with a particular filter criteria such as
* being within the current book or page.
* @param int $pagination
* @param int $pageSize
* @param $filter
* @param $pageId
* @param int $pageNum
* @param int $pageSize
* @return array
*/
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', 'gallery');
@@ -123,7 +120,7 @@ class ImageRepo
$images = $images->whereIn('uploaded_to', $validPageIds);
}
return $this->returnPaginated($images, $pagination, $pageSize);
return $this->returnPaginated($images, $pageNum, $pageSize);
}
/**
@@ -132,6 +129,8 @@ class ImageRepo
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
@@ -140,11 +139,28 @@ class ImageRepo
return $image;
}
/**
* Save a drawing the the database;
* @param string $base64Uri
* @param int $uploadedTo
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function saveDrawing(string $base64Uri, int $uploadedTo)
{
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
$image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
return $image;
}
/**
* Update the details of an image via an array of properties.
* @param Image $image
* @param array $updateDetails
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function updateImageDetails(Image $image, $updateDetails)
{
@@ -156,13 +172,14 @@ class ImageRepo
/**
* Destroys an Image object along with its files and thumbnails.
* Destroys an Image object along with its revisions, files and thumbnails.
* @param Image $image
* @return bool
* @throws \Exception
*/
public function destroyImage(Image $image)
{
$this->imageService->destroyImage($image);
$this->imageService->destroy($image);
return true;
}
@@ -170,8 +187,10 @@ class ImageRepo
/**
* Load thumbnails onto an image object.
* @param Image $image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
private function loadThumbs(Image $image)
protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
@@ -183,22 +202,45 @@ class ImageRepo
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
} catch (FileNotFoundException $exception) {
$image->delete();
return [];
} catch (\Exception $exception) {
return null;
}
}
/**
* Get the raw image data from an Image.
* @param Image $image
* @return null|string
*/
public function getImageData(Image $image)
{
try {
return $this->imageService->getImageData($image);
} catch (\Exception $exception) {
return null;
}
}
}
/**
* Check if the provided image type is valid.
* @param $type
* @return bool
*/
public function isValidType($type)
{
$validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\RolePermission;
use BookStack\Role;
@@ -149,5 +148,4 @@ class PermissionsRepo
$this->permissionService->deleteJointPermissionsForRole($role);
$role->delete();
}
}
}

View File

@@ -52,7 +52,9 @@ class TagRepo
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) return collect();
if ($entity === null) {
return collect();
}
return $entity->tags;
}
@@ -95,7 +97,9 @@ class TagRepo
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
if ($tagName !== false) {
$query = $query->where('name', '=', $tagName);
}
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
return $query->get(['value'])->pluck('value');
@@ -112,7 +116,9 @@ class TagRepo
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') continue;
if (trim($tag['name']) === '') {
continue;
}
$newTags[] = $this->newInstanceFromInput($tag);
}
@@ -132,5 +138,4 @@ class TagRepo
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
}
}
}

View File

@@ -1,8 +1,12 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Image;
use BookStack\Role;
use BookStack\User;
use Exception;
use Images;
class UserRepo
{
@@ -57,13 +61,13 @@ class UserRepo
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
$query->where(function($query) use ($term) {
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);
});
@@ -83,16 +87,7 @@ class UserRepo
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
$user->save();
\Log::error('Failed to save user gravatar image');
}
}
$this->downloadGravatarToUserAvatar($user);
return $user;
}
@@ -104,10 +99,27 @@ class UserRepo
public function attachDefaultRole($user)
{
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
if ($roleId === false) {
$roleId = $this->role->first()->id;
}
$user->attachRoleId($roleId);
}
/**
* Assign a user to a system-level role.
* @param User $user
* @param $systemRoleName
* @throws NotFoundException
*/
public function attachSystemRole(User $user, $systemRoleName)
{
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
if ($role === null) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
}
/**
* Checks if the give user is the only admin.
* @param User $user
@@ -115,10 +127,14 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if (!$user->roles->pluck('name')->contains('admin')) return false;
if (!$user->hasSystemRole('admin')) {
return false;
}
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
$adminRole = $this->role->getSystemRole('admin');
if ($adminRole->users->count() > 1) {
return false;
}
return true;
}
@@ -140,11 +156,18 @@ class UserRepo
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
* @throws Exception
*/
public function destroy(User $user)
{
$user->socialAccounts()->delete();
$user->delete();
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
}
/**
@@ -156,7 +179,7 @@ class UserRepo
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
return Activity::userActivity($user, $count, $page);
}
/**
@@ -213,4 +236,27 @@ class UserRepo
return $this->role->where('system_name', '!=', 'admin')->get();
}
}
/**
* Get a gravatar image for a user and set it as their avatar.
* Does not run if gravatar disabled in config.
* @param User $user
* @return bool
*/
public function downloadGravatarToUserAvatar(User $user)
{
// Get avatar from gravatar and save
if (!config('services.gravatar')) {
return false;
}
try {
$avatar = Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
return false;
}
}
}

View File

@@ -1,10 +1,9 @@
<?php namespace BookStack;
class Role extends Model
{
protected $fillable = ['display_name', 'description'];
protected $fillable = ['display_name', 'description', 'external_auth_id'];
/**
* The roles that belong to the role.
@@ -40,7 +39,9 @@ class Role extends Model
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
if ($permission->getRawAttribute('name') === $permissionName) return true;
if ($permission->getRawAttribute('name') === $permissionName) {
return true;
}
}
return false;
}
@@ -91,5 +92,4 @@ class Role extends Model
{
return static::where('hidden', '=', false)->orderBy('name')->get();
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class RolePermission extends Model
{
/**
@@ -8,7 +7,7 @@ class RolePermission extends Model
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');
}
/**

View File

@@ -14,5 +14,4 @@ class SearchTerm extends Model
{
return $this->morphTo('entity');
}
}

View File

@@ -170,5 +170,4 @@ class ActivityService
Session::flash('success', $message);
}
}
}
}

View File

@@ -8,15 +8,31 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
{
/**
* Get the storage that will be used for storing files.
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function getStorage()
{
$storageType = config('filesystems.default');
// Override default location if set to local public to ensure not visible.
if ($storageType === 'local') {
$storageType = 'local_secure';
}
return $this->fileSystem->disk($storageType);
}
/**
* Get an attachment from storage.
* @param Attachment $attachment
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getAttachmentFromStorage(Attachment $attachment)
{
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
return $this->getStorage()->get($attachmentPath);
return $this->getStorage()->get($attachment->path);
}
/**
@@ -92,16 +108,6 @@ class AttachmentService extends UploadService
]);
}
/**
* Get the file storage base path, amended for storage type.
* This allows us to keep a generic path in the database.
* @return string
*/
private function getStorageBasePath()
{
return $this->isLocal() ? 'storage/' : '';
}
/**
* Updates the file ordering for a listing of attached files.
* @param array $attachmentList
@@ -138,6 +144,7 @@ class AttachmentService extends UploadService
/**
* Delete a File from the database and storage.
* @param Attachment $attachment
* @throws Exception
*/
public function deleteFile(Attachment $attachment)
{
@@ -157,11 +164,10 @@ class AttachmentService extends UploadService
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
$storage = $this->getStorage();
$dirPath = dirname($storedFilePath);
$dirPath = dirname($attachment->path);
$storage->delete($storedFilePath);
$storage->delete($attachment->path);
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
@@ -179,23 +185,20 @@ class AttachmentService extends UploadService
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
$uploadFileName = $attachmentName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
while ($storage->exists($basePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$attachmentPath = $attachmentBasePath . $uploadFileName;
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
$attachmentPath = $basePath . $uploadFileName;
try {
$storage->put($attachmentStoragePath, $attachmentData);
$storage->put($attachmentPath, $attachmentData);
} catch (Exception $e) {
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
}
return $attachmentPath;
}
}
}

View File

@@ -108,6 +108,4 @@ class EmailConfirmationService
}
return $token;
}
}
}

View File

@@ -9,14 +9,16 @@ class ExportService
{
protected $entityRepo;
protected $imageService;
/**
* ExportService constructor.
* @param $entityRepo
*/
public function __construct(EntityRepo $entityRepo)
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
{
$this->entityRepo = $entityRepo;
$this->imageService = $imageService;
}
/**
@@ -24,6 +26,7 @@ class ExportService
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
* @throws \Throwable
*/
public function pageToContainedHtml(Page $page)
{
@@ -38,11 +41,12 @@ class ExportService
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
* @throws \Throwable
*/
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$pages->each(function ($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
@@ -56,6 +60,7 @@ class ExportService
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
* @throws \Throwable
*/
public function bookToContainedHtml(Book $book)
{
@@ -71,6 +76,7 @@ class ExportService
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
* @throws \Throwable
*/
public function pageToPdf(Page $page)
{
@@ -85,11 +91,12 @@ class ExportService
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
* @throws \Throwable
*/
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$pages->each(function ($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
@@ -103,6 +110,7 @@ class ExportService
* Convert a book to a PDF file
* @param Book $book
* @return string
* @throws \Throwable
*/
public function bookToPdf(Book $book)
{
@@ -118,6 +126,7 @@ class ExportService
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
* @throws \Exception
*/
protected function htmlToPdf($html)
{
@@ -127,7 +136,7 @@ class ExportService
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
$pdf = \DomPDF::loadHTML($containedHtml);
}
return $pdf->output();
}
@@ -136,6 +145,7 @@ class ExportService
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @return mixed|string
* @throws \Exception
*/
protected function containHtml($htmlContent)
{
@@ -145,23 +155,14 @@ class ExportService
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$isLocal = strpos(trim($srcString), 'http') !== 0;
if ($isLocal) {
$pathString = public_path(trim($srcString, '/'));
} else {
$pathString = $srcString;
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
}
if ($isLocal && !file_exists($pathString)) continue;
try {
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {
$newImageString = '';
}
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
}
}
@@ -238,17 +239,4 @@ class ExportService
}
return $text;
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Activity extends Facade
@@ -10,5 +9,8 @@ class Activity extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'activity'; }
}
protected static function getFacadeAccessor()
{
return 'activity';
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
@@ -10,5 +9,8 @@ class Images extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'images'; }
}
protected static function getFacadeAccessor()
{
return 'images';
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Setting extends Facade
@@ -10,5 +9,8 @@ class Setting extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'setting'; }
}
protected static function getFacadeAccessor()
{
return 'setting';
}
}

View File

@@ -9,5 +9,8 @@ class Views extends Facade
*
* @return string
*/
protected static function getFacadeAccessor() { return 'views'; }
}
protected static function getFacadeAccessor()
{
return 'views';
}
}

View File

@@ -3,11 +3,11 @@
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use DB;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Cache\Repository as Cache;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -17,20 +17,40 @@ class ImageService extends UploadService
protected $imageTool;
protected $cache;
protected $storageUrl;
protected $image;
/**
* ImageService constructor.
* @param $imageTool
* @param $fileSystem
* @param $cache
* @param Image $image
* @param ImageManager $imageTool
* @param FileSystem $fileSystem
* @param Cache $cache
*/
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->image = $image;
$this->imageTool = $imageTool;
$this->cache = $cache;
parent::__construct($fileSystem);
}
/**
* Get the storage that will be used for storing images.
* @param string $type
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function getStorage($type = '')
{
$storageType = config('filesystems.default');
// Override default location if set to local public to ensure not visible.
if ($type === 'system' && $storageType === 'local_secure') {
$storageType = 'local';
}
return $this->fileSystem->disk($storageType);
}
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
@@ -46,6 +66,24 @@ class ImageService extends UploadService
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
/**
* Save a new image from a uri-encoded base64 string of data.
* @param string $base64Uri
* @param string $name
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/**
* Gets an image from url and saves it to the database.
@@ -59,7 +97,9 @@ class ImageService extends UploadService
{
$imageName = $imageName ? $imageName : basename($url);
$imageData = file_get_contents($url);
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
if ($imageData === false) {
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}
return $this->saveNew($imageName, $imageData, $type);
}
@@ -74,20 +114,20 @@ class ImageService extends UploadService
*/
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
{
$storage = $this->getStorage();
$storage = $this->getStorage($type);
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
}
$fullPath = $imagePath . $imageName;
if ($secureUploads) {
$fullPath = $imagePath . str_random(16) . '-' . $imageName;
}
try {
$storage->put($fullPath, $imageData);
@@ -96,8 +136,6 @@ class ImageService extends UploadService
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
}
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
@@ -112,26 +150,26 @@ class ImageService extends UploadService
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
$image = $this->image->newInstance();
$image->forceFill($imageDetails)->save();
return $image;
}
/**
* Get the storage path, Dependant of storage type.
* Checks if the image is a gif. Returns true if it is, else false.
* @param Image $image
* @return mixed|string
* @return boolean
*/
protected function getPath(Image $image)
protected function isGif(Image $image)
{
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}
/**
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
* @param int $width
* @param int $height
@@ -142,16 +180,19 @@ class ImageService extends UploadService
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
if ($keepRatio && $this->isGif($image)) {
return $this->getPublicUrl($image->path);
}
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
$imagePath = $this->getPath($image);
$imagePath = $image->path;
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
}
$storage = $this->getStorage();
$storage = $this->getStorage($image->type);
if ($storage->exists($thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
}
@@ -161,9 +202,8 @@ class ImageService extends UploadService
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
} else {
throw $e;
}
throw $e;
}
if ($keepRatio) {
@@ -184,40 +224,67 @@ class ImageService extends UploadService
}
/**
* Destroys an Image object along with its files and thumbnails.
* Get the raw data content from an image.
* @param Image $image
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getImageData(Image $image)
{
$imagePath = $image->path;
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/**
* Destroy an image along with its revisions, thumbnails and remaining folders.
* @param Image $image
* @throws Exception
*/
public function destroy(Image $image)
{
$this->destroyImagesFromPath($image->path);
$image->delete();
}
/**
* Destroys an image at the given path.
* Searches for image thumbnails in addition to main provided path..
* @param string $path
* @return bool
*/
public function destroyImage(Image $image)
protected function destroyImagesFromPath(string $path)
{
$storage = $this->getStorage();
$imageFolder = dirname($this->getPath($image));
$imageFileName = basename($this->getPath($image));
$imageFolder = dirname($path);
$imageFileName = basename($path);
$allImages = collect($storage->allFiles($imageFolder));
// Delete image files
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
$expectedIndex = strlen($imagePath) - strlen($imageFileName);
return strpos($imagePath, $imageFileName) === $expectedIndex;
});
$storage->delete($imagesToDelete->all());
// Cleanup of empty folders
foreach ($storage->directories($imageFolder) as $directory) {
if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
$foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
foreach ($foldersInvolved as $directory) {
if ($this->isFolderEmpty($directory)) {
$storage->deleteDirectory($directory);
}
}
if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
$image->delete();
return true;
}
/**
* Save a gravatar image and set a the profile image for a user.
* @param User $user
* @param int $size
* @param int $size
* @return mixed
* @throws Exception
*/
public function saveUserGravatar(User $user, $size = 500)
{
@@ -231,6 +298,93 @@ class ImageService extends UploadService
return $image;
}
/**
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
* Checks based off of only the image name.
* Could be much improved to be more specific but kept it generic for now to be safe.
*
* Returns the path of the images that would be/have been deleted.
* @param bool $checkRevisions
* @param bool $dryRun
* @param array $types
* @return array
*/
public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
{
$types = array_intersect($types, ['gallery', 'drawio']);
$deletedPaths = [];
$this->image->newQuery()->whereIn('type', $types)
->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages')
->where('html', 'like', $searchQuery)->count() > 0;
$inRevision = false;
if ($checkRevisions) {
$inRevision = DB::table('page_revisions')
->where('html', 'like', $searchQuery)->count() > 0;
}
if (!$inPage && !$inRevision) {
$deletedPaths[] = $image->path;
if (!$dryRun) {
$this->destroy($image);
}
}
}
});
return $deletedPaths;
}
/**
* Convert a image URI to a Base64 encoded string.
* Attempts to find locally via set storage method first.
* @param string $uri
* @return null|string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function imageUriToBase64(string $uri)
{
$isLocal = strpos(trim($uri), 'http') !== 0;
// Attempt to find local files even if url not absolute
$base = baseUrl('/');
if (!$isLocal && strpos($uri, $base) === 0) {
$isLocal = true;
$uri = str_replace($base, '', $uri);
}
$imageData = null;
if ($isLocal) {
$uri = trim($uri, '/');
$storage = $this->getStorage();
if ($storage->exists($uri)) {
$imageData = $storage->get($uri);
}
} else {
try {
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageData = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
} catch (\Exception $e) {
}
}
if ($imageData === null) {
return null;
}
return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
}
/**
* Gets a public facing url for an image by checking relevant environment variables.
* @param string $filePath
@@ -252,14 +406,10 @@ class ImageService extends UploadService
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
}
$this->storageUrl = $storageUrl;
}
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
$basePath = ($this->storageUrl == false) ? baseUrl('/') : $this->storageUrl;
return rtrim($basePath, '/') . $filePath;
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
@@ -93,5 +92,4 @@ class Ldap
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}

View File

@@ -1,8 +1,11 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use BookStack\Repos\UserRepo;
use BookStack\Role;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
/**
* Class LdapService
@@ -15,15 +18,55 @@ class LdapService
protected $ldap;
protected $ldapConnection;
protected $config;
protected $userRepo;
protected $enabled;
/**
* LdapService constructor.
* @param Ldap $ldap
* @param UserRepo $userRepo
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserRepo $userRepo)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
$this->userRepo = $userRepo;
$this->enabled = config('auth.method') === 'ldap';
}
/**
* Check if groups should be synced.
* @return bool
*/
public function shouldSyncGroups()
{
return $this->enabled && $this->config['user_to_groups'] !== false;
}
/**
* Search for attributes for a specific user on the ldap
* @param string $userName
* @param array $attributes
* @return null|array
* @throws LdapException
*/
private function getUserWithAttributes($userName, $attributes)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
if ($users['count'] === 0) {
return null;
}
return $users[0];
}
/**
@@ -35,19 +78,13 @@ class LdapService
*/
public function getUserDetails($userName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$emailAttr = $this->config['email_attribute'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
$user = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
@@ -66,8 +103,12 @@ class LdapService
public function validateUserCredentials(Authenticatable $user, $username, $password)
{
$ldapUser = $this->getUserDetails($username);
if ($ldapUser === null) return false;
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
if ($ldapUser === null) {
return false;
}
if ($ldapUser['uid'] !== $user->external_auth_id) {
return false;
}
$ldapConnection = $this->getConnection();
try {
@@ -97,7 +138,9 @@ class LdapService
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
if (!$ldapBind) {
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
}
/**
@@ -108,7 +151,9 @@ class LdapService
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) return $this->ldapConnection;
if ($this->ldapConnection !== null) {
return $this->ldapConnection;
}
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
@@ -118,7 +163,9 @@ class LdapService
// 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, '');
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
@@ -152,4 +199,172 @@ class LdapService
return strtr($filterString, $newAttrs);
}
}
/**
* Get the groups a user is a part of on ldap
* @param string $userName
* @return array
* @throws LdapException
*/
public function getUserGroups($userName)
{
$groupsAttr = $this->config['group_attribute'];
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
if ($user === null) {
return [];
}
$userGroups = $this->groupFilter($user);
$userGroups = $this->getGroupsRecursive($userGroups, []);
return $userGroups;
}
/**
* Get the parent groups of an array of groups
* @param array $groupsArray
* @param array $checked
* @return array
* @throws LdapException
*/
private function getGroupsRecursive($groupsArray, $checked)
{
$groups_to_add = [];
foreach ($groupsArray as $groupName) {
if (in_array($groupName, $checked)) {
continue;
}
$groupsToAdd = $this->getGroupGroups($groupName);
$groups_to_add = array_merge($groups_to_add, $groupsToAdd);
$checked[] = $groupName;
}
$groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
if (!empty($groups_to_add)) {
return $this->getGroupsRecursive($groupsArray, $checked);
} else {
return $groupsArray;
}
}
/**
* Get the parent groups of a single group
* @param string $groupName
* @return array
* @throws LdapException
*/
private function getGroupGroups($groupName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$baseDn = $this->config['base_dn'];
$groupsAttr = strtolower($this->config['group_attribute']);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
if ($groups['count'] === 0) {
return [];
}
$groupGroups = $this->groupFilter($groups[0]);
return $groupGroups;
}
/**
* Filter out LDAP CN and DN language in a ldap search return
* Gets the base CN (common name) of the string
* @param string $ldapSearchReturn
* @return array
*/
protected function groupFilter($ldapSearchReturn)
{
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = [];
$count = 0;
if (isset($ldapSearchReturn[$groupsAttr]['count'])) {
$count = (int) $ldapSearchReturn[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
$dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0];
}
}
return $ldapGroups;
}
/**
* Sync the LDAP groups to the user roles for the current user
* @param \BookStack\User $user
* @param string $username
* @throws LdapException
*/
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
// Get the ids for the roles from the names
$ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups);
// Sync groups
if ($this->config['remove_from_groups']) {
$user->roles()->sync($ldapGroupsAsRoles);
$this->userRepo->attachDefaultRole($user);
} else {
$user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
}
}
/**
* Match an array of group names from LDAP to BookStack system roles.
* Formats LDAP group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchLdapGroupsToSystemsRoles(array $groupNames)
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function(Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$matchedRoles = $roles->filter(function(Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});
return $matchedRoles->pluck('id');
}
/**
* Check a role against an array of group names to see if it matches.
* Checked against role 'external_auth_id' if set otherwise the name of the role.
* @param Role $role
* @param array $groupNames
* @return bool
*/
protected function roleMatchesGroupNames(Role $role, array $groupNames)
{
if ($role->external_auth_id) {
$externalAuthIds = explode(',', strtolower($role->external_auth_id));
foreach ($externalAuthIds as $externalAuthId) {
if (in_array(trim($externalAuthId), $groupNames)) {
return true;
}
}
return false;
}
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
return in_array($roleName, $groupNames);
}
}

View File

@@ -67,13 +67,19 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param Entity[] $entities
*/
protected function readyEntityCache()
protected function readyEntityCache($entities = [])
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
$this->entityCache = [];
foreach ($entities as $entity) {
$type = $entity->getType();
if (!isset($this->entityCache[$type])) {
$this->entityCache[$type] = collect();
}
$this->entityCache[$type]->put($entity->id, $entity);
}
}
/**
@@ -83,14 +89,13 @@ class PermissionService
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
return $this->entityCache['book']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
if ($book === null) {
$book = false;
}
return $book;
@@ -103,14 +108,13 @@ class PermissionService
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
return $this->entityCache['chapter']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
if ($chapter === null) {
$chapter = false;
}
return $chapter;
@@ -122,7 +126,9 @@ class PermissionService
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
if ($this->userRoles !== false) {
return $this->userRoles;
}
$roles = [];
@@ -161,9 +167,9 @@ class PermissionService
*/
protected function bookFetchQuery()
{
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function($query) {
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
@@ -173,8 +179,10 @@ class PermissionService
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
@@ -187,7 +195,9 @@ class PermissionService
}
}
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities, $roles);
}
@@ -205,12 +215,17 @@ class PermissionService
}
$entities[] = $entity->book;
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity->isA('chapter')) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities));
}
@@ -236,7 +251,7 @@ class PermissionService
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
@@ -256,7 +271,7 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
$roleIds = array_map(function($role) {
$roleIds = array_map(function ($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
@@ -265,6 +280,7 @@ class PermissionService
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
@@ -274,24 +290,26 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
* @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
if (count($entities) === 0) {
return;
}
$this->db->transaction(function() use ($entities) {
$this->db->transaction(function () use ($entities) {
foreach (array_chunk($entities, 1000) as $entityChunk) {
$query = $this->db->table('joint_permissions');
foreach ($entityChunk as $entity) {
$query->orWhere(function(QueryBuilder $query) use ($entity) {
$query->orWhere(function (QueryBuilder $query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
});
}
@@ -299,10 +317,11 @@ class PermissionService
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param array $roles
* @throws \Throwable
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
@@ -310,7 +329,7 @@ class PermissionService
$permissionFetch = $this->entityPermission->newQuery();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function($query) use ($entity) {
$permissionFetch->orWhere(function ($query) use ($entity) {
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
});
}
@@ -327,7 +346,7 @@ class PermissionService
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->getRelationValue('permissions') as $permission) {
foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
@@ -341,7 +360,7 @@ class PermissionService
}
}
$this->db->transaction(function() use ($jointPermissions) {
$this->db->transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
}
@@ -357,8 +376,12 @@ class PermissionService
protected function getActions(Entity $entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
if ($entity->isA('chapter') || $entity->isA('book')) {
$baseActions[] = 'page-create';
}
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
return $baseActions;
}
@@ -407,7 +430,10 @@ class PermissionService
}
}
return $this->createJointPermissionDataArray($entity, $role, $action,
return $this->createJointPermissionDataArray(
$entity,
$role,
$action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
@@ -421,7 +447,8 @@ class PermissionService
* @param $action
* @return bool
*/
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
{
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false;
}
@@ -540,11 +567,12 @@ class PermissionService
* @param bool $fetchPageContent
* @return QueryBuilder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
{
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
@@ -557,8 +585,8 @@ class PermissionService
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
});
@@ -606,16 +634,17 @@ class PermissionService
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
@@ -710,5 +739,4 @@ class PermissionService
$this->userRoles = false;
$this->isAdminUser = null;
}
}
}

View File

@@ -64,15 +64,14 @@ class SearchService
* @param string $searchString
* @param string $entityType
* @param int $page
* @param int $count
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @return array[int, Collection];
*/
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
$entityTypesToSearch = $entityTypes;
$results = collect();
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
@@ -80,18 +79,27 @@ class SearchService
$entityTypesToSearch = explode('|', $terms['filters']['type']);
}
$results = collect();
$total = 0;
$hasMore = false;
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
if ($entityTotal > $page * $count) {
$hasMore = true;
}
$total += $entityTotal;
$results = $results->merge($search);
}
return [
'total' => $total,
'count' => count($results),
'has_more' => $hasMore,
'results' => $results->sortByDesc('score')->values()
];
}
@@ -111,7 +119,9 @@ class SearchService
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
@@ -137,13 +147,16 @@ class SearchService
* @param string $entityType
* @param int $page
* @param int $count
* @param string $action
* @param bool $getCount Return the total count of the search
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
if ($getCount) return $query->count();
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
if ($getCount) {
return $query->count();
}
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
@@ -153,9 +166,10 @@ class SearchService
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @param string $action
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page')
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
@@ -164,12 +178,12 @@ class SearchService
if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
$subQuery->where(function(Builder $query) use ($terms) {
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
@@ -177,7 +191,7 @@ class SearchService
// Handle exact term matching
if (count($terms['exact']) > 0) {
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) {
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
@@ -195,10 +209,12 @@ class SearchService
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
$functionName = camel_case('filter_' . $filterTerm);
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
@@ -234,7 +250,9 @@ class SearchService
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
if ($searchTerm !== '') {
$terms['search'][] = $searchTerm;
}
}
// Split filter values out
@@ -267,15 +285,18 @@ class SearchService
* @param string $tagTerm
* @return mixed
*/
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm)
{
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
$tagName = $tagSplit[1];
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
$validOperator = in_array($tagOperator, $this->queryOperators);
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
if (!empty($tagName)) $query->where('name', '=', $tagName);
if (!empty($tagName)) {
$query->where('name', '=', $tagName);
}
if (is_numeric($tagValue) && $tagOperator !== 'like') {
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
// search the value as a string which prevents being able to do number-based operations
@@ -309,8 +330,8 @@ class SearchService
public function indexEntity(Entity $entity)
{
$this->deleteEntityTerms($entity);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
$terms = array_merge($nameTerms, $bodyTerms);
foreach ($terms as $index => $term) {
$terms[$index]['entity_type'] = $entity->getMorphClass();
@@ -323,11 +344,12 @@ class SearchService
* Index multiple Entities at once
* @param Entity[] $entities
*/
protected function indexEntities($entities) {
protected function indexEntities($entities)
{
$terms = [];
foreach ($entities as $entity) {
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
$term['entity_id'] = $entity->id;
$term['entity_type'] = $entity->getMorphClass();
@@ -382,11 +404,15 @@ class SearchService
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitText = explode(' ', $text);
foreach ($splitText as $token) {
if ($token === '') continue;
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) {
$tokenMap[$token] = 0;
}
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
@@ -408,43 +434,63 @@ class SearchService
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('updated_at', '>=', $date);
}
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('updated_at', '<', $date);
}
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('created_at', '>=', $date);
}
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
try {
$date = date_create($input);
} catch (\Exception $e) {
return;
}
$query->where('created_at', '<', $date);
}
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
if (!is_numeric($input) && $input !== 'me') {
return;
}
if ($input === 'me') {
$input = user()->id;
}
$query->where('created_by', '=', $input);
}
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
if (!is_numeric($input) && $input !== 'me') {
return;
}
if ($input === 'me') {
$input = user()->id;
}
$query->where('updated_by', '=', $input);
}
@@ -453,7 +499,10 @@ class SearchService
$query->where('name', 'like', '%' .$input. '%');
}
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$this->filterInName($query, $model, $input);
}
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
@@ -467,16 +516,37 @@ class SearchService
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereHas('views', function($query) {
$query->whereHas('views', function ($query) {
$query->where('user_id', '=', user()->id);
});
}
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereDoesntHave('views', function($query) {
$query->whereDoesntHave('views', function ($query) {
$query->where('user_id', '=', user()->id);
});
}
}
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$functionName = camel_case('sort_by_' . $input);
if (method_exists($this, $functionName)) {
$this->$functionName($query, $model);
}
}
/**
* Sorting filter options
*/
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
{
$commentsTable = $this->db->getTablePrefix() . 'comments';
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
}
}

View File

@@ -40,8 +40,12 @@ class SettingService
*/
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
if (isset($this->localCache[$key])) return $this->localCache[$key];
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
$value = $this->getValueFromStore($key, $default);
$formatted = $this->formatValue($value, $default);
@@ -72,12 +76,16 @@ class SettingService
{
// Check for an overriding value
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
if ($overrideValue !== null) {
return $overrideValue;
}
// Check the cache
$cacheKey = $this->cachePrefix . $key;
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
if ($cacheVal !== null) {
return $cacheVal;
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
@@ -98,6 +106,9 @@ class SettingService
{
$cacheKey = $this->cachePrefix . $key;
$this->cache->forget($cacheKey);
if (isset($this->localCache[$key])) {
unset($this->localCache[$key]);
}
}
/**
@@ -109,11 +120,17 @@ class SettingService
protected function formatValue($value, $default)
{
// Change string booleans to actual booleans
if ($value === 'true') $value = true;
if ($value === 'false') $value = false;
if ($value === 'true') {
$value = true;
}
if ($value === 'false') {
$value = false;
}
// Set to default if empty
if ($value === '') $value = $default;
if ($value === '') {
$value = $default;
}
return $value;
}
@@ -222,8 +239,9 @@ class SettingService
*/
protected function getOverrideValue($key)
{
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') {
return false;
}
return null;
}
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Http\Requests\Request;
use GuzzleHttp\Exception\ClientException;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
@@ -14,7 +16,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* SocialAuthService constructor.
@@ -91,7 +93,6 @@ class SocialAuthService
public function handleLoginCallback($socialDriver)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
$socialId = $socialUser->getId();
@@ -135,7 +136,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
throw new SocialSignInException($message, '/login');
}
/**
@@ -149,8 +150,12 @@ class SocialAuthService
{
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
if (!in_array($driver, $this->validSocialDrivers)) {
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
}
return $driver;
}
@@ -219,5 +224,4 @@ class SocialAuthService
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());
}
}
}

View File

@@ -11,11 +11,6 @@ class UploadService
*/
protected $fileSystem;
/**
* @var FileSystemInstance
*/
protected $storageInstance;
/**
* FileService constructor.
@@ -32,15 +27,10 @@ class UploadService
*/
protected function getStorage()
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
return $this->fileSystem->disk($storageType);
}
/**
* Check whether or not a folder is empty.
* @param $path
@@ -52,13 +42,4 @@ class UploadService
$folders = $this->getStorage()->directories($path);
return (count($files) === 0 && count($folders) === 0);
}
/**
* Check if using a local filesystem.
* @return bool
*/
protected function isLocal()
{
return strtolower(config('filesystems.default')) === 'local';
}
}
}

View File

@@ -27,7 +27,9 @@ class ViewService
public function add(Entity $entity)
{
$user = user();
if ($user === null || $user->isDefault()) return 0;
if ($user === null || $user->isDefault()) {
return 0;
}
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
@@ -49,11 +51,13 @@ class ViewService
* @param int $count
* @param int $page
* @param bool|false|array $filterModel
* @param string $action - used for permission checking
* @return
*/
public function getPopular($count = 10, $page = 0, $filterModel = false)
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
@@ -77,12 +81,16 @@ class ViewService
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
$user = user();
if ($user === null || $user->isDefault()) return collect();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
if ($filterModel) {
$query = $query->where('viewable_type', '=', get_class($filterModel));
}
$query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
@@ -97,5 +105,4 @@ class ViewService
{
$this->view->truncate();
}
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class SocialAccount extends Model
{

View File

@@ -16,4 +16,4 @@ class Tag extends Model
{
return $this->morphTo('entity');
}
}
}

View File

@@ -60,7 +60,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function roles()
{
if ($this->id === 0) return ;
if ($this->id === 0) {
return ;
}
return $this->belongsToMany(Role::class);
}
@@ -81,7 +83,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function hasSystemRole($role)
{
return $this->roles->pluck('system_name')->contains('admin');
return $this->roles->pluck('system_name')->contains($role);
}
/**
@@ -91,9 +93,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function permissions($cache = true)
{
if(isset($this->permissions) && $cache) return $this->permissions;
if (isset($this->permissions) && $cache) {
return $this->permissions;
}
$this->load('roles.permissions');
$permissions = $this->roles->map(function($role) {
$permissions = $this->roles->map(function ($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
@@ -107,7 +111,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if ($this->email === 'guest') return false;
if ($this->email === 'guest') {
return false;
}
return $this->permissions()->pluck('name')->contains($permissionName);
}
@@ -162,7 +168,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
$default = baseUrl('/user_avatar.png');
$imageId = $this->image_id;
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
if ($imageId === 0 || $imageId === '0' || $imageId === null) {
return $default;
}
try {
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
@@ -206,10 +214,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) return $this->name;
if (strlen($this->name) <= $chars) {
return $this->name;
}
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) return $splitName[0];
if (strlen($splitName[0]) <= $chars) {
return $splitName[0];
}
return '';
}

View File

@@ -74,7 +74,9 @@ function userCan($permission, Ownable $ownable = null)
function setting($key = null, $default = false)
{
$settingService = resolve(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
if (is_null($key)) {
return $settingService;
}
return $settingService->get($key, $default);
}
@@ -87,7 +89,9 @@ function setting($key = null, $default = false)
function baseUrl($path, $forceAppDomain = false)
{
$isFullUrl = strpos($path, 'http') === 0;
if ($isFullUrl && !$forceAppDomain) return $path;
if ($isFullUrl && !$forceAppDomain) {
return $path;
}
$path = trim($path, '/');
// Remove non-specified domain if forced and we have a domain
@@ -126,12 +130,50 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
return app('redirect')->to($to, $status, $headers, $secure);
}
function icon($name, $attrs = []) {
$iconPath = resource_path('assets/icons/' . $name . '.svg');
/**
* Get a path to a theme resource.
* @param string $path
* @return string|boolean
*/
function theme_path($path = '')
{
$theme = config('view.theme');
if (!$theme) {
return false;
}
return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path));
}
/**
* Get fetch an SVG icon as a string.
* Checks for icons defined within a custom theme before defaulting back
* to the 'resources/assets/icons' folder.
*
* Returns an empty string if icon file not found.
* @param $name
* @param array $attrs
* @return mixed
*/
function icon($name, $attrs = [])
{
$attrs = array_merge([
'class' => 'svg-icon',
'data-icon' => $name
], $attrs);
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
$iconPath = resource_path('assets/icons/' . $name . '.svg');
$themeIconPath = theme_path('icons/' . $name . '.svg');
if ($themeIconPath && file_exists($themeIconPath)) {
$iconPath = $themeIconPath;
} else if (!file_exists($iconPath)) {
return '';
}
$fileContents = file_get_contents($iconPath);
return str_replace('<svg', '<svg' . $attrString, $fileContents);
}
@@ -159,11 +201,15 @@ function sortUrl($path, $data, $overrideData = [])
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') continue;
if ($trimmedVal === '') {
continue;
}
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) return $path;
if (count($queryStringSections) === 0) {
return $path;
}
return baseUrl($path . '?' . implode('&', $queryStringSections));
}
}

16
artisan
View File

@@ -1,19 +1,19 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
| Initialize The App
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
| We need to get things going before we start up the app.
| The init file loads everything in, in the correct order.
|
*/
require __DIR__.'/bootstrap/autoload.php';
require __DIR__.'/bootstrap/init.php';
$app = require_once __DIR__.'/bootstrap/app.php';
@@ -40,7 +40,7 @@ $status = $kernel->handle(
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
@@ -48,4 +48,4 @@ $status = $kernel->handle(
$kernel->terminate($input, $status);
exit($status);
exit($status);

View File

@@ -1,6 +1,15 @@
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Load Our Own Helpers
|--------------------------------------------------------------------------
|
| This custom function loads any helpers, before the Laravel Framework
| is built so we can override any helpers as we please.
|
*/
require __DIR__.'/../app/helpers.php';
/*
|--------------------------------------------------------------------------
@@ -13,23 +22,4 @@ define('LARAVEL_START', microtime(true));
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../app/helpers.php';
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/cache/compiled.php';
if (file_exists($compiledPath)) {
require $compiledPath;
}
require __DIR__.'/../vendor/autoload.php';

Some files were not shown because too many files have changed in this diff Show More