Compare commits

...

762 Commits

Author SHA1 Message Date
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
818c02ed44 Added null role check to migrate path
Also added check for existing bookshelf role_permissions
in the event the user got that for.
Also related to #1027
2018-09-24 16:30:08 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
9abdab3991 Updated migration to convert MyISAM tables to InnoDB
New bookshelves_books tables requires foreign constraints which error on MyISAM.
For #1027
2018-09-24 15:58:40 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
43122f86e8 Merge pull request #1026 from vriic/master
Update german translation
2018-09-24 10:35:22 +01:00
Nikolai Nikolajevic
935337862c Update german translation 2018-09-24 09:55:37 +02:00
Dan Brown
dd671524f8 Merge pull request #1025 from moucho/master
Spanish translation
2018-09-23 17:18:41 +01:00
Marcos
1cac3d43d3 Spanish translation 2018-09-23 17:31:41 +02:00
Dan Brown
9243c635f2 Made search test a little more consistent 2018-09-23 15:15:44 +01:00
Dan Brown
93f820d9da Updated TinyMCE config to end containers on empty blocks
Makes it easier to escape out of blockquote sections.
Fixes #961
2018-09-23 14:07:50 +01:00
Dan Brown
b62afcad1f Removed search indexing from migration path to prevent Bookshelf issue 2018-09-23 13:25:12 +01:00
Dan Brown
7b32aa163f Added Bookshelves to search system.
Also cleaned up and made search indexing system a little more efficient.
Closes #1023
2018-09-23 12:34:30 +01:00
Dan Brown
eebfd8904e Removed old fulltext indexes from migrations
Prevents forcing of MyISAM for some databases
Removed old code to add indexes and added checks for existing indexes before removal.
Should still allow upgrades, rollbacks to old bookstack versions may be funky but
should not be high use-case.
2018-09-23 00:30:48 +01:00
Dan Brown
0d84a0b976 Extracted search sidebar text to translation file
Closes #864
2018-09-22 23:40:04 +01:00
Dan Brown
88ac636c00 Made it possible to scroll past the end of the markdown editor
Closes #1020
2018-09-22 23:24:51 +01:00
Dan Brown
9dc26a8c52 Prevented TinyMCE clear-color option having scrollbar
Was covering the option if showing and prevented the option being pressed.
Fixes #999
2018-09-22 23:12:39 +01:00
Dan Brown
6543020fd2 Updated tinymce to v4.8.3 2018-09-22 22:58:06 +01:00
Dan Brown
be4f3d62cd Merge branch 'fix/ru-locale' of git://github.com/mullinsmikey/BookStack into mullinsmikey-fix/ru-locale 2018-09-22 22:29:03 +01:00
Dan Brown
da58c41ab6 Prevented attachDefaultRole from trying to re-attach if already existing
Fixes #1003
Added test to cover
2018-09-22 22:09:34 +01:00
Dan Brown
10d08e641c Merge pull request #1021 from moucho/master
Spanish translation
2018-09-22 21:43:19 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Marcos
c4e4cebf14 Spanish translation 2018-09-22 18:36:52 +02:00
Dan Brown
3f58800ed1 Added ability to configure revision limit 2018-09-22 17:30:42 +01:00
Dan Brown
0931ff38e9 Tried to make chapter toggles a little smoother in FF 2018-09-22 16:36:35 +01:00
Dan Brown
07bc0612c0 Merge branch 'master' into fix/#960 2018-09-22 15:57:53 +01:00
Dan Brown
6dec485b45 Fixed sidebar rubber-banding when content is expanded
Hopefully for #905
Ideally layout needs to be re-thought
2018-09-22 15:52:09 +01:00
Dan Brown
a441faf65c Only show codeblock copy icon on hover
Fixes #980
2018-09-22 14:55:33 +01:00
Dan Brown
50ee1462ad Merge pull request #986 from DeehSlash/fix/pt_br_locale
Adds and fixes pt_BR strings
2018-09-22 14:40:23 +01:00
Dan Brown
1cb6ae39c8 Added base RTL support
For #939

- Adds way to check if current language is RTL via config system.
- Made TinyMCE default direction be based on current language text
direction.
- Fixed bullet points to be RTL compatible.
- Set page content body to have direction based on content.
2018-09-22 13:18:26 +01:00
Dan Brown
c667c6e235 Merge branch 'master' of git://github.com/kmoj86/BookStack into kmoj86-master 2018-09-22 12:23:17 +01:00
Dan Brown
e3e484e561 Added custom head content to exports
Closes #981

Also fixed incorrect download tests.
2018-09-22 11:53:40 +01:00
Dan Brown
73fa18a128 Made bookstack cookie name configurable
Closes #1018
2018-09-22 11:42:02 +01:00
Dan Brown
5c2e3f4e56 Extracted download response logic into controller method
Fixes incorrect 'Content-Disposition' header value.
Fixes #581
2018-09-22 11:34:09 +01:00
Dan Brown
c47b578599 Fixed formatting via phpcbf 2018-09-21 18:48:47 +01:00
Dan Brown
e60d11ee04 Altered social auto-reg to be configurable per service
- Added {$service}_AUTO_REGISTER and {$service}_AUTO_CONFIRM_EMAIL env
options for each social auth system.
- Auto-register will allow registration from login, even if registration
is disabled.
- Auto-confirm-email indicates trust and will mark new registrants as
'email_confirmed' and skip 'confirmation email' flow.
- Also added covering tests.
2018-09-21 18:05:06 +01:00
Dan Brown
7ad8314bd7 Merge branch 'feature/autoregistration_social_login' of git://github.com/ibrahimennafaa/BookStack into ibrahimennafaa-feature/autoregistration_social_login 2018-09-21 16:14:52 +01:00
Dan Brown
131fcae4c7 Merge pull request #947 from BookStackApp/bookshelves
Bookshelves
2018-09-21 15:29:52 +01:00
Dan Brown
c8d893fac7 Updated 404 test to not fail based on random long name 2018-09-21 15:24:29 +01:00
Dan Brown
b59e5942c8 Added testing coverage for Bookshelves
Created modified TestResponse so we can use DOM operations in new
Testcases as we move away from the BrowserKit tests.
2018-09-21 15:15:16 +01:00
Dan Brown
8ff969dd17 Updated so permission effect admins more
Asset permissions can now be configured for admins.
joint_permissions will now effect admins more often.
Made so shelves header link will hide if you have no bookshelves view
permission.
2018-09-20 19:48:08 +01:00
Dan Brown
6eead437d8 Added bookshelf permission control UI and copy-down ability 2018-09-20 19:16:11 +01:00
Dan Brown
0b6f83837b Removed joint_permission generation in older migration 2018-09-20 16:03:01 +01:00
Dan Brown
81eb642f75 Added bookshelves homepage options
- Updated homepage selection UI to be more scalable
- Cleaned homepage selection logic in code
- Added seed test data for bookshelves
- Added bookshelves to permission system
2018-09-20 15:27:30 +01:00
Dan Brown
47b08888ba Added bookshelf view, update, delete
- Enabled proper ordering of Books in a shelf.
- Improved related item destroy for all entities.
2018-09-16 19:34:09 +01:00
Abijeet Patro
32e34f10ff Merge pull request #1008 from BookStackApp/revision-deletion
#784 - Adds ability to remove particular revision.
2018-09-16 21:30:04 +05:30
Dan Brown
f455b317ec Added ability to click books in shelf-sort 2018-09-16 16:59:01 +01:00
Abijeet
08b967607f Changes as per code review, and fixes failing test cases.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-16 20:44:09 +05:30
Abijeet
90883bb22b Fixes issue wth the dropdown list upon double clicking.
Closes #960

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-16 13:09:21 +05:30
Abijeet
0c8b6b7324 Final tweaks after code review and fixing failing test cases. 2018-09-16 01:12:36 +05:30
Abijeet
81d3bdc168 Removes the BadRequestException class added earlier.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 21:08:00 +05:30
Abijeet
54ca4487fa Adds tests and few fixes.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 21:05:51 +05:30
Abijeet
25da4d9a8b Added a success message on deletion of revision.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 16:08:20 +05:30
Abijeet
f0add69b61 Adding languages for the revision deletion. 2018-09-15 15:16:04 +05:30
Abijeet
714c7bbd3a Adds code to delete the revision.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 15:15:42 +05:30
Michael Mullins
e0b479efef UTF-8 slugs & UI fixes 2018-09-11 22:42:25 +04:00
Michael Mullins
9d09c4c7b0 Ru locale fix
Fixed russian translations, added several missing strings
2018-09-10 22:28:47 +04:00
Dan Brown
b89411c108 Copied book content, Added create routes
Added view control
Added pivot table for books relation
Added control to assign books
2018-08-27 14:18:09 +01:00
André Luiz da Silva
c472b82ee3 Adds and fixes pt_BR strings 2018-08-24 16:03:04 -03:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Ibrahim Ennafaa
d2f5313f92 add missing @param in method comment 2018-08-21 12:44:42 -04:00
Ibrahim Ennafaa
572e75b783 Update UserRepo.php 2018-08-20 21:19:25 -04:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
d2a9b312e9 Fixed LDAP group sync using wrong user filter
LDAP group sync was trying to find users based on the external_auth_id
which is not garunteed to match the username entered so somtimes
the search for a user would fail.

This passes the username to the group sync.
Picked up by @yoyokko in #959.
2018-08-19 15:24:42 +01:00
Ibrahim Ennafaa
b224a2c8a0 attempt to fix unit test error for admin creation 2018-08-16 21:52:16 +00:00
Dan Brown
fe6dfcedf9 implement social auto registration feature 2018-08-16 21:26:54 +00:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
01260d95f3 Merge pull request #957 from moucho/master
Updated Spanish translation
2018-08-12 14:20:53 +01:00
Dan Brown
d69ba6b47a Updated composer dependancies 2018-08-12 13:42:17 +01:00
Dan Brown
098128aafb Added test to cover new language autodetect config option 2018-08-12 13:34:14 +01:00
Dan Brown
92c9837157 Fixed incorrect type error in LDAP group sync
Should fix #951
2018-08-12 13:28:40 +01:00
Marcos
18e5f86ffa Updated Spanish translation 2018-08-12 14:14:56 +02:00
Dan Brown
c860645a5a Tweaked bug report template to request hosting method 2018-08-12 13:12:47 +01:00
Dan Brown
fcb93dc7c8 Added option to disable public lang autodetect
Also cleaned up localization middleware a little.
Closes #944
2018-08-12 13:10:55 +01:00
Dan Brown
fcdb39e428 Merge pull request #942 from marcusforsberg/master
Updated Swedish translation
2018-08-12 12:51:00 +01:00
Dan Brown
1b3e1863f4 Merge pull request #948 from houbaron/fix/Chinese_translation
Fix/Chinese translation
2018-08-12 12:37:35 +01:00
Dan Brown
fbc2175789 Merge pull request #952 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-08-12 12:36:28 +01:00
Leonardo Martinez
8099c431bb Updated 'Spanish Argentina' translation. 2018-08-06 10:46:53 -03:00
Baron Hou
efbfe0f7af Update Traditional Chinese 2018-08-05 17:07:13 +08:00
Baron Hou
66402b474c Update Simplified Chinese 2018-08-05 17:05:41 +08:00
Dan Brown
c3986cedfc Added shelve icon, improved migration, added role permission
Icon is placeholder for now
Migration will now copy permissions from Books to apply to shelves.
Role view updated with visibility on shelve permission
2018-08-04 12:45:45 +01:00
Dan Brown
b5a2d3c1c4 Merge remote-tracking branch 'origin' into bookshelves 2018-08-04 11:35:01 +01:00
Khalid
a6862362c1 added Arabic to locales 2018-08-03 19:00:51 +03:00
Khalid
7d4ec0d633 added Arabic language to drop-down list. 2018-08-03 18:58:27 +03:00
Khalid
85544c04a9 translated 2018-08-03 18:51:18 +03:00
Khalid
0a8ff6ffad translated 2018-08-03 18:34:39 +03:00
Khalid
04274078c4 translated 2018-08-02 06:38:33 +03:00
Khalid
aac9fbf236 translated 2018-08-01 18:25:44 +03:00
Khalid
08e290f3ea translated 2018-07-30 12:58:12 +03:00
Khalid
86602854ac translated 2018-07-30 12:54:32 +03:00
marcusforsberg
f47f0e05d6 Updated Swedish translation 2018-07-30 09:35:34 +03:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
c83a51f7e2 Merge pull request #904 from lommes/903-socialite-discord
add everything needed to use discord as social login provider
2018-07-29 16:18:10 +01:00
Dan Brown
b922c8029e Merge pull request #933 from nicobubulle/master
French translation update
2018-07-29 16:04:39 +01:00
Dan Brown
653761e67d Merge pull request #925 from alex2702/fix/835
Fixed German translations for notifications
2018-07-29 16:03:29 +01:00
Dan Brown
d59ff132ab Delete ISSUE_TEMPLATE.md 2018-07-29 15:55:13 +01:00
Dan Brown
e6e740b2a1 Update issue templates 2018-07-29 15:54:53 +01:00
Dan Brown
af6f4e6c8c Updated pagination to use theme colour 2018-07-29 15:44:10 +01:00
Dan Brown
69a0f8d502 Prevented error notification being visible on load
Fixes #897

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

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

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

This is happening because -

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

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

Also added padding to the icons in the attachment section.

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

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

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

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

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

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

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

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

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

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

Fixes #223

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

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

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

Towards #741

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

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

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

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

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

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

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

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

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

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

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

Command:

php artisan bookstack:delete-users

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

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

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

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

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

Fixes #442
2017-07-22 14:21:56 +01:00
Dan Brown
bc067e9ad4 Updated dropdowns to hide after option click
Fixes #429
2017-07-22 14:03:06 +01:00
Clément Blanco
245294fbc5 Trying to make the tests green. 2017-07-17 14:42:08 +01:00
Clément Blanco
f38bc75ab4 Trying to make the tests green. 2017-07-17 14:21:41 +01:00
Clément Blanco
3407900abb Trying to make the tests green. 2017-07-17 14:18:03 +01:00
Clément Blanco
684c20c4ea Trying to make the tests green. 2017-07-17 14:09:21 +01:00
Clément Blanco
6ef522df7e Trying to make the tests green. 2017-07-17 14:05:41 +01:00
Clément Blanco
3b771f2976 Trying to make the tests green. 2017-07-17 14:03:31 +01:00
Clément Blanco
afc56c12fe Trying to make the tests green. 2017-07-17 14:01:10 +01:00
Clément Blanco
5eeed03dcd Trying to make the tests green. 2017-07-17 13:53:02 +01:00
Clément Blanco
0d98b4ce5e Trying to make the tests green. 2017-07-17 13:37:15 +01:00
Clément Blanco
ae2ec43a82 Avoid having to wait until all tests are processed to exit upon error/failure. 2017-07-17 13:35:38 +01:00
Clément Blanco
265ed34ffd Update travis.yml to try and solve the test issue around LDAP. 2017-07-17 13:34:19 +01:00
Clément Blanco
711dcb4a48 Update travis.yml to try and solve the test issue around LDAP. 2017-07-17 13:29:29 +01:00
Nilesh Deepak
3079a9f4de Reverted required changes. 2017-07-15 19:07:32 +05:30
Nilesh Deepak
a7d2cfdee2 Resolving test cases 2017-07-15 19:03:02 +05:30
Nilesh Deepak
a149e87ca7 Resolving test cases 2017-07-15 19:00:23 +05:30
Nilesh Deepak
854fd52a27 Resolving test cases 2017-07-15 18:57:09 +05:30
Nilesh Deepak
3d808ac75f Test for cover image 2017-07-15 18:39:13 +05:30
Nilesh Deepak
39b924f158 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 18:37:55 +05:30
Nilesh Deepak
a488ef6b00 Test for cover image. 2017-07-15 18:36:49 +05:30
abijeetp
6d66c38c12 Fixes issues with the test case, now creating a user with the required profile setting. 2017-07-15 18:00:39 +05:30
Nilesh Deepak
922964ecf2 Changes grid container size 2017-07-15 17:50:09 +05:30
Nilesh Deepak
0c70416b5c Test books display options. 2017-07-15 16:33:52 +05:30
Nilesh Deepak
770f30c3a8 Test books display options. 2017-07-15 16:29:42 +05:30
Nilesh Deepak
b4044e6c3a Resolves heading issues in grid view 2017-07-15 16:22:29 +05:30
Nilesh Deepak
9872767f20 Test for cover image upload 2017-07-15 16:19:35 +05:30
Nilesh Deepak
dd4d2f4696 Resolves book heading issues in grid view. 2017-07-15 16:15:45 +05:30
Nilesh Deepak
e5dc0e6bb8 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 16:13:48 +05:30
Nilesh Deepak
85fbe820c4 Adding getHeadingExcerpt to get heading. 2017-07-15 16:11:10 +05:30
abijeetp
832f8eaa94 Fixes the test case related to UserProfileTest. 2017-07-15 15:50:42 +05:30
Abijeet
3435dcc91e Merge pull request #10 from OsmosysSoftware/test-issue-181
Tests for issue 181
2017-07-15 14:29:38 +05:30
Nilesh Deepak
1ed74b8598 Test for grid and list layout selection. 2017-07-15 13:19:49 +05:30
Nilesh Deepak
fd36978c13 Test for layout selection. 2017-07-15 12:26:57 +05:30
Nilesh Deepak
1278a0b818 Test for layout selection. 2017-07-15 11:40:51 +05:30
Nilesh Deepak
6a6516ddd5 Test for layout selection. 2017-07-15 11:31:43 +05:30
Clément Blanco
67bc7007aa Support new lines for book/chapter descriptions
Avoid ignoring new lines when renderring the book/chapter descriptions on their respective detailed views.
2017-07-14 16:05:46 +01:00
Nilesh Deepak
1fe8f13503 Cover image test case 2017-07-14 18:36:50 +05:30
Nilesh Deepak
8f3adcda5d Cover image test case 2017-07-14 18:02:45 +05:30
JachuPL
9e0c931573 Polish translation 2017-07-13 16:00:42 +02:00
Abijeet
21a8df78ee Merge pull request #9 from OsmosysSoftware/feature-181
Feature 181
2017-07-13 15:50:43 +05:30
Nilesh Deepak
7f8351e044 Removed avatar class from form.blade.php 2017-07-13 15:20:53 +05:30
Nilesh Deepak
afc1ecafe9 4. Changed the border color of the gallery item to #ccc 2017-07-13 12:27:14 +05:30
Nilesh Deepak
ab6ff5fda2 3. New default.png 2017-07-13 12:26:01 +05:30
Nilesh Deepak
b0ba1a43a9 2. Added classed col-xs-6 col-sm-4 col-md-4 col-lg-3 in grid-item.blade.php
5. Added <div class="row"> in index.blade.php
2017-07-13 12:24:47 +05:30
Nilesh Deepak
e919cab3d1 1. Thumbnail size when creating or editing book. 2017-07-13 12:22:43 +05:30
Abijeet
f37509062e Merge pull request #8 from OsmosysSoftware/feature-181
Issue 181
2017-07-12 18:41:35 +05:30
Nilesh Deepak
24ee78ccd8 Update. 2017-07-12 18:04:06 +05:30
Nilesh Deepak
d37b398e79 Updates styles. 2017-07-12 13:52:21 +05:30
Nilesh Deepak
7a724f9134 Updated modifications. 2017-07-12 13:44:37 +05:30
Abijeet
f3b2e0fb91 Merge pull request #7 from OsmosysSoftware/revert-3-revert-1-issue-181
Revert "Revert "Bookstack grid view.""
2017-07-12 11:41:01 +05:30
Abijeet
844976c85b Revert "Revert "Bookstack grid view."" 2017-07-12 11:40:50 +05:30
Abijeet
f0d914abbf Merge pull request #5 from BookStackApp/master
Getting latest changes
2017-07-12 11:33:58 +05:30
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
2ea7e10923 Set ldap to not follow referrals by default
Added LDAP_FOLLOW_REFERRALS .env option to override.
Fixes #317
2017-07-10 19:43:49 +01:00
Dan Brown
e7f8188ee5 Prevented textarea styles interfering with codemirror
Closes #424
2017-07-10 19:29:35 +01:00
Abijeet
0ed3023b42 Merge pull request #3 from OsmosysSoftware/revert-1-issue-181
Revert "Bookstack grid view."
2017-07-07 17:28:47 +05:30
Abijeet
3fd61a3600 Revert "Bookstack grid view." 2017-07-07 17:28:34 +05:30
Nilesh Deepak
a663fc8aa8 Merge pull request #1 from OsmosysSoftware/issue-181
Bookstack grid view issue 181.
2017-07-07 17:08:19 +05:30
Nilesh Deepak
d84315fff8 Indentation correction. 2017-07-07 17:06:08 +05:30
Nilesh Deepak
144a6e469d Updated cover image upload and delete function. 2017-07-07 16:29:38 +05:30
Nilesh Deepak
c5f11e4516 Fixed pagination on change of display type. 2017-07-06 10:05:11 +05:30
Nilesh Deepak
16a09e8ff6 Deletion of image file on book deletion. 2017-07-06 10:03:40 +05:30
Nilesh Deepak
f51db4b9f6 Resolved responsiveness issues 2017-07-05 19:58:52 +05:30
Nilesh Deepak
6ad24a6bee Changed public getImageURL function to private. 2017-07-05 18:32:38 +05:30
Nilesh Deepak
5b736c3b36 Updated views to support different languages. 2017-07-05 16:12:29 +05:30
Nilesh Deepak
cc553cc93d Added labels for 'Thumbnail toggle' and 'Cover image' in different languages. 2017-07-05 16:11:15 +05:30
Nilesh Deepak
e88a06291e Updated toggle thumbnails function. 2017-07-05 16:09:20 +05:30
Nilesh Deepak
6eccb3d5b9 Adding new migration. 2017-07-05 16:08:04 +05:30
Nilesh Deepak
026de8c5ca Thumbnail toggle function. 2017-07-05 12:48:41 +05:30
Nilesh Deepak
e10d4b91cf styles.scss 2017-07-05 12:36:26 +05:30
Nilesh Deepak
d089eaf754 Changes in User edit profile page. 2017-07-05 12:32:39 +05:30
Nilesh Deepak
bb2d85965f Removed duplicated styles. 2017-07-05 12:29:16 +05:30
Nilesh Deepak
d99fd1fd65 Applied required changes 2017-07-05 12:26:02 +05:30
Nilesh Deepak
947c58f227 Applied required changes in BookStack. 2017-07-05 12:09:01 +05:30
Nilesh Deepak
bce5fdd5cd Merge branch 'master' into issue-181 2017-07-04 15:16:46 +05:30
Nilesh Deepak
fdf139edb2 Changing column size for responsiveness 2017-07-04 15:04:57 +05:30
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
314d98abc3 Removed logs, Updated version, Fixed inconsistent subheader 2017-07-02 20:33:32 +01:00
Dan Brown
b2dfc069c5 Updated readme attribution 2017-07-02 19:38:28 +01:00
Dan Brown
69c50b9b48 Migrated markdown editor actions to new cm editor 2017-07-02 19:35:13 +01:00
Dan Brown
f101e4f010 Fixed quoting db/table names in encoding migration.
Also fixed incorrect if statement in db config.
2017-07-02 17:34:32 +01:00
Dan Brown
005f0eb4fc Updated default encoding and added conversion migration.
Also updated how DB port is defined so that the DB_PORT
env var can be used or it can be take from the host name.

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

2
.browserslistrc Normal file
View File

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

View File

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

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

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

View File

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

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

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

View File

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

13
.gitignore vendored
View File

@@ -2,22 +2,23 @@
/node_modules
Homestead.yaml
.env
/public/dist
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/plugins
/public/css/*.map
/public/js/*.map
/public/bower
/public/build/
/storage/images
_ide_helper.php
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin
nbproject
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs
.settings/
webpack-stats.json

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,9 @@
class Book extends Entity
{
public $searchFactor = 2;
protected $fillable = ['name', 'description'];
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the url for this book.
@@ -18,13 +19,34 @@ class Book extends Entity
return baseUrl('/books/' . urlencode($this->slug));
}
/*
* Get the edit url for this book.
/**
* Returns book cover image, if book cover not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getEditUrl()
public function getBookCover($width = 440, $height = 250)
{
return $this->getUrl() . '/edit';
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
@@ -45,6 +67,15 @@ class Book extends Entity
return $this->hasMany(Chapter::class);
}
/**
* Get the shelves this book is contained within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function shelves()
{
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
@@ -64,5 +95,4 @@ class Book extends Entity
{
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

83
app/Bookshelf.php Normal file
View File

@@ -0,0 +1,83 @@
<?php namespace BookStack;
class Bookshelf extends Entity
{
protected $table = 'bookshelves';
public $searchFactor = 3;
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
}
/**
* Get the url for this bookshelf.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
if ($path !== false) {
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/shelves/' . urlencode($this->slug));
}
/**
* Returns BookShelf cover image, if cover does not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

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

43
app/Comment.php Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
<?php namespace BookStack;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Entity extends Ownable
{
/**
* @var string - Name of property where the main text content is found
*/
public $textField = 'description';
/**
* @var float - Multiplier for search indexing.
*/
public $searchFactor = 1.0;
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
@@ -26,7 +35,9 @@ class Entity extends Ownable
{
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
if ($matches) return true;
if ($matches) {
return true;
}
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
return $entity->book_id === $this->id;
@@ -65,6 +76,17 @@ class Entity extends Ownable
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the comments for an entity
* @param bool $orderByCreated
* @return MorphMany
*/
public function comments($orderByCreated = true)
{
$query = $this->morphMany(Comment::class, 'entity');
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
}
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@@ -130,7 +152,7 @@ class Entity extends Ownable
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter'];
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
@@ -146,8 +168,10 @@ class Entity extends Ownable
*/
public function getShortName($length = 25)
{
if (strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length - 3) . '...';
if (mb_strlen($this->name) <= $length) {
return $this->name;
}
return mb_substr($this->name, 0, $length - 3) . '...';
}
/**
@@ -163,13 +187,18 @@ class Entity extends Ownable
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery(){return '';}
public function entityRawQuery()
{
return '';
}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path){return '/';}
public function getUrl($path = '/')
{
return $path;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,13 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Services\EmailConfirmationService;
use BookStack\Services\SocialAuthService;
use BookStack\SocialAccount;
use BookStack\User;
use Exception;
use Illuminate\Http\Request;
@@ -15,6 +16,7 @@ use Illuminate\Http\Response;
use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
use Laravel\Socialite\Contracts\User as SocialUser;
class RegisterController extends Controller
{
@@ -52,7 +54,7 @@ class RegisterController extends Controller
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
@@ -90,6 +92,7 @@ class RegisterController extends Controller
/**
* Show the application registration form.
* @return Response
* @throws UserRegistrationException
*/
public function getRegister()
{
@@ -101,20 +104,13 @@ class RegisterController extends Controller
/**
* Handle a registration request for the application.
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \Illuminate\Foundation\Validation\ValidationException
*/
public function postRegister(Request $request)
{
$this->checkRegistrationAllowed();
$validator = $this->validator($request->all());
if ($validator->fails()) {
$this->throwValidationException(
$request, $validator
);
}
$this->validator($request->all())->validate();
$userData = $request->all();
return $this->registerUser($userData);
@@ -138,26 +134,28 @@ class RegisterController extends Controller
* The registrations flow for all users.
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @param bool $emailVerified
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws ConfirmationEmailException
*/
protected function registerUser(array $userData, $socialAccount = false)
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
{
if (setting('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$registrationRestrict = setting('registration-restrict');
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
}
$newUser = $this->userRepo->registerNew($userData);
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
if ($socialAccount) {
$newUser->socialAccounts()->save($socialAccount);
}
if (setting('registration-confirmation') || setting('registration-restrict')) {
if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
$newUser->save();
try {
@@ -230,7 +228,6 @@ class RegisterController extends Controller
return redirect('/register/confirm');
}
$this->emailConfirmationService->sendConfirmation($user);
session()->flash('success', trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
@@ -239,6 +236,8 @@ class RegisterController extends Controller
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function socialRegister($socialDriver)
{
@@ -250,21 +249,45 @@ class RegisterController extends Controller
/**
* The callback for social login services.
* @param $socialDriver
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function socialCallback($socialDriver)
public function socialCallback($socialDriver, Request $request)
{
if (session()->has('social-callback')) {
$action = session()->pull('social-callback');
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
} elseif ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
}
} else {
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
// Check request for error information
if ($request->has('error') && $request->has('error_description')) {
throw new SocialSignInException(trans('errors.social_login_bad_response', [
'socialAccount' => $socialDriver,
'error' => $request->get('error_description'),
]), '/login');
}
$action = session()->pull('social-callback');
// Attempt login or fall-back to register if allowed.
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
if ($action == 'login') {
try {
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
} catch (SocialSignInAccountNotUsed $exception) {
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
throw $exception;
}
}
if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
return redirect()->back();
}
@@ -280,14 +303,16 @@ class RegisterController extends Controller
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @param string $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance
$userData = [
@@ -295,7 +320,6 @@ class RegisterController extends Controller
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
return $this->registerUser($userData, $socialAccount, $emailVerified);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,243 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
class BookshelfController extends Controller
{
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Display a listing of the book.
* @return Response
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
]);
}
/**
* Show the form for creating a new bookshelf.
* @return Response
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
return redirect($bookshelf->getUrl());
}
/**
* Display the specified bookshelf.
* @param String $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
]);
}
/**
* Show the form for editing the specified bookshelf.
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
}
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
/**
* Shows the page to confirm deletion
* @param $slug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
}
/**
* Remove the specified bookshelf from storage.
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function destroy(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function restrict(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
* @param string $slug
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
}
}

View File

@@ -107,17 +107,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
if ($chapter->name !== $request->get('name')) {
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
}
$chapter->fill($request->all());
$chapter->updated_by = user()->id;
$chapter->save();
$this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
@@ -159,7 +156,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
public function showMove($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
@@ -177,7 +175,8 @@ class ChapterController extends Controller
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
public function move($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
@@ -251,10 +250,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
}
/**
@@ -267,10 +263,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
}
/**
@@ -282,10 +275,7 @@ class ChapterController extends Controller
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
$chapterText = $this->exportService->chapterToPlainText($chapter);
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
}
}

View File

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

View File

@@ -51,7 +51,9 @@ abstract class Controller extends BaseController
*/
protected function preventAccessForDemoUsers()
{
if (config('app.env') === 'demo') $this->showPermissionError();
if (config('app.env') === 'demo') {
$this->showPermissionError();
}
}
/**
@@ -100,7 +102,9 @@ abstract class Controller extends BaseController
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
if (userCan($permission, $ownable)) {
return true;
}
return $this->showPermissionError();
}
@@ -113,7 +117,9 @@ abstract class Controller extends BaseController
protected function checkPermissionOr($permissionName, $callback)
{
$callbackResult = $callback();
if ($callbackResult === false) $this->checkPermission($permissionName);
if ($callbackResult === false) {
$this->checkPermission($permissionName);
}
return true;
}
@@ -130,7 +136,6 @@ abstract class Controller extends BaseController
/**
* Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
@@ -146,4 +151,17 @@ abstract class Controller extends BaseController
->withErrors($errors, $this->errorBag());
}
/**
* Create a response that forces a download in the browser.
* @param string $content
* @param string $fileName
* @return \Illuminate\Http\Response
*/
protected function downloadResponse(string $content, string $fileName)
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
}

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -38,11 +37,18 @@ class PageController extends Controller
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
* @throws NotFoundException
*/
public function create($bookSlug, $chapterSlug = null)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -52,7 +58,7 @@ class PageController extends Controller
return redirect($draft->getUrl());
}
// Otherwise show edit view
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
@@ -71,8 +77,14 @@ class PageController extends Controller
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -93,7 +105,7 @@ class PageController extends Controller
public function editDraft($bookSlug, $pageId)
{
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->checkOwnablePermission('page-create', $draft->parent);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
@@ -119,12 +131,10 @@ class PageController extends Controller
]);
$input = $request->all();
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$book = $draftPage->book;
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$parent = $draftPage->parent;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
@@ -145,6 +155,7 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @throws NotFoundException
*/
public function show($bookSlug, $pageSlug)
{
@@ -152,22 +163,33 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) {
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
if ($page === null) {
throw $e;
}
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = $this->entityRepo->renderPage($page);
$page->html = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
$pageNav = $this->entityRepo->getPageNav($page->html);
// check if the comment's are enabled
$commentsEnabled = !setting('app-disable-comments');
if ($commentsEnabled) {
$page->load(['comments.createdBy']);
}
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
]);
}
/**
@@ -210,7 +232,9 @@ class PageController extends Controller
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
if (count($warnings) > 0) {
session()->flash('warning', implode("\n", $warnings));
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
@@ -261,11 +285,10 @@ class PageController extends Controller
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $utcUpdateTimestamp
'timestamp' => $updateTime
]);
}
@@ -323,9 +346,10 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$book = $page->book;
$this->checkOwnablePermission('page-delete', $page);
$this->entityRepo->destroyPage($page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', trans('entities.pages_delete_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
@@ -376,10 +400,11 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
'revision' => $revision
]);
}
@@ -409,6 +434,7 @@ class PageController extends Controller
'page' => $page,
'book' => $page->book,
'diff' => $diff,
'revision' => $revision
]);
}
@@ -428,6 +454,40 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Deletes a revision using the id of the specified revision.
* @param string $bookSlug
* @param string $pageSlug
* @param int $revId
* @throws NotFoundException
* @throws BadRequestException
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroyRevision($bookSlug, $pageSlug, $revId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {
throw new NotFoundException("Revision #{$revId} not found");
}
// Get the current revision for the page
$currentRevision = $page->getCurrentRevision();
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
@@ -438,11 +498,9 @@ class PageController extends Controller
public function exportPdf($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
/**
@@ -454,11 +512,9 @@ class PageController extends Controller
public function exportHtml($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
}
/**
@@ -470,11 +526,8 @@ class PageController extends Controller
public function exportPlainText($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
$pageText = $this->exportService->pageToPlainText($page);
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
@@ -567,6 +620,8 @@ class PageController extends Controller
return redirect()->back();
}
$this->checkOwnablePermission('page-create', $parent);
$this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
@@ -574,12 +629,70 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
* @param string $bookSlug
* @param string $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
'book' => $page->book,
'page' => $page
]);
}
/**
* Create a copy of a page within the requested target destination.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
$parent = $page->chapter ? $page->chapter : $page->book;
} else {
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
try {
$parent = $this->entityRepo->getById($entityType, $entityId);
} catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back();
}
}
$this->checkOwnablePermission('page-create', $parent);
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
session()->flash('success', trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
/**
* Set the permissions for this page.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
@@ -589,5 +702,4 @@ class PageController extends Controller
session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,9 +2,13 @@
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
class Localization
{
protected $rtlLocales = ['ar'];
/**
* Handle an incoming request.
*
@@ -15,19 +19,38 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
if (user()->isDefault()) {
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) continue;
$locale = $lang;
break;
}
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
}
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
/**
* Autodetect the visitors locale by matching locales in their headers
* against the locales supported by BookStack.
* @param Request $request
* @param string $default
* @return string
*/
protected function autoDetectLocale(Request $request, string $default)
{
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (in_array($lang, $availableLocales)) {
return $lang;
}
}
return $default;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
@@ -31,7 +30,9 @@ class PageRevision extends Model
public function getUrl($path = null)
{
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) return $url . '/' . trim($path, '/');
if ($path) {
return $url . '/' . trim($path, '/');
}
return $url;
}
@@ -47,4 +48,15 @@ class PageRevision extends Model
return null;
}
/**
* Allows checking of the exact class, Used to check entity type.
* Included here to align with entities in similar use cases.
* (Yup, Bit of an awkward hack)
* @param $type
* @return bool
*/
public static function isA($type)
{
return $type === 'revision';
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -1,9 +1,11 @@
<?php namespace BookStack\Repos;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Page;
use BookStack\PageRevision;
use BookStack\Services\AttachmentService;
@@ -17,6 +19,10 @@ use Illuminate\Support\Collection;
class EntityRepo
{
/**
* @var Bookshelf
*/
public $bookshelf;
/**
* @var Book $book
@@ -66,6 +72,7 @@ class EntityRepo
/**
* EntityRepo constructor.
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
@@ -76,16 +83,23 @@ class EntityRepo
* @param SearchService $searchService
*/
public function __construct(
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
ViewService $viewService, PermissionService $permissionService,
TagRepo $tagRepo, SearchService $searchService
)
{
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page,
PageRevision $pageRevision,
ViewService $viewService,
PermissionService $permissionService,
TagRepo $tagRepo,
SearchService $searchService
) {
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->pageRevision = $pageRevision;
$this->entities = [
'bookshelf' => $this->bookshelf,
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
@@ -112,9 +126,9 @@ class EntityRepo
* @param bool $allowDrafts
* @return \Illuminate\Database\Query\Builder
*/
protected function entityQuery($type, $allowDrafts = false)
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
{
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission);
if (strtolower($type) === 'page' && !$allowDrafts) {
$q = $q->where('draft', '=', false);
}
@@ -137,10 +151,15 @@ class EntityRepo
* @param string $type
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return Entity
*/
public function getById($type, $id, $allowDrafts = false)
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
if ($ignorePermissions) {
$entity = $this->getEntity($type);
return $entity->newQuery()->find($id);
}
return $this->entityQuery($type, $allowDrafts)->find($id);
}
@@ -157,14 +176,16 @@ class EntityRepo
$q = $this->entityQuery($type)->where('slug', '=', $slug);
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
$q = $q->where('book_id', '=', function($query) use ($bookSlug) {
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
$query->select('id')
->from($this->book->getTable())
->where('slug', '=', $bookSlug)->limit(1);
});
}
$entity = $q->first();
if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
if ($entity === null) {
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
}
return $entity;
}
@@ -190,15 +211,18 @@ class EntityRepo
}
/**
* Get all entities of a type limited by count unless count if false.
* Get all entities of a type with the given permission, limited by count unless count is false.
* @param string $type
* @param integer|bool $count
* @param string $permission
* @return Collection
*/
public function getAll($type, $count = 20)
public function getAll($type, $count = 20, $permission = 'view')
{
$q = $this->entityQuery($type)->orderBy('name', 'asc');
if ($count !== false) $q = $q->take($count);
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
if ($count !== false) {
$q = $q->take($count);
}
return $q->get();
}
@@ -225,7 +249,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('created_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@@ -244,7 +270,9 @@ class EntityRepo
{
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
->orderBy('updated_at', 'desc');
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
if (strtolower($type) === 'page') {
$query = $query->where('draft', '=', false);
}
if ($additionalQuery !== false && is_callable($additionalQuery)) {
$additionalQuery($query);
}
@@ -312,6 +340,17 @@ class EntityRepo
->skip($count * $page)->take($count)->get();
}
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
{
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
@@ -341,12 +380,16 @@ class EntityRepo
$parents[$key] = $entities[$index];
$parents[$key]->setAttribute('pages', collect());
}
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
$tree[] = $entities[$index];
}
$entities[$index]->book = $book;
}
foreach ($entities as $entity) {
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
continue;
}
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
if (!isset($parents[$parentKey])) {
$tree[] = $entity;
@@ -425,7 +468,9 @@ class EntityRepo
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
$query = $query->where('book_id', '=', $bookId);
}
if ($currentId) $query = $query->where('id', '!=', $currentId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
@@ -436,9 +481,10 @@ class EntityRepo
*/
public function updateEntityPermissionsFromRequest($request, Entity $entity)
{
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
$entity->restricted = $request->get('restricted', '') === 'true';
$entity->permissions()->delete();
if ($request->has('restrictions')) {
if ($request->filled('restrictions')) {
foreach ($request->get('restrictions') as $roleId => $restrictions) {
foreach ($restrictions as $action => $value) {
$entity->permissions()->create([
@@ -448,6 +494,7 @@ class EntityRepo
}
}
}
$entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
}
@@ -465,14 +512,19 @@ class EntityRepo
public function createFromInput($type, $input = [], $book = false)
{
$isChapter = strtolower($type) === 'chapter';
$entity = $this->getEntity($type)->newInstance($input);
$entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
$entity->created_by = user()->id;
$entity->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entity) : $entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
$this->searchService->indexEntity($entity);
return $entity;
$entityModel = $this->getEntity($type)->newInstance($input);
$entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
$entityModel->created_by = user()->id;
$entityModel->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
/**
@@ -491,11 +543,38 @@ class EntityRepo
$entityModel->fill($input);
$entityModel->updated_by = user()->id;
$entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
/**
* Sync the books assigned to a shelf from a comma-separated list
* of book IDs.
* @param Bookshelf $shelf
* @param string $books
*/
public function updateShelfBooks(Bookshelf $shelf, string $books)
{
$ids = explode(',', $books);
// Check books exist and match ordering
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
$syncData = [];
foreach ($ids as $index => $id) {
if ($bookIds->contains($id)) {
$syncData[$id] = ['order' => $index];
}
}
$shelf->books()->sync($syncData);
}
/**
* Change the book that an entity belongs to.
* @param string $type
@@ -547,12 +626,39 @@ class EntityRepo
*/
protected function nameToSlug($name)
{
$slug = str_replace(' ', '-', strtolower($name));
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
return $slug;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
@@ -571,7 +677,7 @@ class EntityRepo
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->text = $this->pageToPlainText($draftPage);
$draftPage->draft = false;
$draftPage->revision_count = 1;
@@ -581,6 +687,43 @@ class EntityRepo
return $draftPage;
}
/**
* Create a copy of a page in a new location with a new name.
* @param Page $page
* @param Entity $newParent
* @param string $newName
* @return Page
*/
public function copyPage(Page $page, Entity $newParent, $newName = '')
{
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
$newChapter = $newParent->isA('chapter') ? $newParent : null;
$copyPage = $this->getDraftPage($newBook, $newChapter);
$pageData = $page->getAttributes();
// Update name
if (!empty($newName)) {
$pageData['name'] = $newName;
}
// Copy tags from previous page if set
if ($page->tags) {
$pageData['tags'] = [];
foreach ($page->tags as $tag) {
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
}
}
// Set priority
if ($newParent->isA('chapter')) {
$pageData['priority'] = $this->getNewChapterPriority($newParent);
} else {
$pageData['priority'] = $this->getNewBookPriority($newParent);
}
return $this->publishPageDraft($copyPage, $pageData);
}
/**
* Saves a page revision into the system.
* @param Page $page
@@ -590,7 +733,9 @@ class EntityRepo
public function savePageRevision(Page $page, $summary = null)
{
$revision = $this->pageRevision->newInstance($page->toArray());
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -601,10 +746,13 @@ class EntityRepo
$revision->revision_number = $page->revision_count;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
$this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
$revisionLimit = config('app.revision_limit');
if ($revisionLimit !== false) {
$revisionsToDelete = $this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
if ($revisionsToDelete->count() > 0) {
$this->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
}
}
return $revision;
@@ -618,7 +766,9 @@ class EntityRepo
*/
protected function formatHtml($htmlText)
{
if ($htmlText == '') return $htmlText;
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@@ -632,7 +782,9 @@ class EntityRepo
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
if (get_class($childNode) !== 'DOMElement') {
continue;
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
@@ -671,41 +823,56 @@ class EntityRepo
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @param bool $ignorePermissions
* @return mixed|string
*/
public function renderPage(Page $page)
public function renderPage(Page $page, $ignorePermissions = false)
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) return $content;
if (count($matches[0]) === 0) {
return $content;
}
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
$pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue;
if (is_nan($pageId)) {
continue;
}
$page = $this->getById('page', $pageId);
if ($page === null) {
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $page->html, $content);
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
continue;
}
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
$innerContent = '';
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
if ($isTopLevel) {
$innerContent .= $doc->saveHTML($matchingElem);
} else {
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
}
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
@@ -714,25 +881,34 @@ class EntityRepo
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
* Escape script tags within HTML content.
* @param string $html
* @return mixed
*/
public function getDraftPage(Book $book, $chapter = false)
protected function escapeScripts(string $html)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
return $html;
}
if ($chapter) $page->chapter_id = $chapter->id;
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
}
return $html;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
/**
* Get the plain text version of a page's content.
* @param Page $page
* @return string
*/
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page);
return strip_tags($html);
}
/**
@@ -758,14 +934,18 @@ class EntityRepo
*/
public function getPageNav($pageContent)
{
if ($pageContent == '') return [];
if ($pageContent == '') {
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) return [];
if (is_null($headers)) {
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
@@ -781,7 +961,7 @@ class EntityRepo
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function($header) use ($minLevel) {
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
@@ -816,8 +996,10 @@ class EntityRepo
$userId = user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
@@ -879,7 +1061,9 @@ class EntityRepo
public function getUserPageDraftMessage(PageRevision $draft)
{
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
return $message;
}
return $message . "\n" . trans('entities.pages_draft_edited_notification');
}
@@ -933,7 +1117,7 @@ class EntityRepo
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
$page->text = strip_tags($page->html);
$page->text = $this->pageToPlainText($page);
$page->updated_by = user()->id;
$page->save();
$this->searchService->indexEntity($page);
@@ -953,7 +1137,7 @@ class EntityRepo
if ($page->draft) {
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
$page->text = $this->pageToPlainText($page);
}
$page->save();
return $page;
@@ -975,7 +1159,9 @@ class EntityRepo
}
$draft->fill($data);
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
if (setting('app-editor') !== 'markdown') {
$draft->markdown = '';
}
$draft->save();
return $draft;
@@ -1013,9 +1199,22 @@ class EntityRepo
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Destroy a bookshelf instance
* @param Bookshelf $shelf
* @throws \Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
$this->destroyEntityCommonRelations($shelf);
$shelf->delete();
}
/**
* Destroy the provided book and all its child entities.
* @param Book $book
* @throws NotifyException
* @throws \Throwable
*/
public function destroyBook(Book $book)
{
@@ -1025,17 +1224,14 @@ class EntityRepo
foreach ($book->chapters as $chapter) {
$this->destroyChapter($chapter);
}
\Activity::removeEntity($book);
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$this->searchService->deleteEntityTerms($book);
$this->destroyEntityCommonRelations($book);
$book->delete();
}
/**
* Destroy a chapter and its relations.
* @param Chapter $chapter
* @throws \Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@@ -1045,27 +1241,25 @@ class EntityRepo
$page->save();
}
}
\Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$this->searchService->deleteEntityTerms($chapter);
$this->destroyEntityCommonRelations($chapter);
$chapter->delete();
}
/**
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws \Throwable
*/
public function destroyPage(Page $page)
{
\Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
$this->destroyEntityCommonRelations($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
@@ -1076,16 +1270,47 @@ class EntityRepo
$page->delete();
}
/**
* Destroy or handle the common relations connected to an entity.
* @param Entity $entity
* @throws \Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
\Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$this->permissionService->deleteJointPermissionsForEntity($entity);
$this->searchService->deleteEntityTerms($entity);
}
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
$shelfBooks = $bookshelf->books()->get();
$updatedBookCount = 0;
foreach ($shelfBooks as $book) {
if (!userCan('restrictions-manage', $book)) {
continue;
}
$book->permissions()->delete();
$book->restricted = $bookshelf->restricted;
$book->permissions()->createMany($shelfPermissions);
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
$updatedBookCount++;
}
return $updatedBookCount;
}
}

View File

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

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Repos;
use BookStack\Exceptions\PermissionsException;
use BookStack\RolePermission;
use BookStack\Role;
@@ -81,7 +80,7 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
@@ -91,13 +90,18 @@ class PermissionsRepo
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
$permissions = array_merge($permissions, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
}
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);
@@ -149,5 +153,4 @@ class PermissionsRepo
$this->permissionService->deleteJointPermissionsForRole($role);
$role->delete();
}
}
}

View File

@@ -33,6 +33,7 @@ class TagRepo
* @param $entityType
* @param $entityId
* @param string $action
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
@@ -51,7 +52,9 @@ class TagRepo
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) return collect();
if ($entity === null) {
return collect();
}
return $entity->tags;
}
@@ -94,7 +97,9 @@ class TagRepo
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
if ($tagName !== false) {
$query = $query->where('name', '=', $tagName);
}
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
return $query->get(['value'])->pluck('value');
@@ -111,7 +116,9 @@ class TagRepo
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') continue;
if (trim($tag['name']) === '') {
continue;
}
$newTags[] = $this->newInstanceFromInput($tag);
}
@@ -131,5 +138,4 @@ class TagRepo
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
}
}
}

View File

@@ -1,8 +1,12 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Image;
use BookStack\Role;
use BookStack\User;
use Exception;
use Images;
class UserRepo
{
@@ -57,13 +61,13 @@ class UserRepo
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
$query->where(function($query) use ($term) {
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);
});
@@ -72,40 +76,48 @@ class UserRepo
return $query->paginate($count);
}
/**
/**
* Creates a new user and attaches a role to them.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function registerNew(array $data)
public function registerNew(array $data, $verifyEmail = false)
{
$user = $this->create($data);
$user = $this->create($data, $verifyEmail);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
if (!config('services.disable_services')) {
try {
$avatar = \Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
$user->save();
\Log::error('Failed to save user gravatar image');
}
}
$this->downloadGravatarToUserAvatar($user);
return $user;
}
/**
* Give a user the default role. Used when creating a new user.
* @param $user
* @param User $user
*/
public function attachDefaultRole($user)
public function attachDefaultRole(User $user)
{
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
$user->attachRoleId($roleId);
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
$user->attachRoleId($roleId);
}
}
/**
* Assign a user to a system-level role.
* @param User $user
* @param $systemRoleName
* @throws NotFoundException
*/
public function attachSystemRole(User $user, $systemRoleName)
{
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
if ($role === null) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
}
/**
@@ -115,36 +127,49 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if (!$user->roles->pluck('name')->contains('admin')) return false;
if (!$user->hasSystemRole('admin')) {
return false;
}
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
$adminRole = $this->role->getSystemRole('admin');
if ($adminRole->users->count() > 1) {
return false;
}
return true;
}
/**
* Create a new basic instance of user.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function create(array $data)
public function create(array $data, $verifyEmail = false)
{
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => false
'email_confirmed' => $verifyEmail
]);
}
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
* @throws Exception
*/
public function destroy(User $user)
{
$user->socialAccounts()->delete();
$user->delete();
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
}
/**
@@ -156,7 +181,7 @@ class UserRepo
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
return Activity::userActivity($user, $count, $page);
}
/**
@@ -213,4 +238,27 @@ class UserRepo
return $this->role->where('system_name', '!=', 'admin')->get();
}
}
/**
* Get a gravatar image for a user and set it as their avatar.
* Does not run if gravatar disabled in config.
* @param User $user
* @return bool
*/
public function downloadGravatarToUserAvatar(User $user)
{
// Get avatar from gravatar and save
if (!config('services.gravatar')) {
return false;
}
try {
$avatar = Images::saveUserGravatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
} catch (Exception $e) {
\Log::error('Failed to save user gravatar image');
return false;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\EntityPermission;
@@ -25,6 +26,7 @@ class PermissionService
public $book;
public $chapter;
public $page;
public $bookshelf;
protected $db;
@@ -38,22 +40,31 @@ class PermissionService
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
public function __construct(
JointPermission $jointPermission,
EntityPermission $entityPermission,
Role $role,
Connection $db,
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page
) {
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
}
/**
@@ -67,13 +78,19 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param Entity[] $entities
*/
protected function readyEntityCache()
protected function readyEntityCache($entities = [])
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
$this->entityCache = [];
foreach ($entities as $entity) {
$type = $entity->getType();
if (!isset($this->entityCache[$type])) {
$this->entityCache[$type] = collect();
}
$this->entityCache[$type]->put($entity->id, $entity);
}
}
/**
@@ -83,14 +100,13 @@ class PermissionService
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
return $this->entityCache['book']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
if ($book === null) {
$book = false;
}
return $book;
@@ -103,14 +119,13 @@ class PermissionService
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
return $this->entityCache['chapter']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
if ($chapter === null) {
$chapter = false;
}
return $chapter;
@@ -122,7 +137,9 @@ class PermissionService
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
if ($this->userRoles !== false) {
return $this->userRoles;
}
$roles = [];
@@ -153,6 +170,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@@ -161,20 +184,36 @@ class PermissionService
*/
protected function bookFetchQuery()
{
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function($query) {
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
/**
* @param Collection $shelves
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
{
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all());
}
$this->createManyJointPermissions($shelves, $roles);
}
/**
* Build joint permissions for an array of books
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
@@ -187,13 +226,16 @@ class PermissionService
}
}
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities, $roles);
}
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
@@ -204,20 +246,27 @@ class PermissionService
return;
}
$entities[] = $entity->book;
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity->isA('chapter')) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities));
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
* @throws \Throwable
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
@@ -236,9 +285,15 @@ class PermissionService
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@@ -256,15 +311,16 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
$roleIds = array_map(function($role) {
$roleIds = array_map(function ($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('id', $roleIds)->delete();
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
}
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
@@ -274,24 +330,26 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
* @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
if (count($entities) === 0) {
return;
}
$this->db->transaction(function() use ($entities) {
$this->db->transaction(function () use ($entities) {
foreach (array_chunk($entities, 1000) as $entityChunk) {
$query = $this->db->table('joint_permissions');
foreach ($entityChunk as $entity) {
$query->orWhere(function(QueryBuilder $query) use ($entity) {
$query->orWhere(function (QueryBuilder $query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
});
}
@@ -299,10 +357,11 @@ class PermissionService
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param array $roles
* @throws \Throwable
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
@@ -310,7 +369,7 @@ class PermissionService
$permissionFetch = $this->entityPermission->newQuery();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function($query) use ($entity) {
$permissionFetch->orWhere(function ($query) use ($entity) {
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
});
}
@@ -327,7 +386,7 @@ class PermissionService
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->getRelationValue('permissions') as $permission) {
foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
@@ -341,7 +400,7 @@ class PermissionService
}
}
$this->db->transaction(function() use ($jointPermissions) {
$this->db->transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
}
@@ -357,8 +416,12 @@ class PermissionService
protected function getActions(Entity $entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
if ($entity->isA('chapter') || $entity->isA('book')) {
$baseActions[] = 'page-create';
}
if ($entity->isA('book')) {
$baseActions[] = 'chapter-create';
}
return $baseActions;
}
@@ -389,7 +452,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
if ($entity->isA('book')) {
if ($entity->isA('book') || $entity->isA('bookshelf')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
@@ -407,7 +470,10 @@ class PermissionService
}
}
return $this->createJointPermissionDataArray($entity, $role, $action,
return $this->createJointPermissionDataArray(
$entity,
$role,
$action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
@@ -421,7 +487,8 @@ class PermissionService
* @param $action
* @return bool
*/
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
{
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false;
}
@@ -457,18 +524,13 @@ class PermissionService
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
@@ -540,11 +602,12 @@ class PermissionService
* @param bool $fetchPageContent
* @return QueryBuilder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
{
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
@@ -553,17 +616,16 @@ class PermissionService
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
if (!$this->isAdmin()) {
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
// Add joint permission filter
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
@@ -591,11 +653,6 @@ class PermissionService
});
}
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
@@ -606,16 +663,13 @@ class PermissionService
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
@@ -646,11 +700,6 @@ class PermissionService
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
@@ -675,19 +724,6 @@ class PermissionService
return $q;
}
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
}
return $this->isAdminUser;
}
/**
* Get the current user
* @return User
@@ -710,5 +746,4 @@ class PermissionService
$this->userRoles = false;
$this->isAdminUser = null;
}
}
}

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\SocialAccount;
use Laravel\Socialite\Contracts\User as SocialUser;
class SocialAuthService
{
@@ -14,7 +15,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* SocialAuthService constructor.
@@ -56,18 +57,13 @@ class SocialAuthService
/**
* Handle the social registration process on callback.
* @param $socialDriver
* @return \Laravel\Socialite\Contracts\User
* @throws SocialDriverNotConfigured
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialUser
* @throws UserRegistrationException
*/
public function handleRegistrationCallback($socialDriver)
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
@@ -82,18 +78,26 @@ class SocialAuthService
}
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* Get the social user details via the social driver.
* @param string $socialDriver
* @return SocialUser
* @throws SocialDriverNotConfigured
* @throws SocialSignInException
*/
public function handleLoginCallback($socialDriver)
public function getSocialUser(string $socialDriver)
{
$driver = $this->validateDriver($socialDriver);
return $this->socialite->driver($driver)->user();
}
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInAccountNotUsed
*/
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
{
$socialId = $socialUser->getId();
// Get any attached social accounts or users
@@ -104,7 +108,8 @@ class SocialAuthService
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
return $this->logUserIn($socialAccount->user);
auth()->login($socialAccount->user);
return redirect()->intended('/');
}
// When a user is logged in but the social account does not exist,
@@ -134,14 +139,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
}
private function logUserIn($user)
{
auth()->login($user);
return redirect('/');
throw new SocialSignInAccountNotUsed($message, '/login');
}
/**
@@ -155,8 +153,12 @@ class SocialAuthService
{
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
if (!in_array($driver, $this->validSocialDrivers)) {
abort(404, trans('errors.social_driver_not_found'));
}
if (!$this->checkDriverConfigured($driver)) {
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
}
return $driver;
}
@@ -200,8 +202,28 @@ class SocialAuthService
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
* Check if the current config for the given driver allows auto-registration.
* @param string $driver
* @return bool
*/
public function driverAutoRegisterEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
* @param string $driver
* @return bool
*/
public function driverAutoConfirmEmailEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
}
/**
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialAccount
*/
public function fillSocialAccount($socialDriver, $socialUser)
@@ -225,5 +247,4 @@ class SocialAuthService
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());
}
}
}

View File

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

View File

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

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