Compare commits

...

231 Commits

Author SHA1 Message Date
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
11960d9d3a Cleaned page form JS & spaced tag box
As per #174
2016-09-05 19:33:14 +01:00
Dan Brown
bbd8fff021 Fixed bad image base-urls and forced tinyMCE to use absolute
Also ensured image file existance is checked during base64 conversion
during exports.
Closes #171.
2016-09-03 19:24:58 +01:00
Dan Brown
ec17bd8608 Improved Exception handling, Removed npm requirement for testing 2016-09-03 12:08:58 +01:00
Dan Brown
0dbb8babee Merge branch 'page_link_selector' 2016-09-03 10:40:27 +01:00
Dan Brown
b14e9fc619 Fixed some cross browser flexbox popup issues
Set min height for poor IE & safari flexbox support.
Fixes #105.
2016-09-03 10:32:14 +01:00
Dan Brown
63c6d3478d Added image paste and drop to markdown editor
Only currently tested in chrome.
Closes #128
2016-09-02 21:18:48 +01:00
Dan Brown
781f0e7887 Added draft save indicator and fixed notification positions 2016-09-02 19:26:12 +01:00
Dan Brown
23e014cb25 Added link selector to markdown editor 2016-09-02 18:54:26 +01:00
Dan Brown
5b64358ef1 Added link selector interface to WYSIWYG editor 2016-09-01 20:36:22 +01:00
Dan Brown
56df64063d Updated popup design and started integrating link selector
Extracted design and base styles from image manager into generic popup
classes that can be used by other app components such as the new
popup Book/Chapter/Page link selector
2016-08-30 20:05:59 +01:00
Dan Brown
3f81eba13b Updated travis testing to work with new helper configuration (#175)
* Updated travis to call phpunit globally rather then booting application first
2016-08-27 11:27:23 +01:00
Dan Brown
7973412c29 Improved sort efficiency by a factor of 10
Fixes #145
2016-08-26 20:20:58 +01:00
Dan Brown
f83de5f834 Fixed single word quoted search terms
Fixes #170
2016-08-25 17:17:26 +01:00
Dan Brown
f2ceba978a Removed animation from page content.
Prevents issues with browsers that do not support
animaton-fill-direction.
Fixes #173.
2016-08-25 16:47:25 +01:00
Dan Brown
96c074bb56 Merge fixes from branch 'v0.11' 2016-08-21 15:02:37 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
f8a299caee Fixed login 'intended' redirect for custom urls.
Also changed social account detach wording.
2016-08-21 14:49:40 +01:00
Chris
437dce7756 Applied baseUrl to login redirect 2016-08-21 13:48:56 +01:00
Dan Brown
f8ad820281 Merge pull request #166 from poppahorse/patch-1
Applied baseUrl to login redirect
2016-08-21 13:44:36 +01:00
Dan Brown
757f16a3b5 Improved page tag box structure and fixed footer in PDF export.
Fixes #162.
2016-08-21 13:35:22 +01:00
Chris
632ecc668f Applied baseUrl to login redirect 2016-08-15 15:07:45 +01:00
Dan Brown
92d393537c Merge branch 'v0.11' 2016-08-14 13:09:44 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
43d9d2eba7 Updated all application urls to allow path prefix.
Allows BookStack to be installed at a non-root location on a domain.
Closes #40.
2016-08-14 12:29:35 +01:00
Dan Brown
baa260a03d Started work on subdirectory support 2016-08-13 17:56:25 +01:00
Dan Brown
b157a9927a Fixed tag section and editor safari rendering.
Fixes #152.
2016-08-13 14:54:23 +01:00
Dan Brown
b246a67e8a Fixed double brace issues in both editors.
Double braces were being parsed by angular in both the WYSIWYG and markdown editor.
Fixes #150 and fixes #155.
2016-08-13 14:18:31 +01:00
Dan Brown
2d958e88bf Fixed entities created with blank slugs.
Fixes #156.
2016-08-13 13:53:04 +01:00
Dan Brown
07c7d5af17 Updated elixr and fixed table th element borders
Fixes #164.
2016-08-13 10:02:54 +01:00
Dan Brown
42976ca48c Fixed revision-based redirect on new pages 2016-07-26 18:16:40 +01:00
Dan Brown
d05e85efa9 Fixed back-to-top button on firefox. Fixes #153. 2016-07-26 18:03:10 +01:00
Dan Brown
547e117760 Changed issue template to use md extension 2016-07-26 17:46:09 +01:00
Nick Walke
50a5d3c546 Added issue template 2016-07-21 10:04:06 -05:00
Dan Brown
3fd82200cc Added WYSIWYG editor shortcuts 2016-07-12 21:11:48 +01:00
Dan Brown
7215392784 Changed when revisions are saved and update changelog input
Revisions are now saved when te page content is originally saved whereas before they were saved on the next update to the page.
2016-07-10 12:12:52 +01:00
Dan Brown
8a9a8dfae5 Merge branch 'summary' of git://github.com/younes0/BookStack into younes0-summary 2016-07-10 10:42:47 +01:00
Dan Brown
c44314def3 Added check for s3 bucket name to choose shortest url 2016-07-10 10:28:05 +01:00
Dan Brown
8b899a9cf0 Merge branch 'patch-1' of git://github.com/younes0/BookStack into younes0-patch-1 2016-07-10 10:15:38 +01:00
Dan Brown
0ebdfa4825 Merge pull request #144 from younes0/patch-2
set uploaded images visibility public (relevant for S3 storage)
2016-07-10 10:14:19 +01:00
Younes El Biache
32a06f119b set uploaded files public visibliity (relevant for S3 storage) 2016-07-09 15:26:53 +02:00
Younes El Biache
ec30864ce5 shorter amazon S3 url 2016-07-09 14:33:37 +02:00
Younès EL BIACHE
6bc72e157a edit summary 2016-07-07 20:53:43 +02:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
10418323ef Prevented shift-tab from selecting a tag 2016-07-03 10:47:20 +01:00
Dan Brown
a11d5245ec Cut down homepage queries a little 2016-07-03 10:42:13 +01:00
Dan Brown
565033e0d4 Fixed bug which hid entities and fixed new chapter priority 2016-07-03 10:31:20 +01:00
Dan Brown
c25ef18900 Made list default messages a little nicer 2016-07-03 10:12:12 +01:00
Dan Brown
b0a63ba0cc Tightened and cleaned some list styles 2016-07-03 09:58:45 +01:00
Dan Brown
7b6c88f17c Fixed error on image deletion
Also Added tests to cover image upload and deletion.
Fixes #136.
2016-07-01 20:13:30 +01:00
Dan Brown
361ba8b244 Made stick book navigation recalc on window resize 2016-06-25 15:46:31 +01:00
Dan Brown
9baa96d41c Added chapter move actions. Closes #86 2016-06-25 15:31:38 +01:00
Dan Brown
e584b4926f Travis CI updates (#130)
* Updated travis config to use mysql

* Added cache for composer
2016-06-19 19:30:43 +01:00
Dan Brown
bcd9c2044e Added WYSIWYG editor callouts 2016-06-19 19:02:53 +01:00
Dan Brown
d582e76fed Fixed theme color elements not showing on new instance
Also cleaned notification session access
2016-06-12 13:37:15 +01:00
Dan Brown
991dd8a558 Merged branch add_page_move into master
References #86
2016-06-12 12:49:08 +01:00
Dan Brown
bc49784797 Added tests to cover page_move features 2016-06-12 12:48:06 +01:00
Dan Brown
7f99903fdb Finished off page move functionality 2016-06-12 12:14:14 +01:00
Dan Brown
97d011ac8e Started work on page move view and entity selector 2016-06-11 21:04:18 +01:00
Dan Brown
61596a8e21 Prevent page redirect on draft saving
Only an issue when using the non 'mysqlnd' extension.
Fixes #120
2016-06-10 19:45:53 +01:00
Dan Brown
44e337cef6 Merged branch rovox-master into master 2016-06-04 16:33:23 +01:00
Dan Brown
1bec3eaa1e Added checks to use MyISAM if MySQL 5.5 is found 2016-06-04 16:32:57 +01:00
Dan Brown
5942d796b5 Merge branch 'master' of git://github.com/rovox/BookStack into rovox-master 2016-06-04 15:43:08 +01:00
Dan Brown
eec9c05518 Added tag autosuggestion when no input provided
Shows the most popular tag names/values.
As requested on #121
2016-06-04 15:37:28 +01:00
Dan Brown
246d1621f5 Limited tag value autosuggestions based on tag name
As requested on #121
2016-06-04 14:54:31 +01:00
Dan Brown
6c1e06bf86 Updated License file 2016-06-03 18:52:49 +01:00
Dan Brown
3c1e165134 Add in LICENSE file 2016-06-03 18:51:47 +01:00
Dan Brown
80d1c594cc Added licence and release badges 2016-06-03 18:50:20 +01:00
Dan Brown
947db95d16 Fixed error with similar activity filtering 2016-05-28 14:02:48 +01:00
Dan Brown
5b9362ab0b Added (Ctrl+s) draft force save 2016-05-28 13:51:07 +01:00
Dan Brown
f602b088ac Added basic markdown scroll syncing 2016-05-28 13:24:07 +01:00
robert
4acf0c4ee0 Making sure MyISAM is set for the tables that need it for new installtions that are using mariadb. 2016-05-25 23:52:43 +02:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
9d3f329bc9 Fixed missing column drop on migration rollback 2016-05-22 14:56:26 +01:00
Dan Brown
bd00a03e7b Added support for custom content in HTML head 2016-05-22 11:01:21 +01:00
Dan Brown
be517de7dc Added pagination, sorting & searching to users list
As requested on #113
2016-05-22 10:44:31 +01:00
Dan Brown
23ab1f0c81 Standardised delete aesthetics regarding roles and users
Also added user delete success message.
Fixes #114
2016-05-22 09:23:41 +01:00
Dan Brown
49621e7b15 Fixed flexbox overflow scrolling issues in firefox
Fixes #111
2016-05-22 09:08:37 +01:00
Dan Brown
1ab6f017c9 Made user names clickable at the bottom of assets
Fixes #117
2016-05-22 08:48:40 +01:00
Dan Brown
0b364fd72f Merge pull request #110 from ssddanbrown/page_attributes
Attribute System. Closes #48.
2016-05-15 20:24:57 +01:00
Dan Brown
e80ae76856 Added auto-suggestions to tag names and values 2016-05-15 20:12:53 +01:00
Dan Brown
eebad3e2a0 Fixed tags not being saved on new pages 2016-05-15 14:17:25 +01:00
Dan Brown
db2af47286 Revert travis CI changes 2016-05-15 13:59:16 +01:00
Dan Brown
7ad28aeab4 Updated travis file to hopefully fix CI errors 2016-05-15 13:53:42 +01:00
Dan Brown
8d80e7311c Added tag searching to search interfaces 2016-05-15 13:41:18 +01:00
Dan Brown
7932069535 Added tags to page display and simplified editing flow
Tags now save with the page content data
2016-05-15 10:39:17 +01:00
Dan Brown
78564ec61d Cleaned up tag edit interface 2016-05-14 20:02:00 +01:00
Dan Brown
b80184cd93 Renamed attribute to tags & continued interface
Also fixed page create route broken in last commit
2016-05-13 21:20:21 +01:00
Dan Brown
1fa079b466 Started the page attributes interface 2016-05-12 23:12:05 +01:00
Dan Brown
fcfb9470c9 Added further attribute endpoints and added tests 2016-05-07 14:29:43 +01:00
Dan Brown
c99653f0f2 Fixed bad refactor in the permission service 2016-05-06 20:44:07 +01:00
Dan Brown
5080b4996e Started base work on attribute system 2016-05-06 20:33:08 +01:00
Dan Brown
1903422113 Merge branch 'v0.9' 2016-05-03 21:30:55 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
4e8722661f Added bookstack version in settings
Gets the app version via git. Closes #99
2016-05-03 21:10:05 +01:00
Dan Brown
6f9d2939e7 Updated travisCI to help fix node error 2016-05-02 13:14:35 +01:00
Dan Brown
3234510d31 Merged ci_testing into master 2016-05-02 13:01:46 +01:00
Dan Brown
a40af08018 Changed mariadb version 2016-05-02 12:50:17 +01:00
Dan Brown
223a6b546c Added dependancy caching and travis badge 2016-05-02 12:37:58 +01:00
Dan Brown
0d5da2d9d4 Added travis CI file 2016-05-02 12:26:09 +01:00
Dan Brown
37337b8b1d Added codeship CI badge 2016-05-02 12:02:23 +01:00
Dan Brown
ffcc058927 Merge pull request #106 from ssddanbrown/ci_testing
Ci testing
2016-05-02 11:58:12 +01:00
Dan Brown
3a1cda5802 Updated ldap so extension not required in testing 2016-05-02 11:38:07 +01:00
Dan Brown
5c1015d6fc Updated social testing compatibility 2016-05-02 11:26:47 +01:00
Dan Brown
3385ec3f78 Updated database config to be codeship compatible 2016-05-02 10:35:42 +01:00
Dan Brown
1b46c19849 Merge pull request #103 from ssddanbrown/add_role_view_permissions
Overhauled permission system and stripped migrations of most app code to reduce future breakages. Closes #101.
2016-05-01 22:47:22 +01:00
Dan Brown
75a4fc905b Major permission naming refactor and database migration cleanup 2016-05-01 22:33:56 +01:00
Dan Brown
05666efda9 Added hidden public role to fit with new permissions system 2016-05-01 19:36:53 +01:00
Dan Brown
59367b3417 Improved permission regen performance by factor of 4
Worked around slower eloquent access to speed up permission generation.
2016-04-30 17:16:06 +01:00
Dan Brown
9a31b83b2a Worked around create permission quirks 2016-04-26 21:48:17 +01:00
Dan Brown
a81a56706e Rolled out new permissions system throughout application 2016-04-24 16:54:20 +01:00
Dan Brown
9dc1b35e82 Made settings ui a little less jumpy 2016-04-24 10:28:47 +01:00
Dan Brown
2dc1180a41 Added custom, More consistent hr insertion
Since the previous hr plugin caused large empty p tags
2016-04-23 19:15:49 +01:00
Dan Brown
ada7c83e96 Continued with database work for permissions overhaul
Added to the entity_permissions table with further required fields and indexes.
Wrote the code for checking permissions.
2016-04-23 18:14:26 +01:00
Dan Brown
ea287ebf86 Started creation of intermediate permission table 2016-04-20 21:37:57 +01:00
Dan Brown
043cdeafb3 Merge branch 'master' into add_role_view_permissions 2016-04-20 20:22:30 +01:00
Dan Brown
8b36ca95a3 Improved font quality and cyrillic support
Fixes dodgy font rendering on windows
2016-04-20 19:11:33 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
8933179017 Prevented drafts from showing up in a book sort
Added tests to cover regresssion.
In reference to #100.
2016-04-15 19:51:27 +01:00
Dan Brown
792e786880 Added lists plugin to TinyMCE, Fixed strange nested list behaviour
Fixes #97
2016-04-14 22:34:33 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
0a8030306e Fixed redis config not being set if not using redis 2016-04-12 20:08:49 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
d6bad01130 Fixed draft time display, Cleaned up some code
Cleaned up some comment spacing in book controller and refactored some of the view service functions.
2016-04-09 14:26:42 +01:00
Dan Brown
a33deed26b Added user listing to role edit screen 2016-04-09 13:57:38 +01:00
Dan Brown
2e7345f4f0 Localised draft save time display
In reference to #83.
2016-04-09 13:36:32 +01:00
Dan Brown
6e03078de3 Started work towards adding role view permissions
Work halted as re-write required.
In reference to #92
2016-04-09 12:40:07 +01:00
Dan Brown
1a7de4c2d6 Fixed and cleaned some permisison/role based components 2016-04-09 12:37:58 +01:00
Dan Brown
66ba773367 Updated composer dependancies 2016-04-09 10:56:10 +01:00
Dan Brown
afc3583be8 Made new pages in chapters have a better inital priority 2016-04-07 19:03:00 +01:00
Dan Brown
cbff2c6035 Added uploaded to book/page filters & search in image manager
Also refactored tab styles which affected the settings area.

Closes #41
2016-04-03 14:59:54 +01:00
Dan Brown
8e614ecb6e Updated tests to match recent email confirmation changes 2016-04-03 12:34:10 +01:00
Dan Brown
d099885fd1 Fixed some label casing and fixed incorrect notifications
Fixed the book & chapter permission update notification stating the 'page permissions' have been updated.
2016-04-03 12:19:44 +01:00
Dan Brown
2bb8c3d914 Made email confirmations work with LDAP auth
The email_confirmed user field now actually indicates if an email is confirmed rather than defaulting to true if not checked.
 This ensures toggleing the 'Require email confirmation' setting actually makes all currently unconfirmed users confirm thier emails.
2016-04-03 12:16:54 +01:00
Dan Brown
4caa61fe96 Added a friendlier error for LDAP new user mismatches 2016-04-03 11:16:49 +01:00
Dan Brown
c5960f9b6a Added Redis cache/session support 2016-04-03 11:00:14 +01:00
Dan Brown
412eed19c3 Removed old input checks on entity permission checkboxes
Old input check potentialy causing issues (#89) and is not needed on the pages which it shows.
2016-04-03 10:23:16 +01:00
Dan Brown
e9b596d3bc Merge bugfixes from branch 'v0.8' 2016-03-30 21:49:25 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
8b109bac13 Trimmed long names in header
Fixes #87
2016-03-30 21:28:38 +01:00
Dan Brown
097d9c9f3c Updated entity restrictions to allow permissions, Not just restrict
Also changed wording from 'Restrictions' to 'Permissions' to keep things more familiar and to better reflect what they do.

Referenced in issue #89.
2016-03-30 20:15:44 +01:00
Dan Brown
e7d8a041a8 Merge pull request #84 from ssddanbrown/markdown_editor
Initial implementation of a markdown editor. Closes #57.
2016-03-29 20:18:11 +01:00
Dan Brown
dc2978824e Added basic system tests for markdown editor, Added extra test helpers
Added test helpers for checking if an element exists / does not exist on a page.
Also fixed markdown editor bugs found while creating tests.
2016-03-29 20:13:23 +01:00
Dan Brown
e1994ef2cf Added editor control in admin settings & Fixed some markdown editor bugs
Also updated the setting system with a more sane approach to handling default values. (Now done via the setting-defaults config file)
2016-03-29 19:26:13 +01:00
Dan Brown
efb49019d4 Integrated the markdown editor with the image manager 2016-03-29 18:25:54 +01:00
Dan Brown
ef874712bb Cut readme down and added useful links
Remove a lot of the instructions/config info since much of it is now on the BookStack docs site.
2016-03-25 15:17:04 +00:00
Dan Brown
26965fa08f Added a markdown editor 2016-03-25 14:41:15 +00:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
491f73e0cd Fixed bug causing permission error on save and fixed non-gallery image save 2016-03-13 15:37:46 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
4656c12f6d Fixed bug causing editing notification to always show
Updated tests to prevent happening again
2016-03-13 14:33:43 +00:00
Dan Brown
a06321675a Forced browser scrollbar, Added custom theme compatible logo
Also removed link to search all pages if no pages mached search on global search page.
Closes #74.
2016-03-13 14:00:24 +00:00
Dan Brown
dbe11c1360 Attached images to pages and added restriction filtering
Closes #79
2016-03-13 13:30:47 +00:00
Dan Brown
75ecf1c44d Fixed inset chapter list colors
The colors were being overridden by the custom theme color.
Styles added to force color to page and/or page draft.
2016-03-13 12:43:42 +00:00
Dan Brown
5283919d24 Added new page drafts and started image entity attaching
Closes #80.
2016-03-13 12:04:08 +00:00
Dan Brown
ced8c8e497 Merged branch autosaving_drafts into master 2016-03-12 16:31:37 +00:00
Dan Brown
bf7852ce85 Organised test files & added page update draft tests
Also cleaned styling for new autosave ui parts.
Closes #36.
2016-03-12 16:31:02 +00:00
Dan Brown
30214fde74 Added UI components of page autosaving 2016-03-12 15:52:19 +00:00
Dan Brown
e9c213f803 Fixed firefox list editing bug and made button text size more consistent
Fixes #77
2016-03-09 23:23:28 +00:00
Dan Brown
9f11e045a5 Updated tinyMCE library from 4.3.2 to 4.3.7 2016-03-09 23:20:47 +00:00
Dan Brown
93ebdf724b Changed direct attributes to prevent conflicts 2016-03-09 22:54:18 +00:00
Dan Brown
59ce228c2e Moved page editing to angular controller and started work on update drafts 2016-03-09 22:32:07 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
1d6137f7e2 Added restrictions to user profile lists 2016-03-06 13:17:46 +00:00
Dan Brown
66c56e9d02 Added settings helper and formatted code in some files 2016-03-06 12:55:08 +00:00
Dan Brown
e744d4c82c Changed color picker library and moved color logic to front end
Since the old library was GPLv3 i changed the color picker to tiny-color-picker which is MIT.
Also extracted the styles to a shared view and move color calculation logic to javascript side.
2016-03-06 10:52:10 +00:00
Dan Brown
0774ecc89c Merge branch 'master' into nwalke-update_site_color 2016-03-06 09:08:20 +00:00
Dan Brown
5e7a4c7fb5 Fixed incorrect recents pages on homescreen
Fixed the bug causing the recently updated pages to be exaclty the same as the recently create pages.
Also added in tests to prevent regression.
2016-03-05 22:54:53 +00:00
Dan Brown
76eaf64f94 Fixed errors that occured when merging & refactored entity repositories
Also deleted the git '.orig' files that got added in last merge.
2016-03-05 19:00:26 +00:00
Dan Brown
80865b30a5 Merge branch 'custom_role_system'
Conflicts:
	app/Repos/BookRepo.php
	app/Repos/ChapterRepo.php
	app/Repos/PageRepo.php
2016-03-05 18:21:44 +00:00
Dan Brown
8e6248f57f Added restriction tests and fixed any bugs in the process
Also updated many styles within areas affected by the new permission and roles system.
2016-03-05 18:09:21 +00:00
Dan Brown
268db6b1d0 Added a whole load of permission & role tests 2016-03-05 12:09:09 +00:00
Dan Brown
479dd80a8c Made memcached config allow mulitple servers 2016-03-05 09:47:24 +00:00
Dan Brown
069431db72 Merge branch 'add_memcached' of git://github.com/nwalke/BookStack into nwalke-add_memcached 2016-03-05 09:05:27 +00:00
Dan Brown
bc2b310638 Updated fulltext search with custom escaped query
Changed pdo quoted query to use custom escaping and added like searches when quoted terms are used.
2016-03-04 20:49:35 +00:00
Nick Walke
33bf20cfc8 Found the source of the issue, not sure how to fix 2016-03-04 02:33:57 -06:00
Nick Walke
e3bdc391cd Closes #55. Allows you to set the primary color.
This certainly should not be seen as the final implementation of this.
I imagine that requests like this will happen for many other colors in
the system, it might be good at some point to allow them to upload their
own stylesheet to be used.
2016-03-03 12:01:14 -06:00
Nick Walke
5681f4dd69 Closes #70.
Added the ability to search by multi-word terms using double quotes.
2016-03-02 21:38:23 -06:00
Nick Walke
38d822e04c Closes #69. Implemented and tested memcached. 2016-03-02 20:44:00 -06:00
Dan Brown
8e274a5a84 Refactored some permission controls and increased testing for roles system 2016-03-02 22:35:01 +00:00
Dan Brown
985d2f1c2c Tied entity restriction system into userCan checks 2016-02-29 20:31:21 +00:00
Dan Brown
7f5872372d Added in restriction queries for most lists 2016-02-28 19:03:13 +00:00
Dan Brown
201f788806 Implemented database structure and inital interfaces for entity restrictions 2016-02-28 10:49:41 +00:00
Dan Brown
a14b5c33fd Added missing permission checkboxes and improved image AJAX permission responses 2016-02-27 20:52:46 +00:00
Dan Brown
473261be35 Finished initial implementation of custom role system 2016-02-27 19:24:42 +00:00
Dan Brown
a54be85185 Started work on exposing the role system as editable 2016-02-26 23:44:02 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
54e3122540 Added smarter page finding so changing the page name does not break old urls
Added page & book slug history to revisions so they can be looked up if a page is not found.
2016-02-25 20:01:59 +00:00
Dan Brown
d339ab1125 Updated phpunit testing mail settings due to laravel 5.2 changes 2016-02-22 21:28:53 +00:00
Dan Brown
3ab09ef708 Fixed issue with the book sort not showing all books in sidebar 2016-02-22 21:28:20 +00:00
Dan Brown
c86a122d80 Added table header row styling, made code blocks horizontally scrollbable fixed link popup on firefox 2016-02-22 20:18:08 +00:00
Dan Brown
3a58e37838 Updated phpunit environment variables with some required defaults 2016-02-22 19:39:51 +00:00
Dan Brown
6bd49bcd4b Fixed page listing excerpts not showing due to encoding issues 2016-02-21 13:15:46 +00:00
Dan Brown
61577cf6bf Added entity-specific search results pages. Cleaned & Fixed search results bugs
Added search result pages for pages, chapters and books.
Limited the results on the global search as it just listed out an infinate amount.
Fixed styling on new detailed page listings and also removed the 'bars' from the side to create  a cleaner view.
Fixed bad sql fulltext query format that may have thrown off searches.
Reduced the number of database queries down a thousand or so.
2016-02-21 12:53:58 +00:00
Dan Brown
b4dec2a99c Made page anchor hashes more relevant to the page content
This will help when adding support for new kinds of page content such as markdown as we won't be able to reference the same ID's as before thus they would break on every save.
2016-02-21 11:29:46 +00:00
Dan Brown
fe0b122aca Merge pull request #63 from AkibaWolf/patch-1
Fixes encoding issues the page HTML is formatted on save
2016-02-20 19:24:32 +00:00
Dan Brown
8eb2960950 Added recently created & updated page listings
Closes #46.
2016-02-20 18:51:01 +00:00
AkibaWolf
c2369a740d Update PageRepo.php
Fix encoding problem.

By default DOMDocument::loadHTML treats a string as being encoded with ISO-8859-1. This causes a problem with saving cyrillic pages' text that becomes completely unreadable (like Проверка instead of normal symbols).
2016-02-20 21:31:21 +05:00
Dan Brown
bab6fd1f2f Added recent pages to home view and made the home content more compact 2016-02-20 12:37:06 +00:00
Dan Brown
86fbc9a936 Added tests for profile pages 2016-02-18 19:32:07 +00:00
Dan Brown
4d9726dbdd Added content to user profile pages 2016-02-17 22:11:48 +00:00
Dan Brown
4442a2e6d1 Started work on user profile pages 2016-02-16 21:25:11 +00:00
Dan Brown
293be7093c Merged origin/master into master 2016-02-11 22:36:49 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
354912a1df Made book-navigation sidebar on pages sticky 2016-02-11 22:23:19 +00:00
Nick Walke
eacff3a9f0 Fixes #45 2016-02-11 14:02:17 -06:00
Dan Brown
990acbb9ac Merge pull request #59 from nwalke/master
Updated email on users list to be a link
2016-02-11 18:53:12 +00:00
Nick Walke
17d4533e45 Fixes #58 2016-02-11 01:18:01 -06:00
Dan Brown
d6c00a85ad Fixed incorrect notification when deleting a page 2016-02-10 12:48:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
e0279f93f9 Added a back-to-top button on all pages
The new back-to-top button will show after scrolling a short distance down a long page.
Closes #44.
2016-02-08 20:42:41 +00:00
Dan Brown
9b83c57316 Fixed some design issues and improved page export styling
Fixed alignment on export options dropdown.
Fixed bullet list items sitting too close next to floated content. Fixes #34.
Fixed text overlaying images in PDF exports (Floats removed for now). Fixes #53.
Fixed spaced table cells on html & PDF exports.
2016-02-08 20:41:40 +00:00
Dan Brown
5d73d17c74 Fixed bug preventing LDAP users updating thier profile
Made email not required when a profile is updated. LDAP users without admin privileges could did not have an email field to submit therefore could previously not update thier profile.
2016-02-08 20:35:23 +00:00
Dan Brown
d32460070f Made ldap auth use the 'dn' if a 'uid' is not present.
Fixes #56
2016-02-08 19:45:01 +00:00
Dan Brown
105500e506 Tweaked page form header and added public uploads folder into repo 2016-02-07 10:21:09 +00:00
Dan Brown
8296782149 Updated image controller styling and added preview option
The notification system was also updated so it can be used from JavaScript events such as image manager uploads.

Closes #25
2016-02-07 10:17:38 +00:00
Dan Brown
8e8d582bc6 Updated app requirements & Added some friendlier errors 2016-02-03 20:55:37 +00:00
349 changed files with 11097 additions and 7731 deletions

View File

@@ -3,6 +3,10 @@ APP_ENV=production
APP_DEBUG=false
APP_KEY=SomeRandomString
# The below url has to be set if using social auth options
# or if you are not using BookStack at the root path of your domain.
# APP_URL=http://bookstack.dev
# Database details
DB_HOST=localhost
DB_DATABASE=database_database
@@ -12,8 +16,17 @@ DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
@@ -33,8 +46,6 @@ GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
# URL used for social login redirects, NO TRAILING SLASH
APP_URL=http://bookstack.dev
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false
@@ -53,4 +64,4 @@ MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_ENCRYPTION=null

11
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

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

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ Homestead.yaml
/public/plugins
/public/css/*.map
/public/js/*.map
/public/uploads
/public/bower
/storage/images
_ide_helper.php

31
.travis.yml Normal file
View File

@@ -0,0 +1,31 @@
dist: trusty
sudo: required
language: php
php:
- 7.0
cache:
directories:
- $HOME/.composer/cache
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
- phpenv config-rm xdebug.ini
- composer self-update
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- php artisan clear-compiled -n
- php artisan optimize -n
- php artisan migrate --force -n --database=mysql_testing
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
script:
- phpunit

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Dan Brown
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -2,8 +2,6 @@
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
/**
* @property string key
* @property \User user
@@ -15,15 +13,11 @@ class Activity extends Model
/**
* Get the entity for this activity.
* @return bool
*/
public function entity()
{
if ($this->entity_id) {
return $this->morphTo('entity')->first();
} else {
return false;
}
if ($this->entity_type === '') $this->entity_type = null;
return $this->morphTo('entity');
}
/**
@@ -32,7 +26,7 @@ class Activity extends Model
*/
public function user()
{
return $this->belongsTo('BookStack\User');
return $this->belongsTo(User::class);
}
/**
@@ -50,7 +44,7 @@ class Activity extends Model
* @return bool
*/
public function isSimilarTo($activityB) {
return [$this->key, $this->entitiy_type, $this->entitiy_id] === [$activityB->key, $activityB->entitiy_type, $activityB->entitiy_id];
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
}
}

View File

@@ -1,35 +1,59 @@
<?php
namespace BookStack;
<?php namespace BookStack;
class Book extends Entity
{
protected $fillable = ['name', 'description'];
public function getUrl()
/**
* Get the url for this book.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
return '/books/' . $this->slug;
if ($path !== false) {
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $this->slug);
}
/*
* Get the edit url for this book.
* @return string
*/
public function getEditUrl()
{
return $this->getUrl() . '/edit';
}
/**
* Get all pages within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function pages()
{
return $this->hasMany('BookStack\Page');
return $this->hasMany(Page::class);
}
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function chapters()
{
return $this->hasMany('BookStack\Chapter');
return $this->hasMany(Chapter::class);
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
}

View File

@@ -5,25 +5,47 @@ class Chapter extends Entity
{
protected $fillable = ['name', 'description', 'priority', 'book_id'];
/**
* Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function book()
{
return $this->belongsTo('BookStack\Book');
return $this->belongsTo(Book::class);
}
/**
* Get the pages that this chapter contains.
* @return mixed
*/
public function pages()
{
return $this->hasMany('BookStack\Page')->orderBy('priority', 'ASC');
return $this->hasMany(Page::class)->orderBy('priority', 'ASC');
}
public function getUrl()
/**
* Get the url of this chapter.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
if ($path !== false) {
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
}
/**
* Get an excerpt of this chapter's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\PermissionService;
use Illuminate\Console\Command;
class RegeneratePermissions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'permissions:regen';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Regenerate all system permissions';
/**
* The service to handle the permission system.
*
* @var PermissionService
*/
protected $permissionService;
/**
* Create a new command instance.
*
* @param PermissionService $permissionService
*/
public function __construct(PermissionService $permissionService)
{
$this->permissionService = $permissionService;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->permissionService->buildJointPermissions();
}
}

View File

@@ -15,6 +15,7 @@ class Kernel extends ConsoleKernel
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
];
/**

View File

@@ -1,15 +1,16 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
<?php namespace BookStack;
class EmailConfirmation extends Model
{
protected $fillable = ['user_id', 'token'];
/**
* Get the user that this confirmation is attached to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('BookStack\User');
return $this->belongsTo(User::class);
}
}

View File

@@ -1,14 +1,9 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
abstract class Entity extends Model
class Entity extends Ownable
{
use Ownable;
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
@@ -48,16 +43,64 @@ abstract class Entity extends Model
*/
public function activity()
{
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
}
/**
* Get View objects for this entity.
* @return mixed
*/
public function views()
{
return $this->morphMany('BookStack\View', 'viewable');
return $this->morphMany(View::class, 'viewable');
}
/**
* Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function tags()
{
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get this entities restrictions.
*/
public function permissions()
{
return $this->morphMany(EntityPermission::class, 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
{
return $this->permissions()->where('role_id', '=', $role_id)
->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
*/
public function jointPermissions()
{
return $this->morphMany(JointPermission::class, 'entity');
}
/**
@@ -68,27 +111,43 @@ abstract class Entity extends Model
*/
public static function isA($type)
{
return static::getClassName() === strtolower($type);
return static::getType() === strtolower($type);
}
/**
* Gets the class name.
* @return string
* Get entity type.
* @return mixed
*/
public static function getClassName()
public static function getType()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
return strtolower(static::getClassName());
}
/**
*Gets a limited-length version of the entities name.
* Get an instance of an entity of the given type.
* @param $type
* @return Entity
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
}
return app('BookStack\\' . $className);
}
/**
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public function getShortName($length = 25)
{
if(strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length-3) . '...';
if (strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length - 3) . '...';
}
/**
@@ -98,33 +157,55 @@ abstract class Entity extends Model
* @param string[] array $wheres
* @return mixed
*/
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
{
$termString = '';
foreach ($terms as $term) {
$termString .= htmlentities($term) . '* ';
}
$fields = implode(',', $fieldsToSearch);
$termStringEscaped = \DB::connection()->getPdo()->quote($termString);
$search = static::addSelect(\DB::raw('*, MATCH(name) AGAINST('.$termStringEscaped.' IN BOOLEAN MODE) AS title_relevance'));
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termStringEscaped]);
$exactTerms = [];
if (count($terms) === 0) {
$search = $this;
$orderBy = 'updated_at';
} else {
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
$term = '"' . $term . '"';
} else {
$term = '' . $term . '*';
}
if ($term !== '*') $terms[$key] = $term;
}
$termString = implode(' ', $terms);
$fields = implode(',', $fieldsToSearch);
$search = static::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, $fieldsToSearch) {
foreach ($exactTerms as $exactTerm) {
foreach ($fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = 'title_relevance';
};
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if (!static::isA('book')) $search = $search->with('book');
if (static::isA('page')) $search = $search->with('chapter');
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if ($this->isA('chapter')) {
$search = $search->with('book');
}
return $search->orderBy('title_relevance', 'desc')->get();
return $search->orderBy($orderBy, 'desc');
}
/**
* Get the url for this item.
* @return string
*/
abstract public function getUrl();
}

18
app/EntityPermission.php Normal file
View File

@@ -0,0 +1,18 @@
<?php namespace BookStack;
class EntityPermission extends Model
{
protected $fillable = ['role_id', 'action'];
public $timestamps = false;
/**
* Get all this restriction's attached entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function restrictable()
{
return $this->morphTo('restrictable');
}
}

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
@@ -38,17 +39,52 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
if($e instanceof NotifyException) {
\Session::flash('error', $e->message);
return response()->redirectTo($e->redirectLocation);
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($this->isExceptionType($e, NotifyException::class)) {
session()->flash('error', $this->getOriginalMessage($e));
return redirect($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
$message = $this->getOriginalMessage($e);
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
}
return parent::render($request, $e);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
* @param $type
* @return bool
*/
protected function isExceptionType(Exception $e, $type) {
do {
if (is_a($e, $type)) return true;
} while ($e = $e->getPrevious());
return false;
}
/**
* Get original exception message.
* @param Exception $e
* @return string
*/
protected function getOriginalMessage(Exception $e) {
do {
$message = $e->getMessage();
} while ($e = $e->getPrevious());
return $message;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
<?php namespace BookStack\Exceptions;
class NotFoundException extends PrettyException {
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{
parent::__construct($message, 404);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
<?php
namespace BookStack\Http\Controllers\Auth;
<?php namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use BookStack\Exceptions\SocialSignInException;
@@ -34,16 +33,15 @@ class AuthController extends Controller
protected $redirectAfterLogout = '/login';
protected $username = 'email';
protected $socialAuthService;
protected $emailConfirmationService;
protected $userRepo;
/**
* Create a new authentication controller instance.
* @param SocialAuthService $socialAuthService
* @param SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
@@ -51,6 +49,8 @@ class AuthController extends Controller
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
$this->username = config('auth.method') === 'standard' ? 'email' : 'username';
parent::__construct();
}
@@ -63,15 +63,15 @@ class AuthController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
}
protected function checkRegistrationAllowed()
{
if (!\Setting::get('registration-enabled')) {
if (!setting('registration-enabled')) {
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
}
}
@@ -112,9 +112,10 @@ class AuthController extends Controller
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
*/
protected function authenticated(Request $request, Authenticatable $user)
{
@@ -132,12 +133,21 @@ class AuthController extends Controller
}
if (!$user->exists) {
// Check for users with same email already
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
if ($alreadyUser) {
throw new AuthException('A user with the email ' . $user->email . ' already exists but with different credentials.');
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
return redirect()->intended($this->redirectPath());
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
@@ -153,8 +163,8 @@ class AuthController extends Controller
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
@@ -162,7 +172,7 @@ class AuthController extends Controller
/**
* The registrations flow for all users.
* @param array $userData
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
@@ -170,8 +180,8 @@ class AuthController extends Controller
*/
protected function registerUser(array $userData, $socialAccount = false)
{
if (\Setting::get('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
if (setting('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
@@ -183,15 +193,12 @@ class AuthController extends Controller
$newUser->socialAccounts()->save($socialAccount);
}
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
$newUser->email_confirmed = false;
if (setting('registration-confirmation') || setting('registration-restrict')) {
$newUser->save();
$this->emailConfirmationService->sendConfirmation($newUser);
return redirect('/register/confirm');
}
$newUser->email_confirmed = true;
auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
return redirect($this->redirectPath());

View File

@@ -1,12 +1,8 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
@@ -19,24 +15,26 @@ class BookController extends Controller
protected $bookRepo;
protected $pageRepo;
protected $chapterRepo;
protected $userRepo;
/**
* BookController constructor.
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Display a listing of the book.
*
* @return Response
*/
public function index()
@@ -50,12 +48,11 @@ class BookController extends Controller
/**
* Show the form for creating a new book.
*
* @return Response
*/
public function create()
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->setPageTitle('Create New Book');
return view('books/create');
}
@@ -68,29 +65,25 @@ class BookController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->bookRepo->newFromInput($request->all());
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
$book->created_by = Auth::user()->id;
$book->updated_by = Auth::user()->id;
$book->save();
$book = $this->bookRepo->createFromInput($request->all());
Activity::add($book, 'book_create', $book->id);
return redirect($book->getUrl());
}
/**
* Display the specified book.
*
* @param $slug
* @return Response
*/
public function show($slug)
{
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->bookRepo->getChildren($book);
Views::add($book);
$this->setPageTitle($book->getShortName());
@@ -99,37 +92,32 @@ class BookController extends Controller
/**
* Show the form for editing the specified book.
*
* @param $slug
* @return Response
*/
public function edit($slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle('Edit Book ' . $book->getShortName());
return view('books/edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
*
* @param Request $request
* @param $slug
* @return Response
*/
public function update(Request $request, $slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book->fill($request->all());
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
$book->updated_by = Auth::user()->id;
$book->save();
$book = $this->bookRepo->updateFromInput($book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -141,8 +129,8 @@ class BookController extends Controller
*/
public function showDelete($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle('Delete Book ' . $book->getShortName());
return view('books/delete', ['book' => $book, 'current' => $book]);
}
@@ -154,10 +142,10 @@ class BookController extends Controller
*/
public function sort($bookSlug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = $this->bookRepo->getChildren($book);
$books = $this->bookRepo->getAll();
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->bookRepo->getChildren($book, true);
$books = $this->bookRepo->getAll(false);
$this->setPageTitle('Sort Book ' . $book->getShortName());
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
@@ -177,36 +165,45 @@ class BookController extends Controller
/**
* Saves an array of sort mapping to pages and chapters.
*
* @param string $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function saveSort($bookSlug, Request $request)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->has('sort-tree')) {
return redirect($book->getUrl());
}
$sortedBooks = [];
// Sort pages and chapters
$sortedBooks = [];
$updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id;
foreach ($sortMap as $index => $bookChild) {
$id = $bookChild->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->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
$bookId = $this->bookRepo->exists($bookChild->book) ? intval($bookChild->book) : $defaultBookId;
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$model->priority = $index;
if ($isPage) {
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
// 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)) {
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$model->priority = $priority;
if ($isPage) $model->chapter_id = $chapterId;
$model->save();
$updatedModels->push($model);
}
$model->save();
// Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId;
}
@@ -218,22 +215,56 @@ class BookController extends Controller
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
$this->bookRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}
/**
* Remove the specified book from storage.
*
* @param $bookSlug
* @return Response
*/
public function destroy($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book);
$this->bookRepo->destroyBySlug($bookSlug);
$this->bookRepo->destroy($book);
return redirect('/books');
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
'book' => $book,
'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->bookRepo->updateEntityPermissionsFromRequest($request, $book);
session()->flash('success', 'Book Restrictions Updated');
return redirect($book->getUrl());
}
}

View File

@@ -1,13 +1,9 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\UserRepo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use Views;
@@ -17,20 +13,22 @@ class ChapterController extends Controller
protected $bookRepo;
protected $chapterRepo;
protected $userRepo;
/**
* ChapterController constructor.
* @param $bookRepo
* @param $chapterRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param UserRepo $userRepo
*/
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo, UserRepo $userRepo)
{
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new chapter.
* @param $bookSlug
@@ -38,8 +36,8 @@ class ChapterController extends Controller
*/
public function create($bookSlug)
{
$this->checkPermission('chapter-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle('Create New Chapter');
return view('chapters/create', ['book' => $book, 'current' => $book]);
}
@@ -52,18 +50,16 @@ class ChapterController extends Controller
*/
public function store($bookSlug, Request $request)
{
$this->checkPermission('chapter-create');
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->newFromInput($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
$chapter->priority = $this->bookRepo->getNewPriority($book);
$chapter->created_by = auth()->user()->id;
$chapter->updated_by = auth()->user()->id;
$book->chapters()->save($chapter);
$this->checkOwnablePermission('chapter-create', $book);
$input = $request->all();
$input['priority'] = $this->bookRepo->getNewPriority($book);
$chapter = $this->chapterRepo->createFromInput($input, $book);
Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl());
}
@@ -78,10 +74,18 @@ class ChapterController extends Controller
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
$pages = $this->chapterRepo->getChildren($chapter);
return view('chapters/show', [
'book' => $book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
]);
}
/**
@@ -92,9 +96,9 @@ class ChapterController extends Controller
*/
public function edit($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle('Edit Chapter' . $chapter->getShortName());
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
}
@@ -108,9 +112,9 @@ class ChapterController extends Controller
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$chapter->fill($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
$chapter->updated_by = auth()->user()->id;
@@ -127,9 +131,9 @@ class ChapterController extends Controller
*/
public function showDelete($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle('Delete Chapter' . $chapter->getShortName());
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
}
@@ -142,11 +146,103 @@ class ChapterController extends Controller
*/
public function destroy($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter);
return redirect($book->getUrl());
}
/**
* Show the page for moving a chapter.
* @param $bookSlug
* @param $chapterSlug
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [
'chapter' => $chapter,
'book' => $book
]);
}
/**
* Perform the move action for a chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('chapter-update', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
return redirect($chapter->getUrl());
}
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
$parent = false;
if ($entityType == 'book') {
$parent = $this->bookRepo->getById($entityId);
}
if ($parent === false || $parent === null) {
session()->flash('The selected Book was not found');
return redirect()->back();
}
$this->chapterRepo->changeBook($parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', sprintf('Chapter moved to "%s"', $parent->name));
return redirect($chapter->getUrl());
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
'chapter' => $chapter,
'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->chapterRepo->updateEntityPermissionsFromRequest($request, $chapter);
session()->flash('success', 'Chapter Restrictions Updated');
return redirect($chapter->getUrl());
}
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use HttpRequestException;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
@@ -61,21 +62,19 @@ abstract class Controller extends BaseController
}
/**
* On a permission error redirect to home and display
* On a permission error redirect to home and display.
* the error as a notification.
*/
protected function showPermissionError()
{
Session::flash('error', trans('errors.permission'));
throw new HttpResponseException(
redirect('/')
);
$response = request()->wantsJson() ? response()->json(['error' => trans('errors.permissionJson')], 403) : redirect('/');
throw new HttpResponseException($response);
}
/**
* Checks for a permission.
*
* @param $permissionName
* @param string $permissionName
* @return bool|\Illuminate\Http\RedirectResponse
*/
protected function checkPermission($permissionName)
@@ -83,10 +82,21 @@ abstract class Controller extends BaseController
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
$this->showPermissionError();
}
return true;
}
/**
* Check the current user's permissions against an ownable item.
* @param $permission
* @param Ownable $ownable
* @return bool
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
return $this->showPermissionError();
}
/**
* Check if a user has a permission or bypass if the callback is true.
* @param $permissionName
@@ -100,4 +110,15 @@ abstract class Controller extends BaseController
return true;
}
/**
* Send back a json error message.
* @param string $messageText
* @param int $statusCode
* @return mixed
*/
protected function jsonError($messageText = "", $statusCode = 500)
{
return response()->json(['message' => $messageText], $statusCode);
}
}

View File

@@ -3,39 +3,44 @@
namespace BookStack\Http\Controllers;
use Activity;
use Illuminate\Http\Request;
use BookStack\Repos\EntityRepo;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use Views;
class HomeController extends Controller
{
protected $activityService;
protected $bookRepo;
protected $entityRepo;
/**
* HomeController constructor.
* @param BookRepo $bookRepo
* @param EntityRepo $entityRepo
*/
public function __construct(BookRepo $bookRepo)
public function __construct(EntityRepo $entityRepo)
{
$this->bookRepo = $bookRepo;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Display the homepage.
*
* @return Response
*/
public function index()
{
$activity = Activity::latest();
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
return view('home', ['activity' => $activity, 'recents' => $recents]);
$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->getRecentlyCreatedBooks(10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreatedPages(5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdatedPages(5);
return view('home', [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages
]);
}
}

View File

@@ -1,14 +1,9 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Intervention\Image\Facades\Image as ImageTool;
use Illuminate\Support\Facades\DB;
use BookStack\Image;
use BookStack\Repos\PageRepo;
@@ -20,8 +15,8 @@ class ImageController extends Controller
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
@@ -32,9 +27,9 @@ class ImageController extends Controller
parent::__construct();
}
/**
* Get all images for a specific type, Paginated
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
@@ -44,6 +39,24 @@ class ImageController extends Controller
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType($type, $page = 0, Request $request)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
return response()->json($imgData);
}
/**
* Get all images for a user.
* @param int $page
@@ -55,24 +68,45 @@ class ImageController extends Controller
return response()->json($imgData);
}
/**
* Get gallery images with a specific filter such as book or page
* @param $filter
* @param int $page
* @param Request $request
*/
public function getGalleryFiltered($filter, $page = 0, Request $request)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create');
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'image|mimes:jpeg,gif,png'
'file' => 'is_image'
]);
$imageUpload = $request->file('file');
try {
$image = $this->imageRepo->saveNew($imageUpload, $type);
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
@@ -90,7 +124,7 @@ class ImageController extends Controller
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create');
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
@@ -98,33 +132,32 @@ class ImageController extends Controller
/**
* Update image details
* @param $imageId
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function update($imageId, Request $request)
{
$this->checkPermission('image-update');
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo
* @param Request $request
* @param int $id
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(PageRepo $pageRepo, Request $request, $id)
{
$this->checkPermission('image-delete');
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);

View File

@@ -1,16 +1,16 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Views;
class PageController extends Controller
@@ -20,26 +20,28 @@ class PageController extends Controller
protected $bookRepo;
protected $chapterRepo;
protected $exportService;
protected $userRepo;
/**
* PageController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ExportService $exportService
* @param UserRepo $userRepo
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService, UserRepo $userRepo)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new page.
*
* @param $bookSlug
* @param bool $chapterSlug
* @return Response
@@ -47,33 +49,60 @@ class PageController extends Controller
*/
public function create($bookSlug, $chapterSlug = false)
{
$this->checkPermission('page-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$this->setPageTitle('Create New Page');
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
$draft = $this->pageRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl());
}
/**
* Store a newly created page in storage.
*
* Show form to continue editing a draft page.
* @param $bookSlug
* @param $pageId
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function editDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$draft = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-create', $book);
$this->setPageTitle('Edit Page Draft');
return view('pages/edit', ['page' => $draft, 'book' => $book, 'isDraft' => true]);
}
/**
* Store a new page by changing a draft into a page.
* @param Request $request
* @param $bookSlug
* @param string $bookSlug
* @return Response
*/
public function store(Request $request, $bookSlug)
public function store(Request $request, $bookSlug, $pageId)
{
$this->checkPermission('page-create');
$this->validate($request, [
'name' => 'required|string|max:255'
'name' => 'required|string|max:255'
]);
$input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug);
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
$input['priority'] = $this->bookRepo->getNewPriority($book);
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
$draftPage = $this->pageRepo->getById($pageId, true);
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->chapterRepo->getById($chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
$input['priority'] = $this->chapterRepo->getNewPriority($parent);
} else {
$input['priority'] = $this->bookRepo->getNewPriority($parent);
}
$page = $this->pageRepo->publishDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
@@ -81,7 +110,8 @@ class PageController extends Controller
/**
* Display the specified page.
*
* If the page is not found via the slug the
* revisions are searched for a match.
* @param $bookSlug
* @param $pageSlug
* @return Response
@@ -89,32 +119,71 @@ class PageController extends Controller
public function show($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
try {
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
} catch (NotFoundException $e) {
$page = $this->pageRepo->findPageUsingOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
}
/**
* Get page from an ajax request.
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function getPageAjax($pageId)
{
$page = $this->pageRepo->getById($pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
*
* @param $bookSlug
* @param $pageSlug
* @return Response
*/
public function edit($bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Editing Page ' . $page->getShortName());
$page->isDraft = false;
// Check for active editing
$warnings = [];
if ($this->pageRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
}
// Check for a current draft version for this user
if ($this->pageRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
$page->name = $draft->name;
$page->html = $draft->html;
$page->markdown = $draft->markdown;
$page->isDraft = true;
$warnings [] = $this->pageRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
}
/**
* Update the specified page in storage.
*
* @param Request $request
* @param $bookSlug
* @param $pageSlug
@@ -122,17 +191,42 @@ class PageController extends Controller
*/
public function update(Request $request, $bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$this->validate($request, [
'name' => 'required|string|max:255'
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$this->pageRepo->updatePage($page, $book->id, $request->all());
Activity::add($page, 'page_update', $book->id);
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
* @param Request $request
* @param $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function saveDraft(Request $request, $pageId)
{
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
if ($page->draft) {
$draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown']));
} else {
$draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown']));
}
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => 'Draft saved at ',
'timestamp' => $utcUpdateTimestamp
]);
}
/**
* Redirect from a special link url which
* uses the page id rather than the name.
@@ -153,16 +247,32 @@ class PageController extends Controller
*/
public function showDelete($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle('Delete Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
}
/**
* Show the deletion page for the specified page.
* @param $bookSlug
* @param $pageId
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showDeleteDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle('Delete Draft Page ' . $page->getShortName());
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
}
/**
* Remove the specified page from storage.
*
* @param $bookSlug
* @param $pageSlug
* @return Response
@@ -170,10 +280,28 @@ class PageController extends Controller
*/
public function destroy($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', 'Page deleted');
$this->pageRepo->destroy($page);
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
* @param $bookSlug
* @param $pageId
* @return Response
* @throws NotFoundException
*/
public function destroyDraft($bookSlug, $pageId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getById($pageId, true);
$this->checkOwnablePermission('page-update', $page);
session()->flash('success', 'Draft deleted');
$this->pageRepo->destroy($page);
return redirect($book->getUrl());
}
@@ -218,9 +346,9 @@ class PageController extends Controller
*/
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl());
@@ -239,8 +367,8 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
}
@@ -256,8 +384,8 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
}
@@ -273,9 +401,131 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated(20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => 'Recently Created Pages',
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
$pages = $this->pageRepo->getRecentlyUpdatedPaginated(20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [
'title' => 'Recently Updated Pages',
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Show the view to choose a new parent to move a page into.
* @param $bookSlug
* @param $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showMove($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
return view('pages/move', [
'book' => $book,
'page' => $page
]);
}
/**
* Does the action of moving the location of a page
* @param $bookSlug
* @param $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function move($bookSlug, $pageSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
return redirect($page->getUrl());
}
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
$parent = false;
if ($entityType == 'chapter') {
$parent = $this->chapterRepo->getById($entityId);
} else if ($entityType == 'book') {
$parent = $this->bookRepo->getById($entityId);
}
if ($parent === false || $parent === null) {
session()->flash('The selected Book or Chapter was not found');
return redirect()->back();
}
$this->pageRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', sprintf('Page moved to "%s"', $parent->name));
return redirect($page->getUrl());
}
/**
* Set the permissions for this page.
* @param $bookSlug
* @param $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', 'Page Permissions Updated');
return redirect($page->getUrl());
}
}

View File

@@ -0,0 +1,132 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\PermissionsException;
use BookStack\Repos\PermissionsRepo;
use BookStack\Services\PermissionService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class PermissionController extends Controller
{
protected $permissionsRepo;
/**
* PermissionController constructor.
* @param PermissionsRepo $permissionsRepo
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
parent::__construct();
}
/**
* Show a listing of the roles in the system.
*/
public function listRoles()
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
return view('settings/roles/index', ['roles' => $roles]);
}
/**
* Show the form to create a new role
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function createRole()
{
$this->checkPermission('user-roles-manage');
return view('settings/roles/create');
}
/**
* Store a new role in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function storeRole(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
]);
$this->permissionsRepo->saveNewRole($request->all());
session()->flash('success', 'Role successfully created');
return redirect('/settings/roles');
}
/**
* Show the form for editing a user role.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws PermissionsException
*/
public function editRole($id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) throw new PermissionsException('This role cannot be edited');
return view('settings/roles/edit', ['role' => $role]);
}
/**
* Updates a user role.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function updateRole($id, Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
]);
$this->permissionsRepo->updateRole($id, $request->all());
session()->flash('success', 'Role successfully updated');
return redirect('/settings/roles');
}
/**
* Show the view to delete a role.
* Offers the chance to migrate users.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showDeleteRole($id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => 'Don\'t migrate users']);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
}
/**
* Delete a role from the system,
* Migrate from a previous role if set.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function deleteRole($id, Request $request)
{
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
} catch (PermissionsException $e) {
session()->flash('error', $e->getMessage());
return redirect()->back();
}
session()->flash('success', 'Role successfully deleted');
return redirect('/settings/roles');
}
}

View File

@@ -2,10 +2,10 @@
namespace BookStack\Http\Controllers;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
@@ -15,18 +15,21 @@ class SearchController extends Controller
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $viewService;
/**
* SearchController constructor.
* @param $pageRepo
* @param $bookRepo
* @param $chapterRepo
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param ViewService $viewService
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ViewService $viewService)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->viewService = $viewService;
parent::__construct();
}
@@ -42,11 +45,77 @@ class SearchController extends Controller
return redirect()->back();
}
$searchTerm = $request->get('term');
$pages = $this->pageRepo->getBySearch($searchTerm);
$books = $this->bookRepo->getBySearch($searchTerm);
$chapters = $this->chapterRepo->getBySearch($searchTerm);
$paginationAppends = $request->only('term');
$pages = $this->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$books = $this->bookRepo->getBySearch($searchTerm, 10, $paginationAppends);
$chapters = $this->chapterRepo->getBySearch($searchTerm, [], 10, $paginationAppends);
$this->setPageTitle('Search For ' . $searchTerm);
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
]);
}
/**
* 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->pageRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Page Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $pages,
'title' => 'Page Search Results',
'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->chapterRepo->getBySearch($searchTerm, [], 20, $paginationAppends);
$this->setPageTitle('Chapter Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => 'Chapter Search Results',
'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->bookRepo->getBySearch($searchTerm, 20, $paginationAppends);
$this->setPageTitle('Book Search For ' . $searchTerm);
return view('search/entity-search-list', [
'entities' => $books,
'title' => 'Book Search Results',
'searchTerm' => $searchTerm
]);
}
/**
@@ -68,4 +137,35 @@ class SearchController extends Controller
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
* @param Request $request
* @return mixed
*/
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;
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
if ($entityTypes->contains('page')) $entities = $entities->merge($this->pageRepo->getBySearch($searchTerm)->items());
if ($entityTypes->contains('chapter')) $entities = $entities->merge($this->chapterRepo->getBySearch($searchTerm)->items());
if ($entityTypes->contains('book')) $entities = $entities->merge($this->bookRepo->getBySearch($searchTerm)->items());
$entities = $entities->sortByDesc('title_relevance');
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

@@ -1,42 +1,43 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use Setting;
class SettingController extends Controller
{
/**
* Display a listing of the settings.
*
* @return Response
*/
public function index()
{
$this->checkPermission('settings-update');
$this->checkPermission('settings-manage');
$this->setPageTitle('Settings');
return view('settings/index');
}
// Get application version
$version = false;
if (function_exists('exec')) {
$version = exec('git describe --always --tags ');
}
return view('settings/index', ['version' => $version]);
}
/**
* Update the specified settings in storage.
*
* @param Request $request
* @param Request $request
* @return Response
*/
public function update(Request $request)
{
$this->preventAccessForDemoUsers();
$this->checkPermission('settings-update');
$this->checkPermission('settings-manage');
// Cycles through posted settings and update them
foreach($request->all() as $name => $value) {
if(strpos($name, 'setting-') !== 0) continue;
foreach ($request->all() as $name => $value) {
if (strpos($name, 'setting-') !== 0) continue;
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
}

View File

@@ -0,0 +1,75 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\TagRepo;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
class TagController extends Controller
{
protected $tagRepo;
/**
* TagController constructor.
* @param $tagRepo
*/
public function __construct(TagRepo $tagRepo)
{
$this->tagRepo = $tagRepo;
}
/**
* Get all the Tags for a particular entity
* @param $entityType
* @param $entityId
*/
public function getForEntity($entityType, $entityId)
{
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
return response()->json($tags);
}
/**
* Update the tags for a particular entity.
* @param $entityType
* @param $entityId
* @param Request $request
* @return mixed
*/
public function updateForEntity($entityType, $entityId, Request $request)
{
$entity = $this->tagRepo->getEntity($entityType, $entityId, 'update');
if ($entity === null) return $this->jsonError("Entity not found", 404);
$inputTags = $request->input('tags');
$tags = $this->tagRepo->saveTagsToEntity($entity, $inputTags);
return response()->json([
'tags' => $tags,
'message' => 'Tags successfully updated'
]);
}
/**
* Get tag name suggestions from a given search term.
* @param Request $request
*/
public function getNameSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
return response()->json($suggestions);
}
/**
* Get tag value suggestions from a given search term.
* @param Request $request
*/
public function getValueSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$tagName = $request->has('name') ? $request->get('name') : false;
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Activity;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -30,13 +31,21 @@ class UserController extends Controller
/**
* Display a listing of the users.
* @param Request $request
* @return Response
*/
public function index()
public function index(Request $request)
{
$users = $this->user->all();
$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',
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle('Users');
return view('users/index', ['users' => $users]);
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
}
/**
@@ -45,9 +54,10 @@ class UserController extends Controller
*/
public function create()
{
$this->checkPermission('user-create');
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
return view('users/create', ['authMethod' => $authMethod]);
$roles = $this->userRepo->getAssignableRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
@@ -57,11 +67,10 @@ class UserController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('user-create');
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
'email' => 'required|email|unique:users,email',
'role' => 'required|exists:roles,id'
'email' => 'required|email|unique:users,email'
];
$authMethod = config('auth.method');
@@ -83,7 +92,11 @@ class UserController extends Controller
}
$user->save();
$user->attachRoleId($request->get('role'));
if ($request->has('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
@@ -92,10 +105,9 @@ class UserController extends Controller
$user->save();
}
return redirect('/users');
return redirect('/settings/users');
}
/**
* Show the form for editing the specified user.
* @param int $id
@@ -104,7 +116,7 @@ class UserController extends Controller
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOr('user-update', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
@@ -113,7 +125,8 @@ class UserController extends Controller
$user = $this->user->findOrFail($id);
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle('User Profile');
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod]);
$roles = $this->userRepo->getAssignableRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
/**
@@ -125,16 +138,15 @@ class UserController extends Controller
public function update(Request $request, $id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('user-update', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $id,
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'role' => 'exists:roles,id'
'password-confirm' => 'same:password|required_with:password'
], [
'password-confirm.required_with' => 'Password confirmation required'
]);
@@ -143,8 +155,9 @@ class UserController extends Controller
$user->fill($request->all());
// Role updates
if ($this->currentUser->can('user-update') && $request->has('role')) {
$user->attachRoleId($request->get('role'));
if (userCan('users-manage') && $request->has('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Password updates
@@ -154,12 +167,15 @@ class UserController extends Controller
}
// External auth id updates
if ($this->currentUser->can('user-update') && $request->has('external_auth_id')) {
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
return redirect('/users');
session()->flash('success', 'User successfully updated');
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
return redirect($redirectUrl);
}
/**
@@ -169,7 +185,7 @@ class UserController extends Controller
*/
public function delete($id)
{
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
@@ -186,17 +202,39 @@ class UserController extends Controller
public function destroy($id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->userRepo->getById($id);
if ($this->userRepo->isOnlyAdmin($user)) {
session()->flash('error', 'You cannot delete the only admin');
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
return redirect('/users');
$this->userRepo->destroy($user);
session()->flash('success', 'User successfully removed');
return redirect('/settings/users');
}
/**
* Show the user profile page
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
'assetCounts' => $assetCounts
]);
}
}

View File

@@ -11,14 +11,12 @@ class Authenticate
{
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
*/
public function __construct(Guard $auth)
@@ -28,22 +26,21 @@ class Authenticate
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if(auth()->check() && auth()->user()->email_confirmed == false) {
return redirect()->guest('/register/confirm/awaiting');
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect()->guest(baseUrl('/register/confirm/awaiting'));
}
if ($this->auth->guest() && !Setting::get('app-public')) {
if ($this->auth->guest() && !setting('app-public')) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('/login');
return redirect()->guest(baseUrl('/login'));
}
}

View File

@@ -3,6 +3,11 @@
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {
Route::group(['prefix' => 'pages'], function() {
Route::get('/recently-created', 'PageController@showRecentlyCreated');
Route::get('/recently-updated', 'PageController@showRecentlyUpdated');
});
Route::group(['prefix' => 'books'], function () {
// Books
@@ -14,21 +19,30 @@ Route::group(['middleware' => 'auth'], function () {
Route::delete('/{id}', 'BookController@destroy');
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
Route::get('/{slug}', 'BookController@show');
Route::get('/{bookSlug}/permissions', 'BookController@showRestrict');
Route::put('/{bookSlug}/permissions', 'BookController@restrict');
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/draft/{pageId}', 'PageController@editDraft');
Route::post('/{bookSlug}/draft/{pageId}', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
Route::put('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@restrict');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
Route::delete('/{bookSlug}/draft/{pageId}', 'PageController@destroyDraft');
// Revisions
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
@@ -41,20 +55,18 @@ Route::group(['middleware' => 'auth'], function () {
Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
Route::get('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@showMove');
Route::put('/{bookSlug}/chapter/{chapterSlug}/move', 'ChapterController@move');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@showRestrict');
Route::put('/{bookSlug}/chapter/{chapterSlug}/permissions', 'ChapterController@restrict');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
});
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::get('/users/{id}/delete', 'UserController@delete');
Route::post('/users/create', 'UserController@store');
Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy');
// User Profile routes
Route::get('/user/{userId}', 'UserController@showProfilePage');
// Image routes
Route::group(['prefix' => 'images'], function() {
@@ -67,14 +79,34 @@ Route::group(['middleware' => 'auth'], function () {
Route::post('/{type}/upload', 'ImageController@uploadByType');
Route::get('/{type}/all', 'ImageController@getAllByType');
Route::get('/{type}/all/{page}', 'ImageController@getAllByType');
Route::get('/{type}/search/{page}', 'ImageController@searchByType');
Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered');
Route::delete('/{imageId}', 'ImageController@destroy');
});
// AJAX routes
Route::put('/ajax/page/{id}/save-draft', 'PageController@saveDraft');
Route::get('/ajax/page/{id}', 'PageController@getPageAjax');
Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
// Tag routes (AJAX)
Route::group(['prefix' => 'ajax/tags'], function() {
Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
Route::get('/suggest/names', 'TagController@getNameSuggestions');
Route::get('/suggest/values', 'TagController@getValueSuggestions');
Route::post('/update/{entityType}/{entityId}', 'TagController@updateForEntity');
});
Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
// Links
Route::get('/link/{id}', 'PageController@redirectFromLink');
// Search
Route::get('/search/all', 'SearchController@searchAll');
Route::get('/search/pages', 'SearchController@searchPages');
Route::get('/search/books', 'SearchController@searchBooks');
Route::get('/search/chapters', 'SearchController@searchChapters');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
// Other Pages
@@ -82,8 +114,28 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/home', 'HomeController@index');
// Settings
Route::get('/settings', 'SettingController@index');
Route::post('/settings', 'SettingController@update');
Route::group(['prefix' => 'settings'], function() {
Route::get('/', 'SettingController@index');
Route::post('/', 'SettingController@update');
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::get('/users/{id}/delete', 'UserController@delete');
Route::post('/users/create', 'UserController@store');
Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy');
// Roles
Route::get('/roles', 'PermissionController@listRoles');
Route::get('/roles/new', 'PermissionController@createRole');
Route::post('/roles/new', 'PermissionController@storeRole');
Route::get('/roles/delete/{id}', 'PermissionController@showDeleteRole');
Route::delete('/roles/delete/{id}', 'PermissionController@deleteRole');
Route::get('/roles/{id}', 'PermissionController@editRole');
Route::put('/roles/{id}', 'PermissionController@updateRole');
});
});

View File

@@ -1,14 +1,9 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
use Images;
class Image extends Model
class Image extends Ownable
{
use Ownable;
protected $fillable = ['name'];

24
app/JointPermission.php Normal file
View File

@@ -0,0 +1,24 @@
<?php namespace BookStack;
class JointPermission extends Model
{
public $timestamps = false;
/**
* Get the role that this points to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function entity()
{
return $this->morphOne(Entity::class, 'entity');
}
}

19
app/Model.php Normal file
View File

@@ -0,0 +1,19 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends EloquentModel
{
/**
* Provides public access to get the raw attribute value from the model.
* Used in areas where no mutations are required but performance is critical.
* @param $key
* @return mixed
*/
public function getRawAttribute($key)
{
return parent::getAttributeFromArray($key);
}
}

View File

@@ -1,7 +1,7 @@
<?php namespace BookStack;
trait Ownable
abstract class Ownable extends Model
{
/**
* Relation for the user that created this entity.
@@ -9,7 +9,7 @@ trait Ownable
*/
public function createdBy()
{
return $this->belongsTo('BookStack\User', 'created_by');
return $this->belongsTo(User::class, 'created_by');
}
/**
@@ -18,6 +18,16 @@ trait Ownable
*/
public function updatedBy()
{
return $this->belongsTo('BookStack\User', 'updated_by');
return $this->belongsTo(User::class, 'updated_by');
}
/**
* Gets the class name.
* @return string
*/
public static function getClassName()
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
}
}

View File

@@ -1,15 +1,16 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class Page extends Entity
{
protected $fillable = ['name', 'html', 'priority'];
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
/**
* Converts this page into a simplified array.
* @return mixed
*/
public function toSimpleArray()
{
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
@@ -17,35 +18,69 @@ class Page extends Entity
return $array;
}
/**
* Get the book this page sits in.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function book()
{
return $this->belongsTo('BookStack\Book');
return $this->belongsTo(Book::class);
}
/**
* Get the chapter that this page is in, If applicable.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function chapter()
{
return $this->belongsTo('BookStack\Chapter');
return $this->belongsTo(Chapter::class);
}
/**
* Check if this page has a chapter.
* @return bool
*/
public function hasChapter()
{
return $this->chapter()->count() > 0;
}
/**
* Get the associated page revisions, ordered by created date.
* @return mixed
*/
public function revisions()
{
return $this->hasMany('BookStack\PageRevision')->orderBy('created_at', 'desc');
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc');
}
public function getUrl()
/**
* Get the url for this page.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
return '/books/' . $bookSlug . '/page/' . $this->slug;
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : $this->slug;
if ($path !== false) {
return baseUrl('/books/' . $bookSlug . $midText . $idComponent . '/' . trim($path, '/'));
}
return baseUrl('/books/' . $bookSlug . $midText . $idComponent);
}
/**
* Get an excerpt of this page's content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt($length = 100)
{
return strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
return mb_convert_encoding($text, 'UTF-8');
}
}

View File

@@ -1,23 +1,32 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text'];
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
/**
* Get the user that created the page revision
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
{
return $this->belongsTo('BookStack\User', 'created_by');
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the page this revision originates from.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
{
return $this->belongsTo('BookStack\Page');
return $this->belongsTo(Page::class);
}
/**
* Get the url for this revision.
* @return string
*/
public function getUrl()
{
return $this->page->getUrl() . '/revisions/' . $this->id;

View File

@@ -1,16 +0,0 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class Permission extends Model
{
/**
* The roles that belong to the permission.
*/
public function roles()
{
return $this->belongsToMany('BookStack\Permissions');
}
}

View File

@@ -1,10 +1,6 @@
<?php
<?php namespace BookStack\Providers;
namespace BookStack\Providers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;
use BookStack\User;
class AppServiceProvider extends ServiceProvider
{
@@ -15,7 +11,12 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
// Custom validation methods
\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);
});
}
/**

View File

@@ -3,6 +3,7 @@
namespace BookStack\Providers;
use Auth;
use BookStack\Services\LdapService;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
@@ -25,7 +26,7 @@ class AuthServiceProvider extends ServiceProvider
public function register()
{
Auth::provider('ldap', function($app, array $config) {
return new LdapUserProvider($config['model'], $app['BookStack\Services\LdapService']);
return new LdapUserProvider($config['model'], $app[LdapService::class]);
});
}
}

View File

@@ -2,11 +2,18 @@
namespace BookStack\Providers;
use BookStack\Activity;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use BookStack\Services\ViewService;
use BookStack\Setting;
use BookStack\View;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Support\ServiceProvider;
use BookStack\Services\ActivityService;
use BookStack\Services\SettingService;
use Intervention\Image\ImageManager;
class CustomFacadeProvider extends ServiceProvider
{
@@ -28,24 +35,31 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function() {
return new ActivityService($this->app->make('BookStack\Activity'));
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('views', function() {
return new ViewService($this->app->make('BookStack\View'));
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
});
$this->app->bind('setting', function() {
return new SettingService(
$this->app->make('BookStack\Setting'),
$this->app->make('Illuminate\Contracts\Cache\Repository')
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
});
$this->app->bind('images', function() {
return new ImageService(
$this->app->make('Intervention\Image\ImageManager'),
$this->app->make('Illuminate\Contracts\Filesystem\Factory'),
$this->app->make('Illuminate\Contracts\Cache\Repository')
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class)
);
});
}

View File

@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProvider
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];
$model->email = $userDetails['email'];
$model->email_confirmed = true;
$model->email_confirmed = false;
return $model;
}

View File

@@ -0,0 +1,30 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
class PaginationServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
Paginator::currentPathResolver(function () {
return baseUrl($this->app['request']->path());
});
Paginator::currentPageResolver(function ($pageName = 'page') {
$page = $this->app['request']->input($pageName);
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
return $page;
}
return 1;
});
}
}

View File

@@ -1,28 +1,37 @@
<?php namespace BookStack\Repos;
use Activity;
use Alpha\B;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
class BookRepo
class BookRepo extends EntityRepo
{
protected $book;
protected $pageRepo;
protected $chapterRepo;
/**
* BookRepo constructor.
* @param Book $book
* @param PageRepo $pageRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
*/
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
public function __construct(PageRepo $pageRepo, ChapterRepo $chapterRepo)
{
$this->book = $book;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
parent::__construct();
}
/**
* Base query for getting books.
* Takes into account any restrictions.
* @return mixed
*/
private function bookQuery()
{
return $this->permissionService->enforceBookRestrictions($this->book, 'view');
}
/**
@@ -32,7 +41,7 @@ class BookRepo
*/
public function getById($id)
{
return $this->book->findOrFail($id);
return $this->bookQuery()->findOrFail($id);
}
/**
@@ -42,7 +51,9 @@ class BookRepo
*/
public function getAll($count = 10)
{
return $this->book->orderBy('name', 'asc')->take($count)->get();
$bookQuery = $this->bookQuery()->orderBy('name', 'asc');
if (!$count) return $bookQuery->get();
return $bookQuery->take($count)->get();
}
/**
@@ -52,7 +63,8 @@ class BookRepo
*/
public function getAllPaginated($count = 10)
{
return $this->book->orderBy('name', 'asc')->paginate($count);
return $this->bookQuery()
->orderBy('name', 'asc')->paginate($count);
}
@@ -63,7 +75,7 @@ class BookRepo
*/
public function getLatest($count = 10)
{
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
return $this->bookQuery()->orderBy('created_at', 'desc')->take($count)->get();
}
/**
@@ -92,11 +104,12 @@ class BookRepo
* Get a book by slug
* @param $slug
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug)
{
$book = $this->book->where('slug', '=', $slug)->first();
if ($book === null) abort(404);
$book = $this->bookQuery()->where('slug', '=', $slug)->first();
if ($book === null) throw new NotFoundException('Book not found');
return $book;
}
@@ -107,36 +120,48 @@ class BookRepo
*/
public function exists($id)
{
return $this->book->where('id', '=', $id)->exists();
return $this->bookQuery()->where('id', '=', $id)->exists();
}
/**
* Get a new book instance from request input.
* @param array $input
* @return Book
*/
public function createFromInput($input)
{
$book = $this->book->newInstance($input);
$book->slug = $this->findSuitableSlug($book->name);
$book->created_by = auth()->user()->id;
$book->updated_by = auth()->user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Update the given book from user input.
* @param Book $book
* @param $input
* @return Book
*/
public function newFromInput($input)
public function updateFromInput(Book $book, $input)
{
return $this->book->fill($input);
$book->fill($input);
$book->slug = $this->findSuitableSlug($book->name, $book->id);
$book->updated_by = auth()->user()->id;
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
return $book;
}
/**
* Count the amount of books that have a specific slug.
* @param $slug
* @return mixed
* Destroy the given book.
* @param Book $book
* @throws \Exception
*/
public function countBySlug($slug)
public function destroy(Book $book)
{
return $this->book->where('slug', '=', $slug)->count();
}
/**
* Destroy a book identified by the given slug.
* @param $bookSlug
*/
public function destroyBySlug($bookSlug)
{
$book = $this->getBySlug($bookSlug);
foreach ($book->pages as $page) {
$this->pageRepo->destroy($page);
}
@@ -144,6 +169,8 @@ class BookRepo
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$book->delete();
}
@@ -159,7 +186,7 @@ class BookRepo
}
/**
* @param string $slug
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
@@ -175,18 +202,16 @@ class BookRepo
/**
* Provides a suitable slug for the given book name.
* Ensures the returned slug is unique in the system.
* @param string $name
* @param string $name
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $currentId = false)
{
$originalSlug = Str::slug($name);
$slug = $originalSlug;
$count = 2;
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $currentId)) {
$slug = $originalSlug . '-' . $count;
$count++;
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
@@ -194,36 +219,69 @@ class BookRepo
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @return mixed
*/
public function getChildren(Book $book)
public function getChildren(Book $book, $filterDrafts = false)
{
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
$chapters = $book->chapters()->with('pages')->get();
$children = $pages->merge($chapters);
$pageQuery = $book->pages()->where('chapter_id', '=', 0);
$pageQuery = $this->permissionService->enforcePageRestrictions($pageQuery, 'view');
if ($filterDrafts) {
$pageQuery = $pageQuery->where('draft', '=', false);
}
$pages = $pageQuery->get();
$chapterQuery = $book->chapters()->with(['pages' => function ($query) use ($filterDrafts) {
$this->permissionService->enforcePageRestrictions($query, 'view');
if ($filterDrafts) $query->where('draft', '=', false);
}]);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($chapterQuery, 'view');
$chapters = $chapterQuery->get();
$children = $pages->values();
foreach ($chapters as $chapter) {
$children->push($chapter);
}
$bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
$child->pages = $child->pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
});
return $children->sortBy('priority');
// Sort items with drafts first then by priority.
return $children->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->isA('page') && $child->draft) $score -= 100;
return $score;
});
}
/**
* Get books by search term.
* @param $term
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term)
public function getBySearch($term, $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
$terms = $this->prepareSearchTerms($term);
$bookQuery = $this->permissionService->enforceBookRestrictions($this->book->fullTextSearchQuery(['name', 'description'], $terms));
$bookQuery = $this->addAdvancedSearchQueries($bookQuery, $term);
$books = $bookQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($books as $book) {
//highlight

View File

@@ -2,21 +2,32 @@
use Activity;
use BookStack\Book;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Support\Str;
use BookStack\Chapter;
class ChapterRepo
class ChapterRepo extends EntityRepo
{
protected $chapter;
protected $pageRepo;
/**
* ChapterRepo constructor.
* @param $chapter
* @param $pageRepo
*/
public function __construct(Chapter $chapter)
public function __construct(PageRepo $pageRepo)
{
$this->chapter = $chapter;
$this->pageRepo = $pageRepo;
parent::__construct();
}
/**
* Base query for getting chapters, Takes permissions into account.
* @return mixed
*/
private function chapterQuery()
{
return $this->permissionService->enforceChapterRestrictions($this->chapter, 'view');
}
/**
@@ -26,7 +37,7 @@ class ChapterRepo
*/
public function idExists($id)
{
return $this->chapter->where('id', '=', $id)->count() > 0;
return $this->chapterQuery()->where('id', '=', $id)->count() > 0;
}
/**
@@ -36,7 +47,7 @@ class ChapterRepo
*/
public function getById($id)
{
return $this->chapter->findOrFail($id);
return $this->chapterQuery()->findOrFail($id);
}
/**
@@ -45,7 +56,7 @@ class ChapterRepo
*/
public function getAll()
{
return $this->chapter->all();
return $this->chapterQuery()->all();
}
/**
@@ -53,22 +64,45 @@ class ChapterRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$chapter = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) abort(404);
$chapter = $this->chapterQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($chapter === null) throw new NotFoundException('Chapter not found');
return $chapter;
}
/**
* Get the child items for a chapter
* @param Chapter $chapter
*/
public function getChildren(Chapter $chapter)
{
$pages = $this->permissionService->enforcePageRestrictions($chapter->pages())->get();
// Sort items with drafts first then by priority.
return $pages->sortBy(function ($child, $key) {
$score = $child->priority;
if ($child->draft) $score -= 100;
return $score;
});
}
/**
* Create a new chapter from request input.
* @param $input
* @return $this
* @param Book $book
* @return Chapter
*/
public function newFromInput($input)
public function createFromInput($input, Book $book)
{
return $this->chapter->fill($input);
$chapter = $this->chapter->newInstance($input);
$chapter->slug = $this->findSuitableSlug($chapter->name, $book->id);
$chapter->created_by = auth()->user()->id;
$chapter->updated_by = auth()->user()->id;
$chapter = $book->chapters()->save($chapter);
$this->permissionService->buildJointPermissionsForEntity($chapter);
return $chapter;
}
/**
@@ -85,6 +119,8 @@ class ChapterRepo
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$chapter->delete();
}
@@ -115,22 +151,39 @@ class ChapterRepo
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get a new priority value for a new page to be added
* to the given chapter.
* @param Chapter $chapter
* @return int
*/
public function getNewPriority(Chapter $chapter)
{
$lastPage = $chapter->pages->last();
return $lastPage !== null ? $lastPage->priority + 1 : 0;
}
/**
* Get chapters by the given search term.
* @param $term
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
$terms = $this->prepareSearchTerms($term);
$chapterQuery = $this->permissionService->enforceChapterRestrictions($this->chapter->fullTextSearchQuery(['name', 'description'], $terms, $whereTerms));
$chapterQuery = $this->addAdvancedSearchQueries($chapterQuery, $term);
$chapters = $chapterQuery->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
foreach ($chapters as $chapter) {
//highlight
@@ -142,19 +195,32 @@ class ChapterRepo
/**
* Changes the book relation of this chapter.
* @param $bookId
* @param $bookId
* @param Chapter $chapter
* @param bool $rebuildPermissions
* @return Chapter
*/
public function changeBook($bookId, Chapter $chapter)
public function changeBook($bookId, Chapter $chapter, $rebuildPermissions = false)
{
$chapter->book_id = $bookId;
// Update related activity
foreach ($chapter->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
$chapter->save();
// Update all child pages
foreach ($chapter->pages as $page) {
$this->pageRepo->changeBook($bookId, $page);
}
// Update permissions if applicable
if ($rebuildPermissions) {
$chapter->load('book');
$this->permissionService->buildJointPermissionsForEntity($chapter->book);
}
return $chapter;
}

285
app/Repos/EntityRepo.php Normal file
View File

@@ -0,0 +1,285 @@
<?php namespace BookStack\Repos;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\Services\PermissionService;
use BookStack\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class EntityRepo
{
/**
* @var Book $book
*/
public $book;
/**
* @var Chapter
*/
public $chapter;
/**
* @var Page
*/
public $page;
/**
* @var PermissionService
*/
protected $permissionService;
/**
* Acceptable operators to be used in a query
* @var array
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
/**
* EntityService constructor.
*/
public function __construct()
{
$this->book = app(Book::class);
$this->chapter = app(Chapter::class);
$this->page = app(Page::class);
$this->permissionService = app(PermissionService::class);
}
/**
* Get the latest books added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedBooks($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->permissionService->enforceBookRestrictions($this->book)
->orderBy('created_at', 'desc');
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->skip($page * $count)->take($count)->get();
}
/**
* Get the most recently updated books.
* @param $count
* @param int $page
* @return mixed
*/
public function getRecentlyUpdatedBooks($count = 20, $page = 0)
{
return $this->permissionService->enforceBookRestrictions($this->book)
->orderBy('updated_at', 'desc')->skip($page * $count)->take($count)->get();
}
/**
* Get the latest pages added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedPages($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->permissionService->enforcePageRestrictions($this->page)
->orderBy('created_at', 'desc')->where('draft', '=', false);
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->with('book')->skip($page * $count)->take($count)->get();
}
/**
* Get the latest chapters added to the system.
* @param int $count
* @param int $page
* @param bool $additionalQuery
* @return
*/
public function getRecentlyCreatedChapters($count = 20, $page = 0, $additionalQuery = false)
{
$query = $this->permissionService->enforceChapterRestrictions($this->chapter)
->orderBy('created_at', 'desc');
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
return $query->skip($page * $count)->take($count)->get();
}
/**
* Get the most recently updated pages.
* @param $count
* @param int $page
* @return mixed
*/
public function getRecentlyUpdatedPages($count = 20, $page = 0)
{
return $this->permissionService->enforcePageRestrictions($this->page)
->where('draft', '=', false)
->orderBy('updated_at', 'desc')->with('book')->skip($page * $count)->take($count)->get();
}
/**
* Get draft pages owned by the current user.
* @param int $count
* @param int $page
*/
public function getUserDraftPages($count = 20, $page = 0)
{
$user = auth()->user();
return $this->page->where('draft', '=', true)
->where('created_by', '=', $user->id)
->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get();
}
/**
* Updates entity restrictions from a request
* @param $request
* @param Entity $entity
*/
public function updateEntityPermissionsFromRequest($request, Entity $entity)
{
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
$entity->permissions()->delete();
if ($request->has('restrictions')) {
foreach ($request->get('restrictions') as $roleId => $restrictions) {
foreach ($restrictions as $action => $value) {
$entity->permissions()->create([
'role_id' => $roleId,
'action' => strtolower($action)
]);
}
}
}
$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;
}
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities
*/
public function buildJointPermissions(Collection $collection)
{
$this->permissionService->buildJointPermissionsForEntities($collection);
}
}

View File

@@ -2,7 +2,9 @@
use BookStack\Image;
use BookStack\Page;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use Setting;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -11,16 +13,22 @@ class ImageRepo
protected $image;
protected $imageService;
protected $restrictionService;
protected $page;
/**
* ImageRepo constructor.
* @param Image $image
* @param Image $image
* @param ImageService $imageService
* @param PermissionService $permissionService
* @param Page $page
*/
public function __construct(Image $image, ImageService $imageService)
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
{
$this->image = $image;
$this->imageService = $imageService;
$this->restrictionService = $permissionService;
$this->page = $page;
}
@@ -34,23 +42,17 @@ class ImageRepo
return $this->image->findOrFail($id);
}
/**
* Gets a load images paginated, filtered by image type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* Execute a paginated query, returning in a standard format.
* Also runs the query through the restriction system.
* @param $query
* @param int $page
* @param int $pageSize
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
private function returnPaginated($query, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
}
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
@@ -65,15 +67,74 @@ class ImageRepo
];
}
/**
* Gets a load images paginated, filtered by image type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
{
$images = $this->image->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
}
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* Search for images by query, of a particular type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param string $searchTerm
* @return array
*/
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* 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
* @return array
*/
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
{
$images = $this->image->where('type', '=', 'gallery');
$page = $this->page->findOrFail($pageId);
if ($filter === 'page') {
$images = $images->where('uploaded_to', '=', $page->id);
} elseif ($filter === 'book') {
$validPageIds = $page->book->pages->pluck('id')->toArray();
$images = $images->whereIn('uploaded_to', $validPageIds);
}
return $this->returnPaginated($images, $pagination, $pageSize);
}
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @return Image
*/
public function saveNew(UploadedFile $uploadFile, $type)
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type);
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$this->loadThumbs($image);
return $image;
}
@@ -123,9 +184,9 @@ class ImageRepo
* 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
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)

View File

@@ -1,59 +1,57 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use Carbon\Carbon;
use DOMDocument;
use Illuminate\Support\Str;
use BookStack\Page;
use BookStack\PageRevision;
class PageRepo
class PageRepo extends EntityRepo
{
protected $page;
protected $pageRevision;
protected $tagRepo;
/**
* PageRepo constructor.
* @param Page $page
* @param PageRevision $pageRevision
* @param TagRepo $tagRepo
*/
public function __construct(Page $page, PageRevision $pageRevision)
public function __construct(PageRevision $pageRevision, TagRepo $tagRepo)
{
$this->page = $page;
$this->pageRevision = $pageRevision;
$this->tagRepo = $tagRepo;
parent::__construct();
}
/**
* Check if a page id exists.
* @param $id
* @return bool
* Base query for getting pages, Takes restrictions into account.
* @param bool $allowDrafts
* @return mixed
*/
public function idExists($id)
private function pageQuery($allowDrafts = false)
{
return $this->page->where('page_id', '=', $id)->count() > 0;
$query = $this->permissionService->enforcePageRestrictions($this->page, 'view');
if (!$allowDrafts) {
$query = $query->where('draft', '=', false);
}
return $query;
}
/**
* Get a page via a specific ID.
* @param $id
* @param bool $allowDrafts
* @return mixed
*/
public function getById($id)
public function getById($id, $allowDrafts = false)
{
return $this->page->findOrFail($id);
}
/**
* Get all pages.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->page->all();
return $this->pageQuery($allowDrafts)->findOrFail($id);
}
/**
@@ -61,15 +59,37 @@ class PageRepo
* @param $slug
* @param $bookId
* @return mixed
* @throws NotFoundException
*/
public function getBySlug($slug, $bookId)
{
$page = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) abort(404);
$page = $this->pageQuery()->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
if ($page === null) throw new NotFoundException('Page not found');
return $page;
}
/**
* Search through page revisions and retrieve
* the last page in the current book that
* has a slug equal to the one given.
* @param $pageSlug
* @param $bookSlug
* @return null | Page
*/
public function findPageUsingOldSlug($pageSlug, $bookSlug)
{
$revision = $this->pageRevision->where('slug', '=', $pageSlug)
->whereHas('page', function ($query) {
$this->permissionService->enforcePageRestrictions($query);
})
->where('type', '=', 'version')
->where('book_slug', '=', $bookSlug)->orderBy('created_at', 'desc')
->with('page')->first();
return $revision !== null ? $revision->page : null;
}
/**
* Get a new Page instance from the given input.
* @param $input
* @return Page
*/
@@ -94,8 +114,8 @@ class PageRepo
* Save a new page into the system.
* Input validation must be done beforehand.
* @param array $input
* @param Book $book
* @param int $chapterId
* @param Book $book
* @param int $chapterId
* @return Page
*/
public function saveNew(array $input, Book $book, $chapterId = null)
@@ -114,6 +134,55 @@ class PageRepo
return $page;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
* @param Page $draftPage
* @param array $input
* @return Page
*/
public function publishDraft(Page $draftPage, array $input)
{
$draftPage->fill($input);
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
}
$draftPage->slug = $this->findSuitableSlug($draftPage->name, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->draft = false;
$draftPage->save();
$this->saveRevision($draftPage, 'Initial Publish');
return $draftPage;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return static
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = 'New Page';
$page->created_by = auth()->user()->id;
$page->updated_by = auth()->user()->id;
$page->draft = true;
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
@@ -122,24 +191,23 @@ class PageRepo
*/
protected function formatHtml($htmlText)
{
if($htmlText == '') return $htmlText;
if ($htmlText == '') return $htmlText;
libxml_use_internal_errors(true);
$doc = new \DOMDocument();
$doc->loadHTML($htmlText);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$lastId = false;
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
// Overwrite id if not a bookstack custom id
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
@@ -149,13 +217,18 @@ class PageRepo
}
// Create an unique id for the element
do {
$id = 'bkmrk-' . substr(uniqid(), -5);
} while ($id == $lastId);
$lastId = $id;
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $id);
$idArray[] = $id;
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
}
// Generate inner html as a string
@@ -171,14 +244,18 @@ class PageRepo
/**
* Gets pages by a search term.
* Highlights page content for showing in results.
* @param string $term
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
public function getBySearch($term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = explode(' ', $term);
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
$terms = $this->prepareSearchTerms($term);
$pageQuery = $this->permissionService->enforcePageRestrictions($this->page->fullTextSearchQuery(['name', 'text'], $terms, $whereTerms));
$pageQuery = $this->addAdvancedSearchQueries($pageQuery, $term);
$pages = $pageQuery->paginate($count)->appends($paginationAppends);
// Add highlights to page text.
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
@@ -215,7 +292,7 @@ class PageRepo
*/
public function searchForImage($imageString)
{
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
$pages = $this->pageQuery()->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
@@ -226,25 +303,44 @@ class PageRepo
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param Page $page
* @param int $book_id
* @param string $input
* @return Page
*/
public function updatePage(Page $page, $book_id, $input)
{
// Save a revision before updating
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
$this->saveRevision($page);
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
// Prevent slug being updated if no name change
if ($page->name !== $input['name']) {
$page->slug = $this->findSuitableSlug($input['name'], $book_id, $page->id);
}
// Save page tags if present
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
}
// Update with new details
$userId = auth()->user()->id;
$page->fill($input);
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->updated_by = auth()->user()->id;
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId;
$page->save();
// Remove all update drafts for this user & page.
$this->userUpdateDraftsQuery($page, $userId)->delete();
// Save a revision after updating
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
$this->saveRevision($page, $input['summary']);
}
return $page;
}
@@ -270,14 +366,20 @@ class PageRepo
/**
* Saves a page revision into the system.
* @param Page $page
* @param null|string $summary
* @return $this
*/
public function saveRevision(Page $page)
public function saveRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->fill($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
$revision->created_by = auth()->user()->id;
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
@@ -287,6 +389,155 @@ class PageRepo
return $revision;
}
/**
* Save a page update draft.
* @param Page $page
* @param array $data
* @return PageRevision
*/
public function saveUpdateDraft(Page $page, $data = [])
{
$userId = auth()->user()->id;
$drafts = $this->userUpdateDraftsQuery($page, $userId)->get();
if ($drafts->count() > 0) {
$draft = $drafts->first();
} else {
$draft = $this->pageRevision->newInstance();
$draft->page_id = $page->id;
$draft->slug = $page->slug;
$draft->book_slug = $page->book->slug;
$draft->created_by = $userId;
$draft->type = 'update_draft';
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
$draft->save();
return $draft;
}
/**
* Update a draft page.
* @param Page $page
* @param array $data
* @return Page
*/
public function updateDraftPage(Page $page, $data = [])
{
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
}
$page->save();
return $page;
}
/**
* The base query for getting user update drafts.
* @param Page $page
* @param $userId
* @return mixed
*/
private function userUpdateDraftsQuery(Page $page, $userId)
{
return $this->pageRevision->where('created_by', '=', $userId)
->where('type', 'update_draft')
->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc');
}
/**
* Checks whether a user has a draft version of a particular page or not.
* @param Page $page
* @param $userId
* @return bool
*/
public function hasUserGotPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->count() > 0;
}
/**
* Get the latest updated draft revision for a particular page and user.
* @param Page $page
* @param $userId
* @return mixed
*/
public function getUserPageDraft(Page $page, $userId)
{
return $this->userUpdateDraftsQuery($page, $userId)->first();
}
/**
* Get the notification message that informs the user that they are editing a draft page.
* @param PageRevision $draft
* @return string
*/
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = 'You are currently editing a draft that was last saved ' . $draft->updated_at->diffForHumans() . '.';
if ($draft->page->updated_at->timestamp > $draft->updated_at->timestamp) {
$message .= "\n This page has been updated by since that time. It is recommended that you discard this draft.";
}
return $message;
}
/**
* Check if a page is being actively editing.
* Checks for edits since last page updated.
* Passing in a minuted range will check for edits
* within the last x minutes.
* @param Page $page
* @param null $minRange
* @return bool
*/
public function isPageEditingActive(Page $page, $minRange = null)
{
$draftSearch = $this->activePageEditingQuery($page, $minRange);
return $draftSearch->count() > 0;
}
/**
* Get a notification message concerning the editing activity on
* a particular page.
* @param Page $page
* @param null $minRange
* @return string
*/
public function getPageEditingActiveMessage(Page $page, $minRange = null)
{
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
$userMessage = $pageDraftEdits->count() > 1 ? $pageDraftEdits->count() . ' users have' : $pageDraftEdits->first()->createdBy->name . ' has';
$timeMessage = $minRange === null ? 'since the page was last updated' : 'in the last ' . $minRange . ' minutes';
$message = '%s started editing this page %s. Take care not to overwrite each other\'s updates!';
return sprintf($message, $userMessage, $timeMessage);
}
/**
* A query to check for active update drafts on a particular page.
* @param Page $page
* @param null $minRange
* @return mixed
*/
private function activePageEditingQuery(Page $page, $minRange = null)
{
$query = $this->pageRevision->where('type', '=', 'update_draft')
->where('page_id', '=', $page->id)
->where('updated_at', '>', $page->updated_at)
->where('created_by', '!=', auth()->user()->id)
->with('createdBy');
if ($minRange !== null) {
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
}
return $query;
}
/**
* Gets a single revision via it's id.
* @param $id
@@ -314,7 +565,7 @@ class PageRepo
/**
* Changes the related book for the specified page.
* Changes the book id of any relations to the page that store the book id.
* @param int $bookId
* @param int $bookId
* @param Page $page
* @return Page
*/
@@ -330,16 +581,33 @@ class PageRepo
return $page;
}
/**
* Change the page's parent to the given entity.
* @param Page $page
* @param Entity $parent
*/
public function changePageParent(Page $page, Entity $parent)
{
$book = $parent->isA('book') ? $parent : $parent->book;
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
$page->save();
$page = $this->changeBook($book->id, $page);
$page->load('book');
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Gets a suitable slug for the resource
* @param $name
* @param $bookId
* @param string $name
* @param int $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
@@ -350,13 +618,33 @@ class PageRepo
* Destroy a given page along with its dependencies.
* @param $page
*/
public function destroy($page)
public function destroy(Page $page)
{
Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$page->delete();
}
/**
* Get the latest pages added to the system.
* @param $count
*/
public function getRecentlyCreatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('created_at', 'desc')->paginate($count);
}
}
/**
* Get the latest pages added to the system.
* @param $count
*/
public function getRecentlyUpdatedPaginated($count = 20)
{
return $this->pageQuery()->orderBy('updated_at', 'desc')->paginate($count);
}
}

View File

@@ -0,0 +1,155 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\RolePermission;
use BookStack\Role;
use BookStack\Services\PermissionService;
use Setting;
class PermissionsRepo
{
protected $permission;
protected $role;
protected $permissionService;
protected $systemRoles = ['admin', 'public'];
/**
* PermissionsRepo constructor.
* @param RolePermission $permission
* @param Role $role
* @param PermissionService $permissionService
*/
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
{
$this->permission = $permission;
$this->role = $role;
$this->permissionService = $permissionService;
}
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
{
return $this->role->where('hidden', '=', false)->get();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
{
return $this->role->where('id', '!=', $role->id)->where('hidden', '=', false)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
{
return $this->role->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(str_random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionService->buildJointPermissionForRole($role);
return $role;
}
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
*/
public function updateRole($roleId, $roleData)
{
$role = $this->role->findOrFail($roleId);
if ($role->hidden) throw new PermissionsException("Cannot update a hidden role");
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
}
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);
}
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
public function assignRolePermissions(Role $role, $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray && count($permissionNameArray) > 0) {
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
}
$role->permissions()->sync($permissions);
}
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
throw new PermissionsException('This role is a system role and cannot be deleted');
} else if ($role->id == setting('registration-role')) {
throw new PermissionsException('This role cannot be deleted while set as the default registration role.');
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}
$this->permissionService->deleteJointPermissionsForRole($role);
$role->delete();
}
}

135
app/Repos/TagRepo.php Normal file
View File

@@ -0,0 +1,135 @@
<?php namespace BookStack\Repos;
use BookStack\Tag;
use BookStack\Entity;
use BookStack\Services\PermissionService;
/**
* Class TagRepo
* @package BookStack\Repos
*/
class TagRepo
{
protected $tag;
protected $entity;
protected $permissionService;
/**
* TagRepo constructor.
* @param Tag $attr
* @param Entity $ent
* @param PermissionService $ps
*/
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
{
$this->tag = $attr;
$this->entity = $ent;
$this->permissionService = $ps;
}
/**
* Get an entity instance of its particular type.
* @param $entityType
* @param $entityId
* @param string $action
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
$entityInstance = $this->entity->getEntityInstance($entityType);
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
$searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
return $searchQuery->first();
}
/**
* Get all tags for a particular entity.
* @param string $entityType
* @param int $entityId
* @return mixed
*/
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) return collect();
return $entity->tags;
}
/**
* Get tag name suggestions from scanning existing tag names.
* If no search term is given the 50 most popular tag names are provided.
* @param $searchTerm
* @return array
*/
public function getNameSuggestions($searchTerm = false)
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
} else {
$query = $query->orderBy('count', 'desc')->take(50);
}
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
return $query->get(['name'])->pluck('name');
}
/**
* Get tag value suggestions from scanning existing tag values.
* If no search is given the 50 most popular values are provided.
* Passing a tagName will only find values for a tags with a particular name.
* @param $searchTerm
* @param $tagName
* @return array
*/
public function getValueSuggestions($searchTerm = false, $tagName = false)
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
} else {
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
return $query->get(['value'])->pluck('value');
}
/**
* Save an array of tags to an entity
* @param Entity $entity
* @param array $tags
* @return array|\Illuminate\Database\Eloquent\Collection
*/
public function saveTagsToEntity(Entity $entity, $tags = [])
{
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') continue;
$newTags[] = $this->newInstanceFromInput($tag);
}
return $entity->tags()->saveMany($newTags);
}
/**
* Create a new Tag instance from user input.
* @param $input
* @return static
*/
protected function newInstanceFromInput($input)
{
$name = trim($input['name']);
$value = isset($input['value']) ? trim($input['value']) : '';
// Any other modification or cleanup required can go here
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Repos;
use BookStack\Role;
use BookStack\User;
use Setting;
@@ -10,15 +9,19 @@ class UserRepo
protected $user;
protected $role;
protected $entityRepo;
/**
* UserRepo constructor.
* @param $user
* @param User $user
* @param Role $role
* @param EntityRepo $entityRepo
*/
public function __construct(User $user, Role $role)
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
{
$this->user = $user;
$this->role = $role;
$this->entityRepo = $entityRepo;
}
/**
@@ -39,6 +42,36 @@ class UserRepo
return $this->user->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsers()
{
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count = 20, $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('name', 'like', $term)
->orWhere('email', 'like', $term);
});
}
return $query->paginate($count);
}
/**
* Creates a new user and attaches a role to them.
* @param array $data
@@ -65,8 +98,8 @@ class UserRepo
*/
public function attachDefaultRole($user)
{
$roleId = Setting::get('registration-role');
if ($roleId === false) $roleId = $this->role->getDefault()->id;
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
$user->attachRoleId($roleId);
}
@@ -77,15 +110,10 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if ($user->role->name != 'admin') {
return false;
}
$adminRole = $this->role->where('name', '=', 'admin')->first();
if (count($adminRole->users) > 1) {
return false;
}
if (!$user->roles->pluck('name')->contains('admin')) return false;
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
return true;
}
@@ -99,7 +127,8 @@ class UserRepo
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password'])
'password' => bcrypt($data['password']),
'email_confirmed' => false
]);
}
@@ -112,4 +141,71 @@ class UserRepo
$user->socialAccounts()->delete();
$user->delete();
}
/**
* Get the latest activity for a user.
* @param User $user
* @param int $count
* @param int $page
* @return array
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
}
/**
* Get the recently created content for this given user.
* @param User $user
* @param int $count
* @return mixed
*/
public function getRecentlyCreated(User $user, $count = 20)
{
return [
'pages' => $this->entityRepo->getRecentlyCreatedPages($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreatedChapters($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreatedBooks($count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
];
}
/**
* Get asset created counts for the give user.
* @param User $user
* @return array
*/
public function getAssetCounts(User $user)
{
return [
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
];
}
/**
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAssignableRoles()
{
return $this->role->visible();
}
/**
* Get all the roles which can be given restricted access to
* other entities in the system.
* @return mixed
*/
public function getRestrictableRoles()
{
return $this->role->where('hidden', '=', false)->where('system_name', '=', '')->get();
}
}

View File

@@ -1,49 +1,66 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
/**
* Sets the default role name for newly registered users.
* @var string
*/
protected static $default = 'viewer';
protected $fillable = ['display_name', 'description'];
/**
* The roles that belong to the role.
*/
public function users()
{
return $this->belongsToMany('BookStack\User');
return $this->belongsToMany(User::class);
}
/**
* The permissions that belong to the role.
* Get all related JointPermissions.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function jointPermissions()
{
return $this->hasMany(JointPermission::class);
}
/**
* The RolePermissions that belong to the role.
*/
public function permissions()
{
return $this->belongsToMany('BookStack\Permission');
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
/**
* Check if this role has a permission.
* @param $permissionName
* @return bool
*/
public function hasPermission($permissionName)
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
if ($permission->getRawAttribute('name') === $permissionName) return true;
}
return false;
}
/**
* Add a permission to this role.
* @param Permission $permission
* @param RolePermission $permission
*/
public function attachPermission(Permission $permission)
public function attachPermission(RolePermission $permission)
{
$this->permissions()->attach($permission->id);
}
/**
* Get an instance of the default role.
* @return Role
* Detach a single permission from this role.
* @param RolePermission $permission
*/
public static function getDefault()
public function detachPermission(RolePermission $permission)
{
return static::getRole(static::$default);
$this->permissions()->detach($permission->id);
}
/**
@@ -55,4 +72,24 @@ class Role extends Model
{
return static::where('name', '=', $roleName)->first();
}
/**
* Get the role object for the specified system role.
* @param $roleName
* @return mixed
*/
public static function getSystemRole($roleName)
{
return static::where('system_name', '=', $roleName)->first();
}
/**
* Get all visible roles
* @return mixed
*/
public static function visible()
{
return static::where('hidden', '=', false)->orderBy('name')->get();
}
}

23
app/RolePermission.php Normal file
View File

@@ -0,0 +1,23 @@
<?php namespace BookStack;
class RolePermission extends Model
{
/**
* The roles that belong to the permission.
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
}
/**
* Get the permission object by name.
* @param $name
* @return mixed
*/
public static function getByName($name)
{
return static::where('name', '=', $name)->first();
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
use Illuminate\Support\Facades\Auth;
use BookStack\Activity;
use BookStack\Entity;
use Session;
@@ -9,14 +8,17 @@ class ActivityService
{
protected $activity;
protected $user;
protected $permissionService;
/**
* ActivityService constructor.
* @param $activity
* @param Activity $activity
* @param PermissionService $permissionService
*/
public function __construct(Activity $activity)
public function __construct(Activity $activity, PermissionService $permissionService)
{
$this->activity = $activity;
$this->permissionService = $permissionService;
$this->user = auth()->user();
}
@@ -24,25 +26,26 @@ class ActivityService
* Add activity data to database.
* @param Entity $entity
* @param $activityKey
* @param int $bookId
* @param bool $extra
* @param int $bookId
* @param bool $extra
*/
public function add(Entity $entity, $activityKey, $bookId = 0, $extra = false)
{
$this->activity->user_id = $this->user->id;
$this->activity->book_id = $bookId;
$this->activity->key = strtolower($activityKey);
$activity = $this->activity->newInstance();
$activity->user_id = $this->user->id;
$activity->book_id = $bookId;
$activity->key = strtolower($activityKey);
if ($extra !== false) {
$this->activity->extra = $extra;
$activity->extra = $extra;
}
$entity->activity()->save($this->activity);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
}
/**
* Adds a activity history with a message & without binding to a entitiy.
* Adds a activity history with a message & without binding to a entity.
* @param $activityKey
* @param int $bookId
* @param int $bookId
* @param bool|false $extra
*/
public function addMessage($activityKey, $bookId = 0, $extra = false)
@@ -85,37 +88,63 @@ class ActivityService
*/
public function latest($count = 20, $page = 0)
{
$activityList = $this->activity->orderBy('created_at', 'desc')
->skip($count * $page)->take($count)->get();
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->with('user', 'entity')->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activityList);
}
/**
* Gets the latest activity for an entitiy, Filtering out similar
* Gets the latest activity for an entity, Filtering out similar
* items to prevent a message activity list.
* @param Entity $entity
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @return array
*/
function entityActivity($entity, $count = 20, $page = 0)
public function entityActivity($entity, $count = 20, $page = 0)
{
$activity = $entity->hasMany('BookStack\Activity')->orderBy('created_at', 'desc')
->skip($count * $page)->take($count)->get();
if ($entity->isA('book')) {
$query = $this->activity->where('book_id', '=', $entity->id);
} else {
$query = $this->activity->where('entity_type', '=', get_class($entity))
->where('entity_id', '=', $entity->id);
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activity);
}
/**
* Filters out similar activity.
* @param Activity[] $activity
* Get latest activity for a user, Filtering out similar
* items.
* @param $user
* @param int $count
* @param int $page
* @return array
*/
protected function filterSimilar($activity)
public function userActivity($user, $count = 20, $page = 0)
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
return $this->filterSimilar($activityList);
}
/**
* Filters out similar activity.
* @param Activity[] $activities
* @return array
*/
protected function filterSimilar($activities)
{
$newActivity = [];
$previousItem = false;
foreach ($activity as $activityItem) {
foreach ($activities as $activityItem) {
if ($previousItem === false) {
$previousItem = $activityItem;
$newActivity[] = $activityItem;

View File

@@ -45,7 +45,7 @@ class EmailConfirmationService
'token' => $token,
]);
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
$appName = \Setting::get('app-name', 'BookStack');
$appName = setting('app-name', 'BookStack');
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
});
}

View File

@@ -48,11 +48,13 @@ class ExportService
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$pathString = public_path($srcString);
$isLocal = strpos(trim($srcString), 'http') !== 0;
if ($isLocal) {
$pathString = public_path(trim($srcString, '/'));
} else {
$pathString = $srcString;
}
if ($isLocal && !file_exists($pathString)) continue;
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);

View File

@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@@ -40,14 +41,16 @@ class ImageService
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @return mixed
* @throws ImageUploadException
*/
public function saveNewFromUpload(UploadedFile $uploadedFile, $type)
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
$imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath());
return $this->saveNew($imageName, $imageData, $type);
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
@@ -72,13 +75,14 @@ class ImageService
* @param string $imageName
* @param string $imageData
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
private function saveNew($imageName, $imageData, $type)
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
{
$storage = $this->getStorage();
$secureUploads = Setting::get('app-secure-images');
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
@@ -91,6 +95,7 @@ class ImageService
try {
$storage->put($fullPath, $imageData);
$storage->setVisibility($fullPath, 'public');
} catch (Exception $e) {
throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.');
}
@@ -99,7 +104,8 @@ class ImageService
'name' => $imageName,
'path' => $fullPath,
'url' => $this->getPublicUrl($fullPath),
'type' => $type
'type' => $type,
'uploaded_to' => $uploadedTo
];
if (auth()->user() && auth()->user()->id !== 0) {
@@ -119,10 +125,12 @@ class ImageService
* 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
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws Exception
* @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
@@ -139,8 +147,16 @@ class ImageService
return $this->getPublicUrl($thumbFilePath);
}
// Otherwise create the thumbnail
$thumb = $this->imageTool->make($storage->get($image->path));
try {
$thumb = $this->imageTool->make($storage->get($image->path));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
} else {
throw $e;
}
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
@@ -152,6 +168,7 @@ class ImageService
$thumbData = (string)$thumb->encode();
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
@@ -242,16 +259,22 @@ class ImageService
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if ($storageUrl == false && config('filesystems.default') === 's3') {
$storageDetails = config('filesystems.disks.s3');
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
}
$this->storageUrl = $storageUrl;
}
return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath;
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
}
}
}

View File

@@ -33,6 +33,17 @@ class Ldap
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Set the version number for the given ldap connection.
* @param $ldapConnection
* @param $version
* @return bool
*/
public function setVersion($ldapConnection, $version)
{
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
}
/**
* Search LDAP tree using the provided filter.
* @param resource $ldapConnection

View File

@@ -46,7 +46,7 @@ class LdapService
$user = $users[0];
return [
'uid' => $user['uid'][0],
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
@@ -122,7 +122,7 @@ class LdapService
// Set any required options
if ($this->config['version']) {
$this->ldap->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']);
$this->ldap->setVersion($ldapConnection, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;

View File

@@ -0,0 +1,581 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\JointPermission;
use BookStack\Ownable;
use BookStack\Page;
use BookStack\Role;
use BookStack\User;
use Illuminate\Support\Collection;
class PermissionService
{
protected $userRoles;
protected $isAdmin;
protected $currentAction;
protected $currentUser;
public $book;
public $chapter;
public $page;
protected $jointPermission;
protected $role;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, Book $book, Chapter $chapter, Page $page, Role $role)
{
$this->currentUser = auth()->user();
$userSet = $this->currentUser !== null;
$this->userRoles = false;
$this->isAdmin = $userSet ? $this->currentUser->hasRole('admin') : false;
if (!$userSet) $this->currentUser = new User();
$this->jointPermission = $jointPermission;
$this->role = $role;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
}
/**
* Prepare the local entity cache and ensure it's empty
*/
protected function readyEntityCache()
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
}
/**
* Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
}
return $book;
}
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return Book
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
}
return $chapter;
}
/**
* Get the roles for the current user;
* @return array|bool
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
$roles = [];
if (auth()->guest()) {
$roles[] = $this->role->getSystemRole('public')->id;
return $roles;
}
foreach ($this->currentUser->roles as $role) {
$roles[] = $role->id;
}
return $roles;
}
/**
* Re-generate all entity permission from scratch.
*/
public function buildJointPermissions()
{
$this->jointPermission->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get();
// 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 ($chapters) use ($roles) {
$this->createManyJointPermissions($chapters, $roles);
});
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
$this->createManyJointPermissions($pages, $roles);
});
}
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$roles = $this->role->with('jointPermissions')->get();
$entities = collect([$entity]);
if ($entity->isA('book')) {
$entities = $entities->merge($entity->chapters);
$entities = $entities->merge($entity->pages);
} elseif ($entity->isA('chapter')) {
$entities = $entities->merge($entity->pages);
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->with('jointPermissions')->get();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Build the entity jointPermissions for a particular role.
* @param Role $role
*/
public function buildJointPermissionForRole(Role $role)
{
$roles = collect([$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);
});
}
/**
* Delete the entity jointPermissions attached to a particular role.
* @param Role $role
*/
public function deleteJointPermissionsForRole(Role $role)
{
$this->deleteManyJointPermissionsForRoles([$role]);
}
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Role[] $roles
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
foreach ($roles as $role) {
$role->jointPermissions()->delete();
}
}
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
$this->deleteManyJointPermissionsForEntities([$entity]);
}
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
$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());
});
}
$query->delete();
}
/**
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param Collection $roles
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$jointPermissions = [];
foreach ($entities as $entity) {
foreach ($roles as $role) {
foreach ($this->getActions($entity) as $action) {
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
}
}
}
$this->jointPermission->insert($jointPermissions);
}
/**
* Get the actions related to an entity.
* @param $entity
* @return array
*/
protected function getActions($entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter')) {
$baseActions[] = 'page-create';
} else if ($entity->isA('book')) {
$baseActions[] = 'page-create';
$baseActions[] = 'chapter-create';
}
return $baseActions;
}
/**
* Create entity permission data for an entity and role
* for a particular action.
* @param Entity $entity
* @param Role $role
* @param $action
* @return array
*/
protected function createJointPermissionData(Entity $entity, Role $role, $action)
{
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
$explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction);
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);
}
}
}
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
* @param Entity $entity
* @param Role $role
* @param $action
* @param $permissionAll
* @param $permissionOwn
* @return array
*/
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,
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
'created_by' => $entity->getRawAttribute('created_by')
];
}
/**
* Checks if an entity has a restriction set upon it.
* @param Ownable $ownable
* @param $permission
* @return bool
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin) return true;
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
$allPermission = $this->currentUser && $this->currentUser->can($permission . '-all');
$ownPermission = $this->currentUser && $this->currentUser->can($permission . '-own');
$this->currentAction = 'view';
$isOwner = $this->currentUser && $this->currentUser->id === $ownable->created_by;
return ($allPermission || ($isOwner && $ownPermission));
}
// Handle abnormal create jointPermissions
if ($action === 'create') {
$this->currentAction = $permission;
}
return $this->entityRestrictionQuery($baseQuery)->count() > 0;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/**
* The general query filter to remove all entities
* that the current user does not have access to.
* @param $query
* @return mixed
*/
protected function entityRestrictionQuery($query)
{
return $query->where(function ($parentQuery) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getRoles())
->where('action', '=', $this->currentAction)
->where(function ($query) {
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
});
});
});
});
}
/**
* Add restrictions for a page query
* @param $query
* @param string $action
* @return mixed
*/
public function enforcePageRestrictions($query, $action = 'view')
{
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
$query->where('draft', '=', false);
if ($this->currentUser) {
$query->orWhere(function ($query) {
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser->id);
});
}
});
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add on permission restrictions to a chapter query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceChapterRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add restrictions to a book query.
* @param $query
* @param string $action
* @return mixed
*/
public function enforceBookRestrictions($query, $action = 'view')
{
return $this->enforceEntityRestrictions($query, $action);
}
/**
* Add restrictions for a generic entity
* @param $query
* @param string $action
* @return mixed
*/
public function enforceEntityRestrictions($query, $action = 'view')
{
if ($this->isAdmin) return $query;
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
/**
* Filter items that have entities set a a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{
if ($this->isAdmin) return $query;
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
return $query->where(function ($query) use ($tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
});
});
});
});
}
/**
* Filters pages that are a direct relation to another item.
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin) return $query;
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
return $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where('entity_type', '=', 'Bookstack\\Page')
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser->id);
});
});
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
}
}

View File

@@ -44,28 +44,39 @@ class SettingService
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
* @param $key
* @param $default
* @return mixed
*/
protected function getValueFromStore($key, $default)
{
// Check for an overriding value
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
// Check the cache
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
if ($settingObject !== null) {
$value = $settingObject->value;
$this->cache->forever($cacheKey, $value);
return $value;
}
// Check the defaults set in the app config.
$configPrefix = 'setting-defaults.' . $key;
if (config()->has($configPrefix)) {
$value = config($configPrefix);
$this->cache->forever($cacheKey, $value);
return $value;
}
return $default;
}

View File

@@ -1,14 +1,11 @@
<?php namespace BookStack\Services;
use GuzzleHttp\Exception\ClientException;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Auth\AuthController;
use BookStack\Repos\UserRepo;
use BookStack\SocialAccount;
use BookStack\User;
class SocialAuthService
{
@@ -116,28 +113,29 @@ class SocialAuthService
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
session()->flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
return redirect($currentUser->getEditUrl());
}
// When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
session()->flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
return redirect($currentUser->getEditUrl());
}
// When a user is logged in, A social account exists but the users do not match.
// Change the user that the social account is assigned to.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
session()->flash('success', 'This ' . title_case($socialDriver) . ' account is already used by another user.');
return redirect($currentUser->getEditUrl());
}
// Otherwise let the user know this social account is not used by anyone.
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
if (\Setting::get('registration-enabled')) {
if (setting('registration-enabled')) {
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
}
throw new SocialSignInException($message . '.', '/login');
}
@@ -160,7 +158,7 @@ class SocialAuthService
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured("Your {$driver} social settings are not configured correctly.");
return $driver;
}
@@ -217,7 +215,7 @@ class SocialAuthService
{
session();
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
\Session::flash('success', $socialDriver . ' account successfully detached');
session()->flash('success', title_case($socialDriver) . ' account successfully detached');
return redirect(auth()->user()->getEditUrl());
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Services;
use BookStack\Entity;
use BookStack\View;
@@ -9,15 +8,18 @@ class ViewService
protected $view;
protected $user;
protected $permissionService;
/**
* ViewService constructor.
* @param $view
* @param View $view
* @param PermissionService $permissionService
*/
public function __construct(View $view)
public function __construct(View $view, PermissionService $permissionService)
{
$this->view = $view;
$this->user = auth()->user();
$this->permissionService = $permissionService;
}
/**
@@ -27,7 +29,7 @@ class ViewService
*/
public function add(Entity $entity)
{
if($this->user === null) return 0;
if ($this->user === null) return 0;
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
// Add view if model exists
if ($view) {
@@ -44,52 +46,51 @@ class ViewService
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param bool|false $filterModel
* @param int $count
* @param int $page
* @param bool|false|array $filterModel
*/
public function getPopular($count = 10, $page = 0, $filterModel = false)
{
$skipCount = $count * $page;
$query = $this->view->select('id', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
if ($filterModel && is_array($filterModel)) {
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', get_class($filterModel));
};
$views = $query->with('viewable')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}
/**
* Get all recently viewed entities for the current user.
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @param Entity|bool $filterModel
* @return mixed
*/
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
if($this->user === null) return collect();
$skipCount = $count * $page;
$query = $this->view->where('user_id', '=', auth()->user()->id);
if ($this->user === null) return collect();
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', auth()->user()->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');
return $viewables;
}
/**
* Reset all view counts by deleting all views.
*/
@@ -98,5 +99,4 @@ class ViewService
$this->view->truncate();
}
}

View File

@@ -1,8 +1,4 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
<?php namespace BookStack;
class Setting extends Model
{

View File

@@ -1,8 +1,5 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class SocialAccount extends Model
{
@@ -11,6 +8,6 @@ class SocialAccount extends Model
public function user()
{
return $this->belongsTo('BookStack\User');
return $this->belongsTo(User::class);
}
}

19
app/Tag.php Normal file
View File

@@ -0,0 +1,19 @@
<?php namespace BookStack;
/**
* Class Attribute
* @package BookStack
*/
class Tag extends Model
{
protected $fillable = ['name', 'value', 'order'];
/**
* Get the entity that this tag belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
return $this->morphTo('entity');
}
}

View File

@@ -1,9 +1,6 @@
<?php
namespace BookStack;
<?php namespace BookStack;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
@@ -14,21 +11,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
@@ -50,33 +44,38 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
]);
}
/**
* Permissions and roles
*/
/**
* The roles that belong to the user.
*/
public function roles()
{
return $this->belongsToMany('BookStack\Role');
}
public function getRoleAttribute()
{
return $this->roles()->with('permissions')->first();
return $this->belongsToMany(Role::class);
}
/**
* Loads the user's permissions from their role.
* Check if the user has a role.
* @param $role
* @return mixed
*/
private function loadPermissions()
public function hasRole($role)
{
if (isset($this->permissions)) return;
return $this->roles->pluck('name')->contains($role);
}
/**
* Get all permissions belonging to a the current user.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function permissions($cache = true)
{
if(isset($this->permissions) && $cache) return $this->permissions;
$this->load('roles.permissions');
$permissions = $this->roles[0]->permissions;
$permissionsArray = $permissions->pluck('name')->all();
$this->permissions = $permissionsArray;
$permissions = $this->roles->map(function($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
}
/**
@@ -86,11 +85,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if ($this->email == 'guest') {
return false;
}
$this->loadPermissions();
return array_search($permissionName, $this->permissions) !== false;
if ($this->email === 'guest') return false;
return $this->permissions()->pluck('name')->contains($permissionName);
}
/**
@@ -108,17 +104,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function attachRoleId($id)
{
$this->roles()->sync([$id]);
$this->roles()->attach($id);
}
/**
* Get the social account associated with this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function socialAccounts()
{
return $this->hasMany('BookStack\SocialAccount');
return $this->hasMany(SocialAccount::class);
}
/**
@@ -138,15 +133,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Returns the user's avatar,
* Uses Gravatar as the avatar service.
*
* @param int $size
* @return string
*/
public function getAvatar($size = 50)
{
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return '/user_avatar.png';
return $this->avatar->getThumb($size, $size, false);
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png');
return baseUrl($this->avatar->getThumb($size, $size, false));
}
/**
@@ -155,7 +148,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function avatar()
{
return $this->belongsTo('BookStack\Image', 'image_id');
return $this->belongsTo(Image::class, 'image_id');
}
/**
@@ -164,6 +157,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getEditUrl()
{
return '/users/' . $this->id;
return baseUrl('/settings/users/' . $this->id);
}
/**
* Get the url that links to this user's profile.
* @return mixed
*/
public function getProfileUrl()
{
return baseUrl('/user/' . $this->id);
}
/**
* Get a shortened version of the user's name.
* @param int $chars
* @return string
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) return $this->name;
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) return $splitName[0];
return '';
}
}

View File

@@ -1,8 +1,4 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
<?php namespace BookStack;
class View extends Model
{

View File

@@ -1,30 +1,142 @@
<?php
if (! function_exists('versioned_asset')) {
/**
* Get the path to a versioned file.
*
* @param string $file
* @return string
*
* @throws \InvalidArgumentException
*/
function versioned_asset($file)
{
static $manifest = null;
use BookStack\Ownable;
if (is_null($manifest)) {
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
/**
* Get the path to a versioned file.
*
* @param string $file
* @return string
* @throws Exception
*/
function versioned_asset($file = '')
{
// Don't require css and JS assets for testing
if (config('app.env') === 'testing') return '';
static $manifest = null;
$manifestPath = 'build/manifest.json';
if (is_null($manifest) && file_exists($manifestPath)) {
$manifest = json_decode(file_get_contents(public_path($manifestPath)), true);
} else if (!file_exists($manifestPath)) {
if (config('app.env') !== 'production') {
$path = public_path($manifestPath);
$error = "No {$path} file found, Ensure you have built the css/js assets using gulp.";
} else {
$error = "No {$manifestPath} file found, Ensure you are using the release version of BookStack";
}
if (isset($manifest[$file])) {
return '/' . $manifest[$file];
}
if (file_exists(public_path($file))) {
return '/' . $file;
}
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
throw new \Exception($error);
}
if (isset($manifest[$file])) {
return baseUrl($manifest[$file]);
}
throw new InvalidArgumentException("File {$file} not defined in asset manifest.");
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
* that particular item.
* @param $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan($permission, Ownable $ownable = null)
{
if ($ownable === null) {
return auth()->user() && auth()->user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(\BookStack\Services\PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
/**
* Helper to access system settings.
* @param $key
* @param bool $default
* @return mixed
*/
function setting($key, $default = false)
{
$settingService = app('BookStack\Services\SettingService');
return $settingService->get($key, $default);
}
/**
* Helper to create url's relative to the applications root path.
* @param string $path
* @param bool $forceAppDomain
* @return string
*/
function baseUrl($path, $forceAppDomain = false)
{
$isFullUrl = strpos($path, 'http') === 0;
if ($isFullUrl && !$forceAppDomain) return $path;
$path = trim($path, '/');
if ($isFullUrl && $forceAppDomain) {
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
return rtrim(config('app.url'), '/') . '/' . $path;
}
/**
* Get an instance of the redirector.
* Overrides the default laravel redirect helper.
* Ensures it redirects even when the app is in a subdirectory.
*
* @param string|null $to
* @param int $status
* @param array $headers
* @param bool $secure
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
function redirect($to = null, $status = 302, $headers = [], $secure = null)
{
if (is_null($to)) {
return app('redirect');
}
$to = baseUrl($to);
return app('redirect')->to($to, $status, $headers, $secure);
}
/**
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
* @param $path
* @param array $data
* @param array $overrideData
* @return string
*/
function sortUrl($path, $data, $overrideData = [])
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} else {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') continue;
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) return $path;
return baseUrl($path . '?' . implode('&', $queryStringSections));
}

View File

@@ -14,6 +14,7 @@ define('LARAVEL_START', microtime(true));
|
*/
require __DIR__.'/../app/helpers.php';
require __DIR__.'/../vendor/autoload.php';
/*

View File

@@ -12,7 +12,8 @@
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.0",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "0.6.*"
"barryvdh/laravel-dompdf": "0.6.*",
"predis/predis": "^1.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
@@ -28,10 +29,7 @@
],
"psr-4": {
"BookStack\\": "app/"
},
"files": [
"app/helpers.php"
]
}
},
"autoload-dev": {
"classmap": [

445
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,8 @@ return [
'env' => env('APP_ENV', 'production'),
'editor' => env('APP_EDITOR', 'html'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
@@ -29,7 +31,7 @@ return [
|
*/
'url' => env('APP_URL', 'http://localhost'),
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
/*
|--------------------------------------------------------------------------
@@ -128,7 +130,6 @@ return [
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
@@ -151,6 +152,8 @@ return [
/*
* Application Service Providers...
*/
BookStack\Providers\PaginationServiceProvider::class,
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,

View File

@@ -1,5 +1,17 @@
<?php
// MEMCACHED - Split out configuration into an array
if (env('CACHE_DRIVER') === 'memcached') {
$memcachedServerKeys = ['host', 'port', 'weight'];
$memcachedServers = explode(',', trim(env('MEMCACHED_SERVERS', '127.0.0.1:11211:100'), ','));
foreach ($memcachedServers as $index => $memcachedServer) {
$memcachedServerDetails = explode(':', $memcachedServer);
if (count($memcachedServerDetails) < 2) $memcachedServerDetails[] = '11211';
if (count($memcachedServerDetails) < 3) $memcachedServerDetails[] = '100';
$memcachedServers[$index] = array_combine($memcachedServerKeys, $memcachedServerDetails);
}
}
return [
/*
@@ -49,11 +61,7 @@ return [
'memcached' => [
'driver' => 'memcached',
'servers' => [
[
'host' => '127.0.0.1', 'port' => 11211, 'weight' => 100,
],
],
'servers' => env('CACHE_DRIVER') === 'memcached' ? $memcachedServers : [],
],
'redis' => [
@@ -74,6 +82,6 @@ return [
|
*/
'prefix' => 'laravel',
'prefix' => env('CACHE_PREFIX', 'bookstack'),
];

View File

@@ -1,5 +1,21 @@
<?php
// REDIS - Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisServerKeys = ['host', 'port', 'database'];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
}
}
return [
/*
@@ -68,8 +84,8 @@ return [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'bookstack-test',
'username' => 'bookstack-test',
'password' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
@@ -123,16 +139,6 @@ return [
|
*/
'redis' => [
'cluster' => false,
'default' => [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
],
],
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
];

View File

@@ -0,0 +1,13 @@
<?php
/**
* The defaults for the system settings that are saved in the database.
*/
return [
'app-name' => 'BookStack',
'app-editor' => 'wysiwyg',
'app-color' => '#0288D1',
'app-color-light' => 'rgba(21, 101, 192, 0.15)'
];

View File

@@ -17,6 +17,7 @@ $factory->define(BookStack\User::class, function ($faker) {
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
'email_confirmed' => 1
];
});
@@ -45,3 +46,17 @@ $factory->define(BookStack\Page::class, function ($faker) {
'text' => strip_tags($html)
];
});
$factory->define(BookStack\Role::class, function ($faker) {
return [
'display_name' => $faker->sentence(3),
'description' => $faker->sentence(10)
];
});
$factory->define(BookStack\Tag::class, function ($faker) {
return [
'name' => $faker->city,
'value' => $faker->sentence(3)
];
});

View File

@@ -18,13 +18,16 @@ class CreateUsersTable extends Migration
$table->string('email')->unique();
$table->string('password', 60);
$table->rememberToken();
$table->timestamps();
$table->nullableTimestamps();
});
\BookStack\User::forceCreate([
// Create the initial admin user
DB::table('users')->insert([
'name' => 'Admin',
'email' => 'admin@admin.com',
'password' => bcrypt('password')
'password' => bcrypt('password'),
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
}

View File

@@ -12,12 +12,18 @@ class CreateBooksTable extends Migration
*/
public function up()
{
Schema::create('books', function (Blueprint $table) {
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('books', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
$table->increments('id');
$table->string('name');
$table->string('slug')->indexed();
$table->text('description');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -12,7 +12,13 @@ class CreatePagesTable extends Migration
*/
public function up()
{
Schema::create('pages', function (Blueprint $table) {
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('pages', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
$table->increments('id');
$table->integer('book_id');
$table->integer('chapter_id');
@@ -21,7 +27,7 @@ class CreatePagesTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -16,7 +16,7 @@ class CreateImagesTable extends Migration
$table->increments('id');
$table->string('name');
$table->string('url');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -12,14 +12,19 @@ class CreateChaptersTable extends Migration
*/
public function up()
{
Schema::create('chapters', function (Blueprint $table) {
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
$table->increments('id');
$table->integer('book_id');
$table->string('slug')->indexed();
$table->text('name');
$table->text('description');
$table->integer('priority');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -19,7 +19,7 @@ class CreatePageRevisionsTable extends Migration
$table->longText('html');
$table->longText('text');
$table->integer('created_by');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -20,7 +20,7 @@ class CreateActivitiesTable extends Migration
$table->integer('user_id');
$table->integer('entity_id');
$table->string('entity_type');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -28,7 +28,7 @@ class AddRolesAndPermissions extends Migration
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
$table->nullableTimestamps();
});
// Create table for associating roles to users (Many-to-Many)
@@ -50,7 +50,7 @@ class AddRolesAndPermissions extends Migration
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
$table->nullableTimestamps();
});
// Create table for associating permissions to roles (Many-to-Many)
@@ -68,35 +68,44 @@ class AddRolesAndPermissions extends Migration
// Create default roles
$admin = new \BookStack\Role();
$admin->name = 'admin';
$admin->display_name = 'Admin';
$admin->description = 'Administrator of the whole application';
$admin->save();
$adminId = DB::table('roles')->insertGetId([
'name' => 'admin',
'display_name' => 'Admin',
'description' => 'Administrator of the whole application',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
$editorId = DB::table('roles')->insertGetId([
'name' => 'editor',
'display_name' => 'Editor',
'description' => 'User can edit Books, Chapters & Pages',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
$viewerId = DB::table('roles')->insertGetId([
'name' => 'viewer',
'display_name' => 'Viewer',
'description' => 'User can view books & their content behind authentication',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
$editor = new \BookStack\Role();
$editor->name = 'editor';
$editor->display_name = 'Editor';
$editor->description = 'User can edit Books, Chapters & Pages';
$editor->save();
$viewer = new \BookStack\Role();
$viewer->name = 'viewer';
$viewer->display_name = 'Viewer';
$viewer->description = 'User can view books & their content behind authentication';
$viewer->save();
// Create default CRUD permissions and allocate to admins and editors
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermission = new \BookStack\Permission();
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
$newPermission->display_name = $op . ' ' . $entity . 's';
$newPermission->save();
$admin->attachPermission($newPermission);
$editor->attachPermission($newPermission);
$newPermId = DB::table('permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
['permission_id' => $newPermId, 'role_id' => $adminId],
['permission_id' => $newPermId, 'role_id' => $editorId]
]);
}
}
@@ -105,19 +114,27 @@ class AddRolesAndPermissions extends Migration
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$newPermission = new \BookStack\Permission();
$newPermission->name = strtolower($entity) . '-' . strtolower($op);
$newPermission->display_name = $op . ' ' . $entity;
$newPermission->save();
$admin->attachPermission($newPermission);
$newPermId = DB::table('permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity,
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'permission_id' => $newPermId,
'role_id' => $adminId
]);
}
}
// Set all current users as admins
// (At this point only the initially create user should be an admin)
$users = \BookStack\User::all();
$users = DB::table('users')->get();
foreach ($users as $user) {
$user->attachRole($admin);
DB::table('role_user')->insert([
'role_id' => $adminId,
'user_id' => $user->id
]);
}
}

View File

@@ -15,7 +15,7 @@ class CreateSettingsTable extends Migration
Schema::create('settings', function (Blueprint $table) {
$table->string('setting_key')->primary()->indexed();
$table->text('value');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -18,7 +18,7 @@ class CreateSocialAccountsTable extends Migration
$table->string('driver')->index();
$table->string('driver_id');
$table->string('avatar');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -20,7 +20,7 @@ class AddEmailConfirmationTable extends Migration
$table->increments('id');
$table->integer('user_id')->index();
$table->string('token')->index();
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -18,7 +18,7 @@ class CreateViewsTable extends Migration
$table->integer('viewable_id');
$table->string('viewable_type');
$table->integer('views');
$table->timestamps();
$table->nullableTimestamps();
});
}

View File

@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
$table->dropColumn('external_auth_id');
});
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSlugToRevisions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->string('slug');
$table->index('slug');
$table->string('book_slug');
$table->index('book_slug');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('slug');
$table->dropColumn('book_slug');
});
}
}

View File

@@ -0,0 +1,118 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Get roles with permissions we need to change
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
// Delete old permissions
$permissions = DB::table('permissions')->delete();
// Create & attach new admin permissions
$permissionsToCreate = [
'settings-manage' => 'Manage Settings',
'users-manage' => 'Manage Users',
'user-roles-manage' => 'Manage Roles & Permissions',
'restrictions-manage-all' => 'Manage All Entity Permissions',
'restrictions-manage-own' => 'Manage Entity Permissions On Own Content'
];
foreach ($permissionsToCreate as $name => $displayName) {
$permissionId = DB::table('permissions')->insertGetId([
'name' => $name,
'display_name' => $displayName,
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
// Create & attach new entity permissions
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
if ($editorRole !== null) {
DB::table('permission_role')->insert([
'role_id' => $editorRole->id,
'permission_id' => $permissionId
]);
}
}
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Get roles with permissions we need to change
$adminRoleId = DB::table('roles')->where('name', '=', 'admin')->first()->id;
// Delete old permissions
$permissions = DB::table('permissions')->delete();
// Create default CRUD permissions and allocate to admins and editors
$entities = ['Book', 'Page', 'Chapter', 'Image'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
}
// Create admin permissions
$entities = ['Settings', 'User'];
$ops = ['Create', 'Update', 'Delete'];
foreach ($entities as $entity) {
foreach ($ops as $op) {
$permissionId = DB::table('permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower($op),
'display_name' => $op . ' ' . $entity,
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEntityAccessControls extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('images', function (Blueprint $table) {
$table->integer('uploaded_to')->default(0);
$table->index('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->boolean('restricted')->default(false);
$table->index('restricted');
});
Schema::create('restrictions', function(Blueprint $table) {
$table->increments('id');
$table->integer('restrictable_id');
$table->string('restrictable_type');
$table->integer('role_id');
$table->string('action');
$table->index('role_id');
$table->index('action');
$table->index(['restrictable_id', 'restrictable_type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('images', function (Blueprint $table) {
$table->dropColumn('uploaded_to');
});
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('chapters', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('restricted');
});
Schema::drop('restrictions');
}
}

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