Compare commits

...

572 Commits

Author SHA1 Message Date
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
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
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
0c4ddf16a5 Moved details card above book nav 2017-09-20 21:32:19 +01:00
Dan Brown
74a5e3113e Fixed page includes erroring on save
Closes #514
2017-09-20 21:03:40 +01:00
Dan Brown
df0a982433 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-09-20 20:27:50 +01:00
Dan Brown
212f924ffa Refactored WYSIWYG editor image upload code
Added sketchy timeout to fix images being pasted at end of page.
Fixes #489
2017-09-20 20:27:00 +01:00
Dan Brown
09936566dd Upgrade tinymce version 2017-09-20 20:26:34 +01:00
Dan Brown
9469c04eab Merge pull request #517 from leomartinez/master
Added 'Spanish Argentina' translation
2017-09-20 20:17:39 +01:00
Leonardo Martinez
941bb73a68 Added missing colon 2017-09-19 14:25:44 -03:00
Leonardo Martinez
8e652f5d8f Added 'Spanish Argentina' translation. 2017-09-19 13:43:17 -03:00
Leonardo Martinez
eec1b21928 Added 'Spanish Argentina' translation. 2017-09-19 13:42:39 -03:00
Dan Brown
1baeb7bec9 Merge pull request #510 from sanderdw/patch-1
Update entities.php
2017-09-17 11:09:12 +01:00
Dan Brown
61475ca0a3 Merge pull request #506 from turbotankist/master
Russian lang added
2017-09-14 20:44:19 +01:00
sanderdw
244c5a3ebb Update entities.php 2017-09-14 21:43:47 +02:00
Dan Brown
39e7ac1c15 Updated social login to redirect to intended page.
Closes #508.
2017-09-14 20:20:47 +01:00
alexey
ab7f5def04 Russian lang added 2017-09-12 16:42:04 +03:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
cd7e727f8c Modified IT comment translations as per recent changes 2017-09-10 16:43:05 +01:00
Dan Brown
2329a5cedf Merge branch 'master' of github.com:BookStackApp/BookStack 2017-09-10 16:36:47 +01:00
Dan Brown
d1a4ff9308 Merge pull request #501 from cipi1965/master
Added Italian language
2017-09-10 16:29:44 +01:00
Dan Brown
9a3bc27ef4 Fixed bullet styles and added code highlight on comments 2017-09-10 16:14:04 +01:00
Matteo Piccina
f8315fb9c4 Added Italian language 2017-09-10 16:55:23 +02:00
Dan Brown
bb62dee5a2 Merge pull request #500 from timoschwarzer/translation_update_de
Update german translation
2017-09-10 14:08:23 +01:00
Timo Schwarzer
8acc188e16 Update german translation 2017-09-10 15:01:25 +02:00
Dan Brown
874386ceab Used trans_choice on profile view
Closes #417
2017-09-10 13:43:08 +01:00
Dan Brown
9dfbea8bf9 Restored seeder and fixed scroll on firefox 2017-09-10 13:29:48 +01:00
Dan Brown
c1627a1468 Fixed sidebar scroll on mobile 2017-09-10 13:19:47 +01:00
Dan Brown
576a59a693 Fixed line-height difference in tinymce 2017-09-10 13:08:12 +01:00
Dan Brown
fec6c65b78 Fixed quick save shortcut in wysiwyg editor
Fixes #467
2017-09-10 13:01:48 +01:00
Dan Brown
f8c046d182 Fixed markdown callout tags and cursor pos
Closes #470
2017-09-09 19:41:11 +01:00
Dan Brown
9ca2184f09 Attempt to fix travis switching phpunit version 2017-09-09 19:17:00 +01:00
Dan Brown
fd449582bd Removed comments from seeder since they are not used by tests 2017-09-09 18:48:47 +01:00
Dan Brown
621142a46e Removed outdated translations and updated tests 2017-09-09 18:41:59 +01:00
Dan Brown
0275d2ad58 Added loading icons, Added comment activity 2017-09-09 17:06:30 +01:00
Dan Brown
41f56e659d Added comment reply and delete confirmation.
Also fixed local_id bug
Added component helpers
Added global scroll & Highlight helpers
2017-09-09 15:56:24 +01: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
Dan Brown
fea5630ea4 Made some changes to the comment system
Changed to be rendered server side along with page content.
Changed deletion to fully delete comments from the database.
Added 'local_id' to comments for referencing.
Updated reply system to be non-nested (Incomplete)
Made database comment format entity-agnostic to be more future proof.
Updated designs of comment sections.
2017-09-03 16:37:51 +01:00
Dan Brown
e3f2bde26d Merge branch 'Abijeet-master' to convert comment system to vue 2017-09-02 17:40:40 +01:00
Dan Brown
756ee0b172 Merge branch 'master' of git://github.com/Abijeet/BookStack into Abijeet-master 2017-09-02 17:36:58 +01:00
Dan Brown
c81b63b56f Fixed broken page content includes 2017-09-02 16:06:03 +01:00
Dan Brown
70ee28ee13 Fixed long attachment names breaking outer boxes
Closes #460
2017-09-02 15:21:05 +01:00
Dan Brown
1c9ecc3edd Reformatted sortable toolbox components 2017-09-02 15:06:52 +01:00
Dan Brown
9bd5d6a422 Merge pull request #483 from msaus/japanese_lang_update
Japanese lang update
2017-09-02 14:39:14 +01:00
Dan Brown
1e41ccbc7a Added ability to override codemirror theme
Also cleaned codemirror file while there.
In referece to #455
2017-09-02 13:34:37 +01:00
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
Dan Brown
0a402e3c63 Made custom home ignore permissions and added tests
Closes #126 and #372
2017-08-28 13:55:39 +01:00
Dan Brown
55759bd22a Added ability to set a page to view on the homepage.
Relates to #372 and #126
2017-08-28 13:38:32 +01:00
Dan Brown
d8e1f52ddd Made new sidebar layout responsive 2017-08-27 15:16:51 +01:00
Dan Brown
baea92b206 Migrated entity selector out of angular 2017-08-27 14:31:34 +01:00
Dan Brown
cfc05c1b22 Improved primary color control in settings 2017-08-27 12:59:56 +01:00
Dan Brown
ebf78d49a8 Merge pull request #480 from BookStackApp/design_update_2017
Design update 2017
2017-08-26 17:22:30 +01:00
Dan Brown
4cb4c9e568 Updated remaining views to 2017 design update.
Also fixed issue with duplicate confirmation email.
2017-08-26 17:17:04 +01:00
Dan Brown
36f524a354 Updated page view styles to align with 2017 update 2017-08-26 15:41:33 +01:00
Dan Brown
b60d2190ac Updated error views for redesign 2017-08-26 14:53:23 +01:00
Dan Brown
2f8b8c580d Resolved current failing tests 2017-08-26 14:41:46 +01:00
Dan Brown
5187d3fa78 Updated chapter views with new design 2017-08-26 14:36:48 +01:00
Dan Brown
9c07741972 Updated readme with project definition 2017-08-26 13:49:56 +01:00
Dan Brown
8fcbe44d3e Updated styles for auth and books views.
Also added sourcemaps to gulp sass build
2017-08-26 13:24:55 +01:00
Bharadwaja G
7f902e41c7 Resolved conflicts 2017-08-24 12:21:43 +05:30
soseki
3966fb1df6 update Japanese language for code editor 2017-08-24 15:30:07 +09:00
soseki
830614fb19 add Japanese language for code editor 2017-08-24 15:24:43 +09:00
Abijeet
76ae5c7398 Removes some unused code. 2017-08-23 01:04:36 +05:30
Abijeet
769935f99e Reverting database.php and app.php 2017-08-23 00:52:50 +05:30
Abijeet
6920d6eef1 Fixes the comment check for linked comment. 2017-08-22 01:39:09 +05:30
Abijeet
b5cd3bff3c Added functionality to highlight a comment. 2017-08-22 01:31:11 +05:30
Abijeet
ac07cb41b6 Fixed formatting and added error messages. 2017-08-21 02:21:31 +05:30
Abijeet
e8fa58f201 Removed code from the directives. 2017-08-21 00:38:59 +05:30
Abijeet
1f6994b62c Added code for permissions, removed unnecessary code. 2017-08-20 21:26:44 +05:30
Abijeet
47d82a1ac2 Merge branch 'master' of https://github.com/BookStackApp/BookStack
Conflicts:
	resources/assets/js/vues/vues.js
2017-08-20 20:35:56 +05:30
Abijeet
703d579561 Refactored Angular code to instead use VueJS, left with permissions, testing and load testing. 2017-08-20 20:21:32 +05:30
Abijeet
ed375bfaf7 Refactored Angular code to instead use VueJS, left with permissions, testing and load testing. 2017-08-20 20:21:27 +05:30
Dan Brown
3da8c01c1f Rolled out new design further 2017-08-20 13:57:25 +01:00
Dan Brown
295f520f21 Started design update 2017-08-19 18:32:24 +01:00
Dan Brown
fba7ae923d Darkened a few cm code styles for improved legibility 2017-08-19 15:51:51 +01:00
Dan Brown
28a9bd514f Updated header design 2017-08-19 15:33:22 +01:00
Dan Brown
666c86b108 Removed included fonts, Set to use system fonts.
All font definitions moved into _text.scss.
Needs override documentation to complete.
Relates to #423.
2017-08-19 14:33:55 +01:00
Dan Brown
194293664f Removed v-show delayed content display 2017-08-19 14:09:03 +01:00
Dan Brown
039ee5d06c Aligned entity dash name and fixed chapter toggle on dash 2017-08-19 14:04:38 +01:00
Dan Brown
afc66b3c3d Migrated attachment manager to vue 2017-08-19 13:55:56 +01:00
Dan Brown
a04b31866d Cleaned social callback 2017-08-17 19:44:35 +01:00
Dan Brown
db3dde98ef Merge pull request #474 from timoschwarzer/translation_update_de
Update and fix German translation
2017-08-17 18:51:35 +01:00
Timo Schwarzer
0d6a6b5b63 Update/fix german translation 2017-08-16 22:31:07 +02:00
Abijeet Patro
4df3267521 Merge pull request #13 from BookStackApp/master
Getting the latest changes.
2017-08-14 23:09:26 +05:30
Dan Brown
d3e4a1a6f9 Converted tag autosuggestion to vue component 2017-08-13 13:25:30 +01:00
Dan Brown
b023699f1b Converted tag manager to be fully vue based 2017-08-13 11:50:40 +01:00
Dan Brown
f338dbe3f8 Started vueifying tag system 2017-08-10 20:11:25 +01:00
Dan Brown
ab07f7df6c Converted image manager into vue component 2017-08-09 21:33:00 +01:00
Dan Brown
a59d73de7b Fixed bug causing image manager popup not to show 2017-08-07 19:32:31 +01:00
Dan Brown
1ac7618bb1 Updated clipboard lib reference and version used 2017-08-06 21:21:20 +01:00
Dan Brown
2a069880cd Converted jQuery bits into raw JS components 2017-08-06 21:08:03 +01:00
Dan Brown
5e5928a8a6 Added vanilla JS component system 2017-08-06 18:01:49 +01:00
Dan Brown
d6e87420c3 Merged comment migrations and incremented dev version 2017-08-01 20:05:49 +01:00
Dan Brown
79cfd39fde Merge branch 'Abijeet-master' 2017-08-01 20:04:33 +01:00
Dan Brown
e9831a7507 Merge branch 'master' of git://github.com/Abijeet/BookStack into Abijeet-master 2017-08-01 19:24:33 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
9126da6299 Updated dev command details
Closes #453
2017-07-28 11:32:42 +01:00
Dan Brown
fea139d8e7 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-07-27 19:09:44 +01:00
Dan Brown
ac7a8a8e1e Expanded the available editor shortcuts in both editors
Adds formatting on ctrl+nums for everything on formats dropdown.
Closes #85.
2017-07-27 19:07:58 +01:00
Dan Brown
bbaa2f4cda Merge pull request #435 from JachuPL/polish-localization
Polish translation
2017-07-27 16:48:19 +01:00
Dan Brown
9d61eecd81 Merge branch 'Cyber-Duck-master' 2017-07-27 16:29:09 +01:00
Dan Brown
21247e10d0 Reverted travis changes and added html escaping 2017-07-27 16:28:23 +01:00
Dan Brown
c1fc06ae34 Merge branch 'master' of git://github.com/Cyber-Duck/BookStack into Cyber-Duck-master 2017-07-27 16:20:38 +01:00
Dan Brown
a0eb3d1079 Merge pull request #446 from Joorem/french-spelling
French spelling
2017-07-27 16:18:57 +01:00
Dan Brown
164aea3a3a Merge pull request #448 from 10bass/subdir-search-fix
Update search.js
2017-07-27 16:17:57 +01:00
Dan Brown
ec83f83017 Added breadcrumbs to pages in entity select
Fixes #391
2017-07-27 16:10:58 +01:00
Dan Brown
5cd08ab2f5 Fixed custom plugin when developing 2017-07-27 15:43:17 +01:00
Dan Brown
072f6b103e Vastly sped up gulp watch and added livereload 2017-07-27 15:14:53 +01:00
10bass
b4dcde252b Update search.js
Trying to apply an exact match or tag would previously redirect to /search, regardless of the installation path.
2017-07-24 20:06:15 -04:00
Jérôme Le Gal
0b2c3c1aa7 settings.php: add missing french translation 2017-07-22 23:45:09 +02:00
Jérôme Le Gal
0dc9d0bed7 errors.php: add missing french translation 2017-07-22 23:45:09 +02:00
Jérôme Le Gal
a2a2e37797 settings.php: fix some spelling issues in french translation 2017-07-22 23:45:09 +02:00
Jérôme Le Gal
3d9819f97c passwords.php: fix some spelling issues in french translation 2017-07-22 23:45:09 +02:00
Jérôme Le Gal
b2b4a24d7c errors.php: fix some spelling issues in french translation 2017-07-22 23:45:08 +02:00
Jérôme Le Gal
7557d6d619 entities.php: fix some spelling issues in french translation 2017-07-22 23:45:08 +02:00
Jérôme Le Gal
57eeb9b0a3 common.php: fix some spelling issues in french translation 2017-07-22 23:45:08 +02:00
Jérôme Le Gal
813c7d5902 auth.php: fix some spelling issues in french translation 2017-07-22 23:45:08 +02:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
f200b4183d Defined LDAP constant for testing without LDAP installed 2017-07-22 17:22:31 +01:00
Dan Brown
33642c20ec Fixed faulty text rendering calls and LDAP tests 2017-07-22 17:10:52 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
2704962277 Updated utfmb4 upgrade command 2017-07-22 16:19:17 +01:00
Dan Brown
6bcd89acf7 Moved utf8mb4 migration to command instead of migration
To prevent errors upon migration.
Command generates out the SQL syntax to make the change instead
so the upgrade can be done manually.

In reference to #425
2017-07-22 15:54:17 +01:00
Dan Brown
433cb9b3b2 Improved breadcrumb responsiveness
Closes #426
2017-07-22 15:20:36 +01:00
Dan Brown
7f43372dd4 Fixed broken code block rendering when using DOMPDF
Fixes #427
2017-07-22 14:34:17 +01:00
Dan Brown
b12e2ceada Added included content into page's text view
Allows rendered content to be shown in listings and used in searches.
Also prevented angular tags in content being parsed in listings.

Fixes #442
2017-07-22 14:21:56 +01:00
Dan Brown
bc067e9ad4 Updated dropdowns to hide after option click
Fixes #429
2017-07-22 14:03:06 +01:00
Clément Blanco
245294fbc5 Trying to make the tests green. 2017-07-17 14:42:08 +01:00
Clément Blanco
f38bc75ab4 Trying to make the tests green. 2017-07-17 14:21:41 +01:00
Clément Blanco
3407900abb Trying to make the tests green. 2017-07-17 14:18:03 +01:00
Clément Blanco
684c20c4ea Trying to make the tests green. 2017-07-17 14:09:21 +01:00
Clément Blanco
6ef522df7e Trying to make the tests green. 2017-07-17 14:05:41 +01:00
Clément Blanco
3b771f2976 Trying to make the tests green. 2017-07-17 14:03:31 +01:00
Clément Blanco
afc56c12fe Trying to make the tests green. 2017-07-17 14:01:10 +01:00
Clément Blanco
5eeed03dcd Trying to make the tests green. 2017-07-17 13:53:02 +01:00
Clément Blanco
0d98b4ce5e Trying to make the tests green. 2017-07-17 13:37:15 +01:00
Clément Blanco
ae2ec43a82 Avoid having to wait until all tests are processed to exit upon error/failure. 2017-07-17 13:35:38 +01:00
Clément Blanco
265ed34ffd Update travis.yml to try and solve the test issue around LDAP. 2017-07-17 13:34:19 +01:00
Clément Blanco
711dcb4a48 Update travis.yml to try and solve the test issue around LDAP. 2017-07-17 13:29:29 +01:00
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
Clément Blanco
67bc7007aa Support new lines for book/chapter descriptions
Avoid ignoring new lines when renderring the book/chapter descriptions on their respective detailed views.
2017-07-14 16:05:46 +01:00
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
JachuPL
9e0c931573 Polish translation 2017-07-13 16:00:42 +02:00
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
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
2ea7e10923 Set ldap to not follow referrals by default
Added LDAP_FOLLOW_REFERRALS .env option to override.
Fixes #317
2017-07-10 19:43:49 +01:00
Dan Brown
e7f8188ee5 Prevented textarea styles interfering with codemirror
Closes #424
2017-07-10 19:29:35 +01:00
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
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
314d98abc3 Removed logs, Updated version, Fixed inconsistent subheader 2017-07-02 20:33:32 +01:00
Dan Brown
b2dfc069c5 Updated readme attribution 2017-07-02 19:38:28 +01:00
Dan Brown
69c50b9b48 Migrated markdown editor actions to new cm editor 2017-07-02 19:35:13 +01:00
Dan Brown
f101e4f010 Fixed quoting db/table names in encoding migration.
Also fixed incorrect if statement in db config.
2017-07-02 17:34:32 +01:00
Dan Brown
005f0eb4fc Updated default encoding and added conversion migration.
Also updated how DB port is defined so that the DB_PORT
env var can be used or it can be take from the host name.

Fixes #405
2017-07-02 17:30:12 +01:00
Dan Brown
7293ad7b24 Merge branch 'S64-japanese-translation' 2017-07-02 16:13:28 +01:00
Dan Brown
c5b58d8fd2 Merge branch 'japanese-translation' of git://github.com/S64/BookStack into S64-japanese-translation 2017-07-02 16:09:12 +01:00
Dan Brown
4db2c274e2 Prevent empty-state actions visible without permission.
Fixes #411
2017-07-02 15:59:40 +01:00
Dan Brown
cbff801aec Added test to cover f99c8ff.
Closes #409
2017-07-02 15:40:42 +01:00
Dan Brown
d5d83da766 Added diff and sh code types
Verified changes to ensure fixes #296.
2017-07-01 16:10:52 +01:00
Dan Brown
de6d8a811c Added quick lang-selection options to code editor 2017-07-01 15:50:28 +01:00
Dan Brown
a94844b6b7 Fixed code block selection and drag/drop issues 2017-07-01 15:38:11 +01:00
Dan Brown
968e7b8b72 Finished off main functionality of custom tinymce code editor 2017-07-01 13:23:46 +01:00
Shuma Yoshioka
5f461c9796 Add ja locale to config 2017-07-01 16:40:06 +09:00
Shuma Yoshioka
b2b8706d05 Translate errors.php 2017-07-01 16:38:43 +09:00
Shuma Yoshioka
2d345fe454 Translate entities.php 2017-07-01 16:38:20 +09:00
Shuma Yoshioka
7bec70d429 Translate common.php 2017-07-01 16:37:07 +09:00
Shuma Yoshioka
60710e7fb4 Translate auth.php 2017-07-01 16:33:54 +09:00
Shuma Yoshioka
f10d3a8564 Translate validation.php 2017-07-01 16:32:36 +09:00
Shuma Yoshioka
b020520366 Translate passwords.php 2017-07-01 16:32:02 +09:00
Shuma Yoshioka
52010e5820 Translate pagination.php 2017-07-01 16:31:32 +09:00
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
Shuma Yoshioka
75eca03b05 Translate settings.php 2017-06-24 20:49:24 +09:00
Shuma Yoshioka
b8ebefd803 Translate components.php 2017-06-24 20:48:34 +09:00
Shuma Yoshioka
168c66815e add ja to settings 2017-06-24 20:46:38 +09:00
Shuma Yoshioka
26e703a1cd translate activities.php 2017-06-19 00:02:09 +09:00
Dan Brown
c8be214ee0 Started tinymce code editor components 2017-06-17 15:07:55 +01:00
Dan Brown
2060cdb931 Added code highlighting syntax modes 2017-06-17 12:41:18 +01:00
Abijeet
574ee820a9 #47 - Fixes the issues with the test case. 2017-06-13 02:37:50 +05:30
Abijeet
7d02f77e67 #47 - Added more test cases to test the APIs and permission for comments. 2017-06-13 02:31:17 +05:30
Abijeet
fd50efb503 #47 - Putting the comments right under the page. 2017-06-11 11:41:33 +05:30
Abijeet
9dbd7fa618 #47 - Adding comments to the dummy content seeder. 2017-06-11 11:40:37 +05:30
Abijeet
8dab31b87a Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-06-10 22:56:07 +05:30
Abijeet
e155c52256 #47 - Fixes a few issues with the code. 2017-06-10 22:55:36 +05:30
Abijeet Patro
c76e7c706c adding a comment on top. 2017-06-10 19:47:45 +05:30
Abijeet
552943c033 #47 - Undos changes in config files. 2017-06-10 19:46:00 +05:30
Abijeet
4efe3b41da #47 - Added translations for other language files using Google translate. 2017-06-10 15:21:28 +05:30
Abijeet
218376a41c #47 - Fetching values from language files. 2017-06-08 01:30:43 +05:30
Abijeet
e647ec22b1 #47 - Adds direct linking to comments. 2017-06-08 01:14:53 +05:30
Abijeet
38fe756725 #47 - Fixes a couple of issues found during testing - delete not updating the UI, delete none not working properly. 2017-06-07 23:45:29 +05:30
Abijeet
652a67ad65 Removes some unncessary code. 2017-06-06 23:20:40 +05:30
Abijeet
5bd9da6054 #47 - Adds various translations in English, and a few code improvements. 2017-06-06 01:46:59 +05:30
Abijeet
7c6fe8c4e2 #47 - Changes the location of the reply and edit comment box. 2017-06-05 00:20:37 +05:30
Abijeet
689d1eb082 #47 - Adds a cancel button for edit and reply button. 2017-06-04 20:43:56 +05:30
Abijeet
06d75e1804 #47 - Updates the total comments when a comment is added. 2017-06-04 20:12:01 +05:30
Dan Brown
0528e98d9c Merge branch 'v0.16' 2017-06-04 15:39:55 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
f99c8ff99a Fixed role permission removal bug 2017-06-04 15:37:10 +01:00
Abijeet
9558f84b97 #47 - Adds functionality to delete a comment. Also reduces the number of watchers. 2017-06-04 18:52:44 +05:30
Abijeet
2fd421b115 #47 - Adds comment level permissions to the front-end. 2017-06-04 11:17:14 +05:30
Abijeet
6ff440e677 Merge branch 'BookStackApp-master' 2017-06-04 10:20:25 +05:30
Abijeet
0bda5554dd Getting the latest changes 2017-06-04 10:20:01 +05:30
Abijeet
860d4d4be5 #47 - Changes the way we are handling fetching of data for the comment section. 2017-05-30 09:02:47 +05:30
Dan Brown
88f93f76dd Updated the markdown editor to use codemirror as editor
Improved scroll sync system to be smarter
2017-05-28 16:02:46 +01:00
Dan Brown
e5fc6bf5fa Moved from highlight.js to codemirror 2017-05-28 13:16:21 +01:00
Dan Brown
8990a9817f Updated tinymce to latest version 2017-05-28 11:57:58 +01:00
Abijeet
9a97995f18 #47 Displays the time for comments and border bottom for sub comments. 2017-05-25 08:04:19 +05:30
Abijeet
1a1e71cd60 #47 Adds two attributes updated and created to display time to user. 2017-05-25 08:03:27 +05:30
Abijeet
34802ff8a6 #47 Inserts null for updated_at when the user is creating a comment. 2017-05-25 08:02:49 +05:30
Abijeet
0ff5aad9c0 #47 Hides the reply button based if comments are 2 levels deep. 2017-05-24 07:02:11 +05:30
Abijeet
03e5d61798 #47 Implements the reply and edit functionality for comments. 2017-05-16 00:40:14 +05:30
Abijeet Patro
4f231d1bf0 Merge pull request #11 from BookStackApp/master
Fixed chapter check for non-mysqlnd instances
2017-05-15 22:25:33 +05:30
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
75981c2412 Fixed chapter check for non-mysqlnd instances
Fixes #383
2017-05-07 19:34:56 +01:00
Abijeet
8b82753218 #47 - Gets rid of simplemde 2017-05-03 02:42:04 +05:30
Abijeet Patro
3368fe42d8 Merge pull request #10 from BookStackApp/master
Latest changes
2017-05-03 01:41:08 +05:30
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f8ae4c335e Made single entity updates more efficent 2017-04-30 19:44:59 +01:00
Dan Brown
5570e858e5 Made more efficiency improvements to permission system 2017-04-30 11:38:58 +01:00
Dan Brown
1859a4d356 Refactored permission system components
Split joint permission creation into chunks

Fixes #374
2017-04-29 22:01:43 +01:00
Dan Brown
ad4642c2c4 Enabled translation when not logged in
Reads from the Accept-Language HTTP header.
Also fixed some encoding for ES translations.

Fixes #375
2017-04-29 16:47:41 +01:00
Dan Brown
92108d710d Re-enabled html in markdown editor
Fixes #378
2017-04-29 16:10:38 +01:00
Abijeet
c3ea0d333e #47 - Adds functionality to display child comments. Also has some code towards the reply functionality. 2017-04-27 02:35:29 +05:30
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
44347ee353 Fixed search system id clash 2017-04-23 20:27:49 +01:00
Dan Brown
9e704fcae4 Updated testing database connection issue 2017-04-23 17:51:01 +01:00
Dan Brown
fdd816b17d Merge pull request #362 from DaneEveritt/patch-1
Queue confirmation emails.
2017-04-23 17:15:06 +01:00
Dan Brown
82e2c523e6 Fixed chapter breadcrumbs and testing issues 2017-04-22 14:08:12 +01:00
Dan Brown
a323b0d49c Allowed child entity permissions to override parent permissions
Updated elements of a page display and sidebar render to allow
child permissions to work even when parent entitites have permission
set. This allows a page with a 'view' permission to be viewable even
when the parent book or chapter is not viewable.

Fixes #366
2017-04-22 13:39:34 +01:00
Dan Brown
4c985aac7e Added page revision counting
Adds stored revision counts to pages and the revisions themselves.
Closes #321
2017-04-20 20:58:54 +01:00
Dan Brown
87e18b8068 Merge pull request #357 from diegoseso/master
Spanish translation completed
2017-04-19 06:40:00 +01:00
Diego Jose Sosa Diaz
607a2c91fc Fixing encoding of files affecting accents. 2017-04-18 23:41:19 +02:00
Abijeet
d447355a61 Adding the view templates and styles. 2017-04-19 01:24:33 +05:30
Abijeet
8e2437498f Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-04-19 01:23:27 +05:30
Abijeet Patro
9de85283cd Merge pull request #9 from BookStackApp/master
Get the latest changes.
2017-04-19 01:23:21 +05:30
Abijeet
b3d4c199ae Merge branch 'master' of https://github.com/Abijeet/BookStack
Conflicts:
	.gitignore
2017-04-19 01:21:45 +05:30
Dan Brown
fde970ba59 Switched out markdown render
Fixes #304.
Fixes #359.
2017-04-17 12:21:10 +01:00
Dan Brown
ec7be1b08b Merge pull request #363 from solidnerd/add-env-for-logging
Add APP_LOGGING
2017-04-17 11:46:32 +01:00
solidnerd
746a760a23 Add APP_LOGGING
This will add an variable for logging types to make it easier to define outside via .env.

Signed-off-by: solidnerd <niclas@mietz.io>
2017-04-17 09:55:11 +02:00
Dan Brown
1a09d88891 Added fade effect to page content highlighting
Closes #314
2017-04-16 16:46:55 +01:00
Dan Brown
46c01ecba2 Merge pull request #358 from jendrol/master
Add Slovak translation
2017-04-16 15:06:32 +01:00
Dan Brown
544ece03a5 Merge pull request #360 from Abijeet/spellcheck-fix
Fixes #354, Adds the spellchecker option
2017-04-16 15:02:20 +01:00
Dan Brown
5fee7c4db1 Merge pull request #340 from BookStackApp/search_system
Implementation of new search system
2017-04-16 11:01:00 +01:00
Dan Brown
8ed9f75d57 Fixed model extending mis-use 2017-04-16 10:54:23 +01:00
Dan Brown
a15b179676 Updated testcases for new search system.
Finishes implementation of new search system.
Closes #271
Closes #344
Fixes #285
Fixes #269
Closes #64
2017-04-16 10:47:44 +01:00
Dan Brown
73844b9eeb Enabled type search filter in book search 2017-04-15 19:31:11 +01:00
Dan Brown
dcde599709 Added chapter search
Migrated book search to vue-based system.
Updated old tag seached.
Made chapter page layout widths same as book page.
Closes #344
2017-04-15 19:16:07 +01:00
Dan Brown
0e0945ef84 Finished off UI for search system 2017-04-15 15:04:30 +01:00
Dan Brown
ad125327c0 Migrated to custom gulp setup and conintue search interface 2017-04-14 18:47:33 +01:00
Dane Everitt
dfaf20dd83 Actually include the Queueable namespace... 2017-04-13 20:09:38 -04:00
Dane Everitt
786262db3b Queue confirmation emails.
Implements Laravel's queue abilities into the email notification job. Should not affect existing installations that are not using queues as the environment file defaults to `sync`.
2017-04-13 20:03:03 -04:00
Abijeet
29a4110d8f Fixes #354, Adds the spellchecker option
Uses the browser_spellchecker option documented here - https://www.tinymce.com/docs/configure/spelling/#browser_spellcheck
2017-04-13 23:57:57 +05:30
Vlado Jendroľ
9b639f715f Add Slovak translation 2017-04-11 23:20:52 +02:00
Dan Brown
46f3d78c8a Fixed entity type filter bug in new search system 2017-04-09 21:12:13 +01:00
Dan Brown
1338ae2fc3 Started search interface, Added in vue and moved fonts 2017-04-09 20:59:57 +01:00
Dan Brown
37813a223a Improved DB prefix support and removed old search method 2017-04-09 14:44:56 +01:00
Diego Jose Sosa Diaz
b488e969bb Reaching 100 % Spanish translation 2017-04-06 23:12:05 +02:00
Diego Jose Sosa Diaz
1377296ef4 Translating to spanish entities errors and settings 2017-04-06 09:35:24 +02:00
Dan Brown
01cb22af37 Added tag searches and advanced filters to new search 2017-03-27 18:05:34 +01:00
Dan Brown
331305333d Added search term parsing and exact term matches 2017-03-27 11:57:33 +01:00
Dan Brown
0651eae7ec Improve efficiency of single entity search indexing 2017-03-26 19:34:53 +01:00
Dan Brown
1552417598 Developed basic search queries.
Updated search & permission regen commands with ability to specify
database.
2017-03-26 19:24:57 +01:00
Abijeet Patro
4e71a5a47b Merge pull request #8 from BookStackApp/master
Getting the latest changes.
2017-03-25 23:56:05 +05:30
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
cc0ce7c630 Fixed bug preventing page revision restore
Added regression tests to cover.
Fixes #341
2017-03-23 22:19:14 +00:00
Dan Brown
070d4aeb6c Started implementation of new search system 2017-03-19 12:48:44 +00:00
Dan Brown
668ce26269 Fixed back button behaviour on books edit
As reported in #339
2017-03-19 08:32:04 +00:00
Abijeet
410e967eb1 Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-02-05 16:46:32 +05:30
Abijeet Patro
388f2f40dc Merge pull request #7 from BookStackApp/master
Getting the latest.
2017-02-05 16:46:03 +05:30
Abijeet
148350009c #47 Adds comment permission to each role. 2017-01-29 14:25:20 +05:30
Abijeet
70991fc1e5 Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-01-29 09:35:46 +05:30
Abijeet Patro
e5c4e0ac86 Merge pull request #6 from BookStackApp/master
Getting the latest
2017-01-29 09:35:21 +05:30
Abijeet
397db04428 Added comments controller, model, repo, and the database schema. Modified existing Page model to associate with comments. 2017-01-13 21:45:48 +05:30
Abijeet Patro
cd6572b61a Merge pull request #3 from BookStackApp/master
Getting the latest
2017-01-03 07:54:28 +05:30
Abijeet
581881d0ca Merging gitignore. 2016-11-29 00:24:15 +05:30
Abijeet Patro
d2efc2f47f Merge pull request #2 from BookStackApp/master
Getting the latest
2016-11-29 00:23:30 +05:30
Abijeet Patro
4cc73657a1 Merge pull request #1 from ssddanbrown/master
Getting the latest of BookStack.
2016-09-25 13:29:22 +05:30
593 changed files with 41793 additions and 9859 deletions

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,16 @@ 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
# External services such as Gravatar
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
@@ -64,4 +74,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

View File

@@ -4,10 +4,18 @@ Desired Feature:
### For Bug Reports
* BookStack Version:
* BookStack Version *(Found in settings, Please don't put 'latest')*:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Actual Behavior
##### Current Behavior
##### Steps to Reproduce

12
.gitignore vendored
View File

@@ -2,22 +2,22 @@
/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
/public/bower
/public/build/
/storage/images
_ide_helper.php
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin
nbproject
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs
.settings/

View File

@@ -2,7 +2,8 @@ dist: trusty
sudo: false
language: php
php:
- 7.0
- 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
@@ -25,4 +25,4 @@ after_failure:
- cat storage/logs/laravel.log
script:
- phpunit
- phpunit

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;
}
@@ -32,5 +33,4 @@ class Attachment extends Ownable
{
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
@@ -56,4 +86,12 @@ class Book extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
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,8 +1,9 @@
<?php namespace BookStack;
class Chapter extends Entity
{
public $searchFactor = 1.3;
protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $with = ['book'];
@@ -51,4 +52,12 @@ class Chapter extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
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";
}
}

43
app/Comment.php Normal file
View File

@@ -0,0 +1,43 @@
<?php namespace BookStack;
class Comment extends Ownable
{
protected $fillable = ['text', 'html', 'parent_id'];
protected $appends = ['created', 'updated'];
/**
* Get the entity that this comment belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
return $this->morphTo('entity');
}
/**
* Check if a comment has been updated since creation.
* @return bool
*/
public function isUpdated()
{
return $this->updated_at->timestamp > $this->created_at->timestamp;
}
/**
* Get created date as a relative diff.
* @return mixed
*/
public function getCreatedAttribute()
{
return $this->created_at->diffForHumans();
}
/**
* Get updated date as a relative diff.
* @return mixed
*/
public function getUpdatedAttribute()
{
return $this->updated_at->diffForHumans();
}
}

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

@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
*
* @var string
*/
protected $signature = 'bookstack:regenerate-permissions';
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
/**
* The console command description.
@@ -46,7 +46,15 @@ class RegeneratePermissions extends Command
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->permissionService->setConnection(\DB::connection($this->option('database')));
}
$this->permissionService->buildJointPermissions();
\DB::setDefaultConnection($connection);
$this->comment('Permissions regenerated');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\SearchService;
use Illuminate\Console\Command;
class RegenerateSearch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Re-index all content for searching';
protected $searchService;
/**
* Create a new command instance.
*
* @param SearchService $searchService
*/
public function __construct(SearchService $searchService)
{
parent::__construct();
$this->searchService = $searchService;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(\DB::connection($this->option('database')));
}
$this->searchService->indexAllEntities();
\DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class UpgradeDatabaseEncoding extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:db-utf8mb4 {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4';
/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
}
$database = DB::getDatabaseName();
$tables = DB::select('SHOW TABLES');
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
$this->line('USE `'.$database.'`;');
$key = 'Tables_in_' . $database;
foreach ($tables as $table) {
$tableName = $table->$key;
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
}
DB::setDefaultConnection($connection);
}
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Console;
<?php namespace BookStack\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -13,10 +11,7 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\ClearViews::class,
\BookStack\Console\Commands\ClearActivity::class,
\BookStack\Console\Commands\ClearRevisions::class,
\BookStack\Console\Commands\RegeneratePermissions::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,10 +1,19 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable
{
protected $fieldsToSearch = ['name', 'description'];
/**
* @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.
@@ -26,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;
@@ -65,6 +76,26 @@ class Entity extends Ownable
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the comments for an entity
* @param bool $orderByCreated
* @return MorphMany
*/
public function comments($orderByCreated = true)
{
$query = $this->morphMany(Comment::class, 'entity');
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
}
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function searchTerms()
{
return $this->morphMany(SearchTerm::class, 'entity');
}
/**
* Get this entities restrictions.
*/
@@ -85,17 +116,6 @@ class Entity extends Ownable
->where('action', '=', $action)->count() > 0;
}
/**
* Check if this entity has live (active) restrictions in place.
* @param $role_id
* @param $action
* @return bool
*/
public function hasActiveRestriction($role_id, $action)
{
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
}
/**
* Get the entity jointPermissions this is connected to.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@@ -148,72 +168,38 @@ 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) . '...';
}
/**
* Perform a full-text search on this entity.
* @param string[] $fieldsToSearch
* @param string[] $terms
* @param string[] array $wheres
* Get the body text of this entity.
* @return mixed
*/
public function fullTextSearchQuery($terms, $wheres = [])
public function getText()
{
$exactTerms = [];
$fuzzyTerms = [];
$search = static::newQuery();
return $this->{$this->textField};
}
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
} else {
$term = '' . $term . '*';
if ($term !== '*') $fuzzyTerms[] = $term;
}
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return '';
}
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
// Perform fulltext search if relevant terms exist.
if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
}
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) {
foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if ($this->isA('chapter')) {
$search = $search->with('book');
}
return $search->orderBy($orderBy, 'desc');
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path)
{
return '/';
}
}

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

@@ -4,11 +4,14 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Validation\ValidationException;
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 +29,10 @@ 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
*/
public function report(Exception $e)
{
@@ -60,18 +63,44 @@ 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 $this->loadErrorMiddleware($request, function ($request) use ($e) {
$message = $e->getMessage() ?: trans('errors.404_page_not_found');
return response()->view('errors/404', ['message' => $message], 404);
});
}
return parent::render($request, $e);
}
/**
* Load the middleware required to show state/session-enabled error pages.
* @param Request $request
* @param $callback
* @return mixed
*/
protected function loadErrorMiddleware(Request $request, $callback)
{
$middleware = (\Route::getMiddlewareGroups()['web_errors']);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then($callback);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
* @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 +110,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 +133,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;
@@ -182,11 +183,16 @@ 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
*/
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 +210,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

@@ -70,20 +70,21 @@ class LoginController extends Controller
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) {
@@ -102,12 +103,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]);
}
@@ -121,4 +131,4 @@ class LoginController extends Controller
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}
}

View File

@@ -8,6 +8,7 @@ use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount;
use BookStack\User;
use Exception;
use Illuminate\Http\Request;
@@ -52,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;
@@ -90,6 +91,7 @@ class RegisterController extends Controller
/**
* Show the application registration form.
* @return Response
* @throws UserRegistrationException
*/
public function getRegister()
{
@@ -101,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\Foundation\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);
@@ -140,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)
{
@@ -230,7 +224,6 @@ class RegisterController extends Controller
return redirect('/register/confirm');
}
$this->emailConfirmationService->sendConfirmation($user);
session()->flash('success', trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
@@ -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,21 +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')) {
$action = session()->pull('social-callback');
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
} elseif ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
}
} else {
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);
}
return redirect()->back();
}
@@ -283,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)
{
@@ -297,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

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Book;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
@@ -35,11 +36,19 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 10);
$books = $this->entityRepo->getAllPaginated('book', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$this->setPageTitle('Books');
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$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,
'booksViewType' => $booksViewType
]);
}
/**
@@ -83,7 +92,12 @@ class BookController extends Controller
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
$this->setPageTitle($book->getShortName());
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
return view('books/show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'activity' => Activity::entityActivity($book, 20, 0)
]);
}
/**
@@ -95,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]);
}
@@ -113,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());
}
/**
@@ -141,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]);
}
@@ -171,48 +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) {
$updatedBook = $this->entityRepo->getById('book', $bookId);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
// 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

@@ -159,7 +159,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 +178,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

@@ -0,0 +1,93 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\CommentRepo;
use BookStack\Repos\EntityRepo;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
class CommentController extends Controller
{
protected $entityRepo;
protected $commentRepo;
/**
* CommentController constructor.
* @param EntityRepo $entityRepo
* @param CommentRepo $commentRepo
*/
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
{
$this->entityRepo = $entityRepo;
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Save a new comment for a Page
* @param Request $request
* @param integer $pageId
* @param null|integer $commentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function savePageComment(Request $request, $pageId, $commentId = null)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
try {
$page = $this->entityRepo->getById('page', $pageId, true);
} catch (ModelNotFoundException $e) {
return response('Not found', 404);
}
$this->checkOwnablePermission('page-view', $page);
// Prevent adding comments to draft pages
if ($page->draft) {
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
}
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments/comment', ['comment' => $comment]);
}
/**
* Update an existing comment.
* @param Request $request
* @param integer $commentId
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function update(Request $request, $commentId)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
return view('comments/comment', ['comment' => $comment]);
}
/**
* Delete a comment from the system.
* @param integer $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('comment-delete', $comment);
$this->commentRepo->delete($comment);
return response()->json(['message' => trans('entities.comment_deleted')]);
}
}

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;
@@ -29,24 +30,36 @@ class HomeController extends Controller
$activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [
$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);
}
$view = $customHomepage ? 'home-custom' : 'home';
return view($view, [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages
'draftPages' => $draftPages,
'customHomepage' => $customHomepage
]);
}
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function getTranslations() {
$locale = trans()->getLocale();
public function getTranslations()
{
$locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
@@ -76,4 +89,33 @@ class HomeController extends Controller
]);
}
/**
* Get an icon via image request.
* Can provide a 'color' parameter with hex value to color the icon.
* @param $iconName
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getIcon($iconName, Request $request)
{
$attrs = [];
if ($request->filled('color')) {
$attrs['fill'] = '#' . $request->get('color');
}
$icon = icon($iconName, $attrs);
return response($icon, 200, [
'Content-Type' => 'image/svg+xml',
'Cache-Control' => 'max-age=3600',
]);
}
/**
* 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');
}
}

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,90 @@ 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);
}
/**
* Replace the data content of a drawing.
* @param string $id
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function replaceDrawing(string $id, Request $request)
{
$this->validate($request, [
'image' => 'required|string'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-update', $image);
try {
$image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data);
} 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 +214,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 +230,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)
{
@@ -162,7 +257,7 @@ class ImageController extends Controller
$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);
$isForced = in_array($request->get('force', ''), [true, 'true']);
if (!$isForced) {
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {
@@ -173,6 +268,4 @@ class ImageController extends Controller
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@@ -145,6 +145,7 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @throws NotFoundException
*/
public function show($bookSlug, $pageSlug)
{
@@ -152,22 +153,33 @@ 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());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = $this->entityRepo->renderPage($page);
$page->html = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
$pageNav = $this->entityRepo->getPageNav($page->html);
// 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, 'pageContent' => $pageContent]);
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
]);
}
/**
@@ -210,7 +222,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', [
@@ -323,9 +337,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());
}
@@ -376,10 +391,11 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
'revision' => $revision
]);
}
@@ -409,6 +425,7 @@ class PageController extends Controller
'page' => $page,
'book' => $page->book,
'diff' => $diff,
'revision' => $revision
]);
}
@@ -438,6 +455,7 @@ class PageController extends Controller
public function exportPdf($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
@@ -454,6 +472,7 @@ class PageController extends Controller
public function exportHtml($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
@@ -589,5 +608,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]);
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\SearchService;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
@@ -8,16 +9,19 @@ class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
/**
* SearchController constructor.
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
parent::__construct();
}
@@ -27,84 +31,25 @@ class SearchController extends Controller
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchAll(Request $request)
public function search(Request $request)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'hasNextPage' => $results['has_more'],
'nextPageLink' => $nextPageLink
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $pages,
'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the books in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchBooks(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $books,
'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm
]);
}
/**
* Searches all entities within a book.
@@ -115,16 +60,24 @@ class SearchController extends Controller
*/
public function searchBook(Request $request, $bookId)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities.
@@ -134,18 +87,13 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entities = collect();
$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);
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains($entityType)) {
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
@@ -155,7 +103,4 @@ class SearchController extends Controller
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

@@ -33,7 +33,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);
}
@@ -41,5 +43,4 @@ class SettingController extends Controller
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}
}

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,10 +27,20 @@ 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
],
'web_errors' => [
\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,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',
'bindings',
@@ -42,10 +53,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

@@ -15,7 +15,19 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
$locale = setting()->getUser(user(), 'language', $defaultLang);
if (user()->isDefault()) {
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) {
continue;
}
$locale = $lang;
break;
}
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);

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

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

View File

@@ -2,12 +2,16 @@
namespace BookStack\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification
class ConfirmEmail extends Notification implements ShouldQueue
{
use Queueable;
public $token;
/**
@@ -45,5 +49,4 @@ class ConfirmEmail extends Notification
->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,6 +1,5 @@
<?php namespace BookStack;
class Page extends Entity
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
@@ -8,8 +7,7 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
protected $fieldsToSearch = ['name', 'text'];
public $textField = 'text';
/**
* Converts this page into a simplified array.
@@ -96,4 +94,14 @@ class Page extends Entity
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent
* @return string
*/
public function entityRawQuery($withContent = false)
{
$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;
}
@@ -47,4 +48,15 @@ class PageRevision extends Model
return null;
}
/**
* Allows checking of the exact class, Used to check entity type.
* Included here to align with entities in similar use cases.
* (Yup, Bit of an awkward hack)
* @param $type
* @return bool
*/
public static function isA($type)
{
return $type === 'revision';
}
}

View File

@@ -15,14 +15,17 @@ 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); ?>";
});
// Allow longer string lengths after upgrade to utf8mb4
\Schema::defaultStringLength(191);
}
/**
@@ -32,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

@@ -34,28 +34,28 @@ 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(ImageManager::class),
$this->app->make(Factory::class),

View File

@@ -16,6 +16,10 @@ 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',
],
];

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;
});
}
}
}

90
app/Repos/CommentRepo.php Normal file
View File

@@ -0,0 +1,90 @@
<?php namespace BookStack\Repos;
use BookStack\Comment;
use BookStack\Entity;
/**
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo
{
/**
* @var Comment $comment
*/
protected $comment;
/**
* CommentRepo constructor.
* @param Comment $comment
*/
public function __construct(Comment $comment)
{
$this->comment = $comment;
}
/**
* Get a comment by ID.
* @param $id
* @return Comment|\Illuminate\Database\Eloquent\Model
*/
public function getById($id)
{
return $this->comment->newQuery()->findOrFail($id);
}
/**
* Create a new comment on an entity.
* @param Entity $entity
* @param array $data
* @return Comment
*/
public function create(Entity $entity, $data = [])
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$entity->comments()->save($comment);
return $comment;
}
/**
* Update an existing comment.
* @param Comment $comment
* @param array $input
* @return mixed
*/
public function update($comment, $input)
{
$comment->updated_by = user()->id;
$comment->update($input);
return $comment;
}
/**
* Delete a comment from the system.
* @param Comment $comment
* @return mixed
*/
public function delete($comment)
{
return $comment->delete();
}
/**
* Get the next local ID relative to the linked entity.
* @param Entity $entity
* @return int
*/
protected function getNextLocalId(Entity $entity)
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) {
return 1;
}
return $comments->local_id + 1;
}
}

View File

@@ -4,10 +4,12 @@ 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;
use BookStack\Services\PermissionService;
use BookStack\Services\SearchService;
use BookStack\Services\ViewService;
use Carbon\Carbon;
use DOMDocument;
@@ -59,13 +61,12 @@ class EntityRepo
protected $tagRepo;
/**
* Acceptable operators to be used in a query
* @var array
* @var SearchService
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
protected $searchService;
/**
* EntityService constructor.
* EntityRepo constructor.
* @param Book $book
* @param Chapter $chapter
* @param Page $page
@@ -73,12 +74,18 @@ class EntityRepo
* @param ViewService $viewService
* @param PermissionService $permissionService
* @param TagRepo $tagRepo
* @param SearchService $searchService
*/
public function __construct(
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo
)
{
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;
@@ -91,6 +98,7 @@ class EntityRepo
$this->viewService = $viewService;
$this->permissionService = $permissionService;
$this->tagRepo = $tagRepo;
$this->searchService = $searchService;
}
/**
@@ -109,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);
}
@@ -134,10 +142,15 @@ class EntityRepo
* @param string $type
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Entity
*/
public function getById($type, $id, $allowDrafts = false)
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
if ($ignorePermissions) {
$entity = $this->getEntity($type);
return $entity->newQuery()->find($id);
}
return $this->entityQuery($type, $allowDrafts)->find($id);
}
@@ -154,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;
}
@@ -187,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();
}
@@ -216,12 +234,15 @@ class EntityRepo
* @param int $count
* @param int $page
* @param bool|callable $additionalQuery
* @return Collection
*/
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
{
$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);
}
@@ -234,12 +255,15 @@ class EntityRepo
* @param int $count
* @param int $page
* @param bool|callable $additionalQuery
* @return Collection
*/
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
{
$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);
}
@@ -327,7 +351,7 @@ class EntityRepo
if ($rawEntity->entity_type === 'BookStack\\Page') {
$entities[$index] = $this->page->newFromBuilder($rawEntity);
if ($renderPages) {
$entities[$index]->html = $rawEntity->description;
$entities[$index]->html = $rawEntity->html;
$entities[$index]->html = $this->renderPage($entities[$index]);
};
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
@@ -336,13 +360,21 @@ 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;
continue;
}
$chapter = $parents[$parentKey];
$chapter->pages->push($entity);
}
@@ -354,6 +386,7 @@ class EntityRepo
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Chapter $chapter
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getChapterChildren(Chapter $chapter)
{
@@ -361,56 +394,6 @@ class EntityRepo
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
}
/**
* Search entities of a type via a given query.
* @param string $type
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
$q = $this->addAdvancedSearchQueries($q, $term);
$entities = $q->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
// Highlight page content
if ($type === 'page') {
//lookahead/behind assertions ensures cut between words
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
foreach ($entities as $page) {
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
//delimiter between occurrences
$results = [];
foreach ($matches as $line) {
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
}
$matchLimit = 6;
if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
$result = join('... ', $results);
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
if (strlen($result) < 5) $result = $page->getExcerpt(80);
$page->searchSnippet = $result;
}
return $entities;
}
// Highlight chapter/book content
foreach ($entities as $entity) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
$entity->searchSnippet = $result;
}
return $entities;
}
/**
* Get the next sequential priority for a new child element in the given book.
@@ -465,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;
}
@@ -476,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([
@@ -488,108 +474,12 @@ class EntityRepo
}
}
}
$entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
}
/**
* Prepare a string of search terms by turning
* it into an array of terms.
* Keeps quoted terms together.
* @param $termString
* @return array
*/
public function prepareSearchTerms($termString)
{
$termString = $this->cleanSearchTermString($termString);
preg_match_all('/(".*?")/', $termString, $matches);
$terms = [];
if (count($matches[1]) > 0) {
foreach ($matches[1] as $match) {
$terms[] = $match;
}
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
}
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
return $terms;
}
/**
* Removes any special search notation that should not
* be used in a full-text search.
* @param $termString
* @return mixed
*/
protected function cleanSearchTermString($termString)
{
// Strip tag searches
$termString = preg_replace('/\[.*?\]/', '', $termString);
// Reduced multiple spacing into single spacing
$termString = preg_replace("/\s{2,}/", " ", $termString);
return $termString;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
$escapedOperators[] = preg_quote($operator);
}
return join('|', $escapedOperators);
}
/**
* Parses advanced search notations and adds them to the db query.
* @param $query
* @param $termString
* @return mixed
*/
protected function addAdvancedSearchQueries($query, $termString)
{
$escapedOperators = $this->getRegexEscapedOperators();
// Look for tag searches
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
if (count($tags[0]) > 0) {
$this->applyTagSearches($query, $tags);
}
return $query;
}
/**
* Apply extracted tag search terms onto a entity query.
* @param $query
* @param $tags
* @return mixed
*/
protected function applyTagSearches($query, $tags) {
$query->where(function($query) use ($tags) {
foreach ($tags[1] as $index => $tagName) {
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
$tagOperator = $tags[3][$index];
$tagValue = $tags[4][$index];
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
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
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
} else {
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
}
} else {
$query->where('name', '=', $tagName);
}
});
}
});
return $query;
}
/**
* Create a new entity from request input.
@@ -608,12 +498,13 @@ class EntityRepo
$entity->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entity) : $entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
$this->searchService->indexEntity($entity);
return $entity;
}
/**
* Update entity details from request input.
* Use for books and chapters
* Used for books and chapters
* @param string $type
* @param Entity $entityModel
* @param array $input
@@ -628,6 +519,7 @@ class EntityRepo
$entityModel->updated_by = user()->id;
$entityModel->save();
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
@@ -668,11 +560,11 @@ class EntityRepo
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities
* @param Book $book
*/
public function buildJointPermissions(Collection $collection)
public function buildJointPermissionsForBook(Book $book)
{
$this->permissionService->buildJointPermissionsForEntities($collection);
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
@@ -682,9 +574,12 @@ 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;
}
@@ -706,12 +601,13 @@ class EntityRepo
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->text = $this->pageToPlainText($draftPage);
$draftPage->draft = false;
$draftPage->revision_count = 1;
$draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
$this->searchService->indexEntity($draftPage);
return $draftPage;
}
@@ -724,7 +620,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;
@@ -732,6 +630,7 @@ class EntityRepo
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->revision_number = $page->revision_count;
$revision->save();
// Clear old revisions
@@ -751,7 +650,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'));
@@ -765,7 +666,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')) {
@@ -804,41 +707,56 @@ class EntityRepo
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @param bool $ignorePermissions
* @return mixed|string
*/
public function renderPage(Page $page)
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;
}
$page = $this->getById('page', $pageId);
if ($page === null) {
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $page->html, $content);
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
continue;
}
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
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);
}
@@ -846,6 +764,35 @@ 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
* @return string
*/
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page);
return strip_tags($html);
}
/**
* Get a new draft page instance.
* @param Book $book
@@ -860,9 +807,12 @@ class EntityRepo
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
@@ -890,14 +840,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) {
@@ -913,7 +867,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;
});
@@ -948,9 +902,12 @@ class EntityRepo
$userId = user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
// Remove all update drafts for this user & page.
@@ -961,6 +918,8 @@ class EntityRepo
$this->savePageRevision($page, $input['summary']);
}
$this->searchService->indexEntity($page);
return $page;
}
@@ -1008,7 +967,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');
}
@@ -1057,13 +1018,15 @@ class EntityRepo
*/
public function restorePageRevision(Page $page, Book $book, $revisionId)
{
$page->revision_count++;
$this->savePageRevision($page);
$revision = $this->getById('page_revision', $revisionId);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
$page->text = strip_tags($page->html);
$page->text = $this->pageToPlainText($page);
$page->updated_by = user()->id;
$page->save();
$this->searchService->indexEntity($page);
return $page;
}
@@ -1080,7 +1043,7 @@ class EntityRepo
if ($page->draft) {
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
$page->text = $this->pageToPlainText($page);
}
$page->save();
return $page;
@@ -1102,7 +1065,9 @@ class EntityRepo
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
$draft->save();
return $draft;
@@ -1156,6 +1121,7 @@ class EntityRepo
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$this->searchService->deleteEntityTerms($book);
$book->delete();
}
@@ -1175,12 +1141,14 @@ class EntityRepo
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$this->searchService->deleteEntityTerms($chapter);
$chapter->delete();
}
/**
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
*/
public function destroyPage(Page $page)
{
@@ -1190,6 +1158,13 @@ class EntityRepo
$page->revisions()->delete();
$page->permissions()->delete();
$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);
@@ -1199,17 +1174,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,39 @@ 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;
}
/**
* Replace the image content of a drawing.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function replaceDrawingContent(Image $image, string $base64Uri)
{
return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri);
}
/**
* 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)
{
@@ -170,6 +197,8 @@ class ImageRepo
/**
* Load thumbnails onto an image object.
* @param Image $image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
private function loadThumbs(Image $image)
{
@@ -183,22 +212,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 = ['drawing', '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

@@ -33,6 +33,7 @@ class TagRepo
* @param $entityType
* @param $entityId
* @param string $action
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
@@ -51,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;
}
@@ -94,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');
@@ -111,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);
}
@@ -131,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::destroyImage($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,6 +1,5 @@
<?php namespace BookStack;
class Role extends Model
{
@@ -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');
}
/**

17
app/SearchTerm.php Normal file
View File

@@ -0,0 +1,17 @@
<?php namespace BookStack;
class SearchTerm extends Model
{
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
public $timestamps = false;
/**
* Get the entity that this term belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
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

@@ -27,9 +27,9 @@ class ExportService
*/
public function pageToContainedHtml(Page $page)
{
$this->entityRepo->renderPage($page);
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
'page' => $page
])->render();
return $this->containHtml($pageHtml);
}
@@ -42,7 +42,7 @@ class ExportService
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', [
@@ -74,9 +74,9 @@ class ExportService
*/
public function pageToPdf(Page $page)
{
$this->entityRepo->renderPage($page);
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
'page' => $page
])->render();
return $this->htmlToPdf($html);
}
@@ -89,7 +89,7 @@ class ExportService
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', [
@@ -127,7 +127,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 +136,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)
{
@@ -153,9 +154,31 @@ class ExportService
} else {
$pathString = $srcString;
}
if ($isLocal && !file_exists($pathString)) continue;
// Attempt to find local files even if url not absolute
$base = baseUrl('/');
if (strpos($srcString, $base) === 0) {
$isLocal = true;
$relString = str_replace($base, '', $srcString);
$pathString = public_path(trim($relString, '/'));
}
if ($isLocal && !file_exists($pathString)) {
continue;
}
try {
$imageContent = file_get_contents($pathString);
if ($isLocal) {
$imageContent = file_get_contents($pathString);
} else {
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageContent = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
}
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {
@@ -238,17 +261,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

@@ -31,6 +31,23 @@ class ImageService extends UploadService
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 +63,49 @@ 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);
}
/**
* Replace the data for an image via a Base64 encoded string.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws ImageUploadException
*/
public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
$storage = $this->getStorage();
try {
$storage->put($image->path, $data);
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path]));
}
return $image;
}
/**
* Gets an image from url and saves it to the database.
@@ -59,7 +119,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,16 +136,16 @@ 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;
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;
}
@@ -96,8 +158,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,8 +172,8 @@ class ImageService extends UploadService
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
$image = (new Image());
$image->forceFill($imageDetails)->save();
return $image;
}
@@ -124,14 +184,22 @@ class ImageService extends UploadService
*/
protected function getPath(Image $image)
{
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
return $image->path;
}
/**
* Checks if the image is a gif. Returns true if it is, else false.
* @param Image $image
* @return boolean
*/
protected function isGif(Image $image) {
return strtolower(pathinfo($this->getPath($image), 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,6 +210,10 @@ class ImageService extends UploadService
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
if ($keepRatio && $this->isGif($image)) {
return $this->getPublicUrl($this->getPath($image));
}
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
$imagePath = $this->getPath($image);
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
@@ -150,8 +222,7 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath);
}
$storage = $this->getStorage();
$storage = $this->getStorage($image->type);
if ($storage->exists($thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
}
@@ -161,9 +232,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) {
@@ -183,10 +253,24 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath);
}
/**
* Get the raw data content from an image.
* @param Image $image
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getImageData(Image $image)
{
$imagePath = $this->getPath($image);
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/**
* Destroys an Image object along with its files and thumbnails.
* @param Image $image
* @return bool
* @throws Exception
*/
public function destroyImage(Image $image)
{
@@ -205,9 +289,13 @@ class ImageService extends UploadService
// Cleanup of empty folders
foreach ($storage->directories($imageFolder) as $directory) {
if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
if ($this->isFolderEmpty($directory)) {
$storage->deleteDirectory($directory);
}
}
if ($this->isFolderEmpty($imageFolder)) {
$storage->deleteDirectory($imageFolder);
}
if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
$image->delete();
return true;
@@ -216,8 +304,9 @@ class ImageService extends UploadService
/**
* 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)
{
@@ -252,14 +341,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,6 +1,5 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
@@ -42,8 +41,12 @@ class LdapService
$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;
if ($users['count'] === 0) {
return null;
}
$user = $users[0];
return [
@@ -64,8 +67,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 {
@@ -95,7 +102,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')));
}
}
/**
@@ -106,7 +115,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') {
@@ -116,7 +127,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);
@@ -149,5 +162,4 @@ class LdapService
}
return strtr($filterString, $newAttrs);
}
}
}

View File

@@ -3,6 +3,7 @@
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\EntityPermission;
use BookStack\JointPermission;
use BookStack\Ownable;
use BookStack\Page;
@@ -10,6 +11,7 @@ use BookStack\Role;
use BookStack\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
class PermissionService
@@ -28,22 +30,25 @@ class PermissionService
protected $jointPermission;
protected $role;
protected $entityPermission;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Connection $db
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->book = $book;
$this->chapter = $chapter;
@@ -51,6 +56,15 @@ class PermissionService
// TODO - Update so admin still goes through filters
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Prepare the local entity cache and ensure it's empty
*/
@@ -74,7 +88,9 @@ class PermissionService
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if ($book === null) {
$book = false;
}
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
}
@@ -94,7 +110,9 @@ class PermissionService
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if ($chapter === null) {
$chapter = false;
}
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
}
@@ -108,7 +126,9 @@ class PermissionService
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
if ($this->userRoles !== false) {
return $this->userRoles;
}
$roles = [];
@@ -133,22 +153,51 @@ class PermissionService
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get();
$roles = $this->role->with('permissions')->get()->all();
// Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
$this->createManyJointPermissions($chapters, $roles);
});
/**
* Get a query for fetching a book with it's children.
* @return QueryBuilder
*/
protected function bookFetchQuery()
{
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
$this->createManyJointPermissions($pages, $roles);
});
/**
* Build joint permissions for an array of books
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities, $roles);
}
/**
@@ -157,18 +206,27 @@ class PermissionService
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$roles = $this->role->get();
$entities = collect([$entity]);
$entities = [$entity];
if ($entity->isA('book')) {
$entities = $entities->merge($entity->chapters);
$entities = $entities->merge($entity->pages);
} elseif ($entity->isA('chapter')) {
$entities = $entities->merge($entity->pages);
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
return;
}
$entities[] = $entity->book;
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->createManyJointPermissions($entities, $roles);
$this->buildJointPermissionsForEntities(collect($entities));
}
/**
@@ -177,8 +235,8 @@ class PermissionService
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->get();
$this->deleteManyJointPermissionsForEntities($entities);
$roles = $this->role->newQuery()->get();
$this->deleteManyJointPermissionsForEntities($entities->all());
$this->createManyJointPermissions($entities, $roles);
}
@@ -188,23 +246,12 @@ class PermissionService
*/
public function buildJointPermissionForRole(Role $role)
{
$roles = collect([$role]);
$roles = [$role];
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
@@ -223,9 +270,10 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
foreach ($roles as $role) {
$role->jointPermissions()->delete();
}
$roleIds = array_map(function ($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
}
/**
@@ -243,54 +291,94 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
if (count($entities) === 0) {
return;
}
$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->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
$query->delete();
});
}
/**
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param Collection $roles
* @param array $roles
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
$entityRestrictedMap = [];
$permissionFetch = $this->entityPermission->newQuery();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function ($query) use ($entity) {
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
});
}
$permissions = $permissionFetch->get();
// Create a mapping of explicit entity permissions
$permissionMap = [];
foreach ($permissions as $permission) {
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
$permissionMap[$key] = $isRestricted;
}
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->getRelationValue('permissions') as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
// Create Joint Permission Data
foreach ($entities as $entity) {
foreach ($roles as $role) {
foreach ($this->getActions($entity) as $action) {
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
}
}
}
$this->jointPermission->insert($jointPermissions);
$this->db->transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
}
});
}
/**
* Get the actions related to an entity.
* @param $entity
* @param Entity $entity
* @return array
*/
protected function getActions($entity)
protected function getActions(Entity $entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter')) {
$baseActions[] = 'page-create';
} else if ($entity->isA('book')) {
if ($entity->isA('chapter') || $entity->isA('book')) {
$baseActions[] = 'page-create';
}
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
return $baseActions;
return $baseActions;
}
/**
@@ -298,14 +386,16 @@ class PermissionService
* for a particular action.
* @param Entity $entity
* @param Role $role
* @param $action
* @param string $action
* @param array $permissionMap
* @param array $rolePermissionMap
* @return array
*/
protected function createJointPermissionData(Entity $entity, Role $role, $action)
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
{
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
$explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction);
@@ -313,54 +403,50 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
}
if ($entity->isA('book')) {
if (!$entity->restricted) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
$chapter = $this->getChapter($entity->chapter_id);
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
$acknowledgeChapter = ($chapter && $chapter->restricted);
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
} else {
$hasAccess = $entity->hasRestriction($role->id, $action);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
if ($entity->restricted) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
if ($entity->isA('book')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
// For chapters and pages, Check if explicit permissions are set on the Book.
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
$hasPermissiveAccessToParents = !$book->restricted;
// For pages with a chapter, Check if explicit permissions are set on the Chapter
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
$chapter = $this->getChapter($entity->chapter_id);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
if ($chapter->restricted) {
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
}
}
return $this->createJointPermissionDataArray(
$entity,
$role,
$action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
}
/**
* Check for an active restriction in an entity map.
* @param $entityMap
* @param Entity $entity
* @param Role $role
* @param $action
* @return bool
*/
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;
}
/**
@@ -375,11 +461,10 @@ class PermissionService
*/
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
{
$entityClass = get_class($entity);
return [
'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entityClass,
'entity_type' => $entity->getMorphClass(),
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
@@ -406,7 +491,7 @@ class PermissionService
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
@@ -476,19 +561,19 @@ class PermissionService
* @param integer $book_id
* @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder
* @return QueryBuilder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageContentSelect = $fetchPageContent ? 'html' : "''";
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->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);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
@@ -496,8 +581,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);
});
});
@@ -514,7 +599,7 @@ class PermissionService
* @param string $entityType
* @param Builder|Entity $query
* @param string $action
* @return mixed
* @return Builder
*/
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{
@@ -540,7 +625,7 @@ class PermissionService
}
/**
* Filter items that have entities set a a polymorphic relation.
* Filter items that have entities set as a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
@@ -649,5 +734,4 @@ class PermissionService
$this->userRoles = false;
$this->isAdminUser = null;
}
}
}

View File

@@ -0,0 +1,550 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\SearchTerm;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
class SearchService
{
protected $searchTerm;
protected $book;
protected $chapter;
protected $page;
protected $db;
protected $permissionService;
protected $entities;
/**
* Acceptable operators to be used in a query
* @var array
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
/**
* SearchService constructor.
* @param SearchTerm $searchTerm
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Connection $db
* @param PermissionService $permissionService
*/
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
{
$this->searchTerm = $searchTerm;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->db = $db;
$this->entities = [
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
];
$this->permissionService = $permissionService;
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Search all entities in the system.
* @param string $searchString
* @param string $entityType
* @param int $page
* @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)
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
$entityTypesToSearch = $entityTypes;
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
} else if (isset($terms['filters']['type'])) {
$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);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, 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()
];
}
/**
* Search a book for entities
* @param integer $bookId
* @param string $searchString
* @return Collection
*/
public function searchBook($bookId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$entityTypes = ['page', 'chapter'];
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
return $results->sortByDesc('score')->take(20);
}
/**
* Search a book for entities
* @param integer $chapterId
* @param string $searchString
* @return Collection
*/
public function searchChapter($chapterId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/**
* Search across a particular entity type.
* @param array $terms
* @param string $entityType
* @param int $page
* @param int $count
* @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)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
if ($getCount) {
return $query->count();
}
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
}
/**
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page')
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
// Handle normal search terms
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) {
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) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
}
// Handle exact term matching
if (count($terms['exact']) > 0) {
$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 .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
});
}
});
}
// Handle tag searches
foreach ($terms['tags'] as $inputTerm) {
$this->applyTagSearch($entitySelect, $inputTerm);
}
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
$functionName = camel_case('filter_' . $filterTerm);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
}
/**
* Parse a search string into components.
* @param $searchString
* @return array
*/
protected function parseSearchString($searchString)
{
$terms = [
'search' => [],
'exact' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exact' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') {
$terms['search'][] = $searchTerm;
}
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
$escapedOperators[] = preg_quote($operator);
}
return join('|', $escapedOperators);
}
/**
* Apply a tag search term onto a entity query.
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $tagTerm
* @return mixed
*/
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) {
$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 (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
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
$query->whereRaw("value ${tagOperator} ${tagValue}");
} else {
$query->where('value', $tagOperator, $tagValue);
}
} else {
$query->where('name', '=', $tagName);
}
});
return $query;
}
/**
* Get an entity instance via type.
* @param $type
* @return Entity
*/
protected function getEntity($type)
{
return $this->entities[strtolower($type)];
}
/**
* Index the given entity.
* @param Entity $entity
*/
public function indexEntity(Entity $entity)
{
$this->deleteEntityTerms($entity);
$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();
$terms[$index]['entity_id'] = $entity->id;
}
$this->searchTerm->newQuery()->insert($terms);
}
/**
* Index multiple Entities at once
* @param Entity[] $entities
*/
protected function indexEntities($entities)
{
$terms = [];
foreach ($entities as $entity) {
$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();
$terms[] = $term;
}
}
$chunkedTerms = array_chunk($terms, 500);
foreach ($chunkedTerms as $termChunk) {
$this->searchTerm->newQuery()->insert($termChunk);
}
}
/**
* Delete and re-index the terms for all entities in the system.
*/
public function indexAllEntities()
{
$this->searchTerm->truncate();
// Chunk through all books
$this->book->chunk(1000, function ($books) {
$this->indexEntities($books);
});
// Chunk through all chapters
$this->chapter->chunk(1000, function ($chapters) {
$this->indexEntities($chapters);
});
// Chunk through all pages
$this->page->chunk(1000, function ($pages) {
$this->indexEntities($pages);
});
}
/**
* Delete related Entity search terms.
* @param Entity $entity
*/
public function deleteEntityTerms(Entity $entity)
{
$entity->searchTerms()->delete();
}
/**
* Create a scored term array from the given text.
* @param $text
* @param float|int $scoreAdjustment
* @return array
*/
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) {
$tokenMap[$token] = 0;
}
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
foreach ($tokenMap as $token => $count) {
$terms[] = [
'term' => $token,
'score' => $count * $scoreAdjustment
];
}
return $terms;
}
/**
* Custom entity search filters
*/
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
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;
}
$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;
}
$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;
}
$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;
}
$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;
}
$query->where('updated_by', '=', $input);
}
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->where('name', 'like', '%' .$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)
{
$query->where($model->textField, 'like', '%' .$input. '%');
}
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->where('restricted', '=', true);
}
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$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->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'];
/**
* 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();
@@ -104,7 +105,8 @@ class SocialAuthService
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
return $this->logUserIn($socialAccount->user);
auth()->login($socialAccount->user);
return redirect()->intended('/');
}
// When a user is logged in but the social account does not exist,
@@ -134,14 +136,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
}
private function logUserIn($user)
{
auth()->login($user);
return redirect('/');
throw new SocialSignInException($message, '/login');
}
/**
@@ -155,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;
}
@@ -225,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) {
@@ -62,7 +64,7 @@ class ViewService
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', get_class($filterModel));
};
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}
@@ -77,12 +79,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 +103,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';

View File

@@ -1,31 +1,38 @@
{
"name": "ssddanbrown/bookstack",
"name": "bookstackapp/bookstack",
"description": "BookStack documentation platform",
"keywords": ["BookStack", "Documentation"],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.4.*",
"php": ">=7.0.0",
"laravel/framework": "5.5.*",
"fideloper/proxy": "~3.3",
"ext-tidy": "*",
"intervention/image": "^2.3",
"intervention/image": "^2.4",
"laravel/socialite": "^3.0",
"barryvdh/laravel-ide-helper": "^2.2.3",
"barryvdh/laravel-debugbar": "^2.3.2",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0",
"socialiteproviders/slack": "^3.0"
"barryvdh/laravel-snappy": "^0.4.0",
"socialiteproviders/slack": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
"socialiteproviders/gitlab": "^3.0",
"socialiteproviders/twitch": "^3.0"
},
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.0",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0",
"symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "3.1.*"
"symfony/dom-crawler": "3.1.*",
"laravel/browser-kit-testing": "^2.0",
"barryvdh/laravel-ide-helper": "^2.4.1",
"barryvdh/laravel-debugbar": "^3.1.0",
"squizlabs/php_codesniffer": "^3.2"
},
"autoload": {
"classmap": [
@@ -56,17 +63,23 @@
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize",
"php artisan cache:clear",
"php artisan view:clear"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"refresh-test-database": [
"php artisan migrate:refresh --database=mysql_testing",
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
]
},
"config": {
"preferred-install": "dist"
"optimize-autoloader": true,
"preferred-install": "dist",
"platform": {
"php": "7.0"
}
}
}

6023
composer.lock generated

File diff suppressed because it is too large Load Diff

18
config/app.php Normal file → Executable file
View File

@@ -2,10 +2,13 @@
return [
'env' => env('APP_ENV', 'production'),
'editor' => env('APP_EDITOR', 'html'),
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list')
],
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
/*
|--------------------------------------------------------------------------
@@ -58,6 +61,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN'],
/*
|--------------------------------------------------------------------------
@@ -100,7 +104,7 @@ return [
|
*/
'log' => 'single',
'log' => env('APP_LOGGING', 'single'),
/*
|--------------------------------------------------------------------------
@@ -146,8 +150,6 @@ return [
*/
Intervention\Image\ImageServiceProvider::class,
Barryvdh\DomPDF\ServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,
Barryvdh\Snappy\ServiceProvider::class,
@@ -162,7 +164,6 @@ return [
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class,
],
/*
@@ -218,9 +219,8 @@ return [
*/
'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'DomPDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
/**
* Custom
@@ -233,4 +233,6 @@ return [
],
'proxies' => env('APP_PROXIES', ''),
];

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