Compare commits

...

454 Commits

Author SHA1 Message Date
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
4499ae84bb Made fixes to es languge files and users page
Fixed PHP formatting error in ES lang file and added tests to cover.
Made user edit page more responsive on smaller devices.
Fixed 'cancel' button on profile screen when the user does not have
permission to manage users.
2017-03-05 15:34:54 +00:00
Dan Brown
d4e790d3cf Added lang tests and update export text keys 2017-03-05 15:10:06 +00:00
Dan Brown
9b35aa42a2 Fixed spanish encoding, Added new lang to settings 2017-03-05 14:43:43 +00:00
Dan Brown
7163997367 Merge pull request #334 from diegoseso/master
First spanish translation effort
2017-03-05 14:18:55 +00:00
Dan Brown
2385a6c29b Merge pull request #325 from arietimmerman/dutchbranch
Dutch Language Files
2017-03-05 14:15:47 +00:00
Dan Brown
36173eb47d Removed extension from translation script link
Also fixed bug causing EN translation backup to not be passed
to javascript translation system.

Closes #328
2017-03-05 14:10:55 +00:00
Diego Jose Sosa Diaz
f7645824d9 First spanish translation effort 2017-03-03 00:21:33 +01:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
bcafa73faf Set composer to clean bootstrap/cache before an update 2017-02-27 16:55:40 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
e3eefba745 Fixed export testing and updated travis settings 2017-02-26 21:39:15 +00:00
Dan Brown
a90f564980 Made LDAP email attribute configurable via .env
Closes #306
2017-02-26 14:51:49 +00:00
Dan Brown
253132afdf Added chapter export options
Closes #177
2017-02-26 14:25:02 +00:00
Dan Brown
eded8abded Added book export and created export tests to cover
In reference to #177
2017-02-26 13:26:51 +00:00
Dan Brown
0abed1afe5 Added clear activity/revision commands. Cleaned commands.
Added testing to cover each command.
Removed example laravel inspire command.
Standardised command names to be behind 'bookstack' naming.
In reference to #320.
2017-02-26 09:16:24 +00:00
Dan Brown
22077d4181 Updated DOMPDF to latest version 2017-02-25 14:59:56 +00:00
Dan Brown
b0e849f413 Added checkbox sytax parsing to markdown lists
Closes #319
2017-02-25 13:16:26 +00:00
Dan Brown
af3c0e43a5 Prevented custom HTML being inserted on settings page
Gives option for fixing if badly formatted HTML is inserted.
Closes #310
2017-02-25 12:41:32 +00:00
Dan Brown
387047f262 Fixed inaccessible revisions, added regression tests
Fixes #309
2017-02-25 12:29:01 +00:00
Dan Brown
4a2a539c08 Merge pull request #295 from ReeseSebastian/master
Updated and improved german translation
2017-02-23 19:19:21 +00:00
Arie Timmerman
4214fcd2fa Updated Dutch language files 2017-02-11 11:58:45 +01:00
Arie Timmerman
b2b64fb853 Started with Dutch translation 2017-02-10 22:10:41 +01:00
Dan Brown
a6128a1df1 Merge bugfixes from branch 'v0.14' 2017-02-05 21:24:15 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
6638ee47d3 Fixed entities wrongly visible on 404
Also ensured header state as expected on 404.
In reference to BookStackApp/website#9
2017-02-05 21:19:29 +00:00
Dan Brown
65899a3e91 Prevented settings being overfetched from db/cache 2017-02-05 18:57:57 +00:00
Dan Brown
86625a7642 Neatened up social login/register buttons 2017-02-05 15:28:53 +00:00
Dan Brown
ee495450cc Improved multi-line callout rendering
Closes #300
2017-02-05 14:47:26 +00:00
Dan Brown
d369d315a7 Fixed non-browserkit testcase and seeder issues 2017-02-05 14:37:50 +00:00
Dan Brown
7c9937e924 Converted sort tests to non browserkit testing
Added testing to cover book sort endpoint.
Closes #283
2017-02-05 14:20:59 +00:00
Dan Brown
33a2999a57 Namespaced tests to align with new laravel default 2017-02-04 11:58:42 +00:00
Dan Brown
076693efc9 Added facebook, slack & twitter sign in options.
Also added icon svg blade helper.
Closes #125. Starts #213.
Requires documentation.
2017-02-04 11:01:49 +00:00
Sebastian Reese
54d1fcde5b Merge branch 'master' of https://github.com/BookStackApp/BookStack 2017-02-02 00:43:54 +01:00
Sebastian Reese
1c656d6556 Updated and improved german translation 2017-02-02 00:43:24 +01:00
Dan Brown
2431ce9f86 Merge branch 'v0.14' 2017-02-01 22:28:38 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
80f844139c Fixed missing subscript styling
Closes #284
2017-02-01 22:20:44 +00:00
Dan Brown
9eecaea31a Attempt to fix bookchildren and user getThumb
Hopefully Fixes #292 and #294 and #287
2017-02-01 22:16:32 +00:00
Dan Brown
3ccfa0e7fc Fixed readme badge links & added contributing block 2017-01-30 19:31:24 +00:00
Dan Brown
6669998c10 Upgraded to Laravel 5.4 2017-01-25 19:35:40 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
492e2f173e Fixed error causing permissions to be deleted on book sort
Closes #282
2017-01-23 22:27:11 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
380f0f2042 Prevented a missing avatar from crashing the application 2017-01-22 12:19:50 +00:00
Dan Brown
33d4844f17 Fixed role 'manage own permissions' permission 2017-01-22 12:16:02 +00:00
Dan Brown
989de47f22 Fixed hidden book children for admins on upgrade 2017-01-22 12:02:30 +00:00
Dan Brown
8f19231ed5 Added options to use database cache & sessions 2017-01-21 16:39:50 +00:00
Dan Brown
5c60f27a7d Merge branch 'page_transclusion'
Closes #123
2017-01-21 16:17:33 +00:00
Dan Brown
2d4034f3b7 Added transclusion tests and fixed other tests 2017-01-21 16:16:27 +00:00
Dan Brown
56d58ad8e5 Updated pointer to be able to show includes
Also fixed pointer copying on flash-blocked browsers
2017-01-21 14:58:03 +00:00
Dan Brown
a4f6bc63f0 Updated page include logic to use blade-style tags
It will also snippets of a page if and id is provided in a tag
2017-01-21 13:53:00 +00:00
Dan Brown
26da81a3b0 Added pt_BR to language dropdown and renamed folder 2017-01-18 20:04:29 +00:00
Dan Brown
ec9410b510 Merge pull request #279 from NakaharaL/master
Brazilian Portuguese Localization
2017-01-18 19:48:53 +00:00
NakaharaL
a0035e4de2 Brazilian Portuguese Localization
Translated files for messages from English to Brazilian Portuguese
2017-01-18 08:29:18 -03:00
Dan Brown
e4e3b25c22 Started page transclusion system 2017-01-16 21:24:48 +00:00
Dan Brown
d8c5f72258 Updated issue template and added TinyMCE autolinking
Closes #267
2017-01-16 19:33:29 +00:00
Dan Brown
d67ad47b2c Merge pull request #274 from GeneralMediaCH/feature/translations
French translations
2017-01-16 19:23:30 +00:00
Dan Brown
b08e49b59d Updated CI to not use github token 2017-01-16 19:14:58 +00:00
sirgix
d3dc73ca06 remove trailing spaces 2017-01-16 15:34:34 +01:00
sirgix
d5ea15e6dd fix typo 2017-01-16 14:51:45 +01:00
sirgix
5d45286646 french translation 2017-01-16 14:51:33 +01:00
Dan Brown
dabf149411 Added user setting system and added user-lang option
Supports #115
2017-01-15 16:27:24 +00:00
Dan Brown
ee5ded6e1e Updated permission bookChildrenQuery to use QueryBuilder 2017-01-15 15:00:29 +00:00
Dan Brown
598b07b53d Updated LDAP to allow protocol to be specified
As per details by fredericmohr in #236
2017-01-14 17:55:09 +00:00
Dan Brown
e211f31370 Merge branch 'patch-2' of git://github.com/fredericmohr/BookStack into fredericmohr-patch-2 2017-01-14 17:30:49 +00:00
Dan Brown
0bcf608e0b Fixed page navigation with special chars in id
Also made so it smooth-scrolls and uses app theme color.
Fixes #254
2017-01-14 16:36:12 +00:00
Dan Brown
969ad8911c Updated page nav to hide when empty 2017-01-14 15:34:52 +00:00
Dan Brown
581c382f65 Fixed image delete permission issue
Also fixed missing translations and wrote tests to cover issue.
Fixes #258
2017-01-08 19:19:30 +00:00
Dan Brown
f7f86ff821 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-01-08 18:43:33 +00:00
Dan Brown
212cd710aa Fixed default empty app settings effecting blank app color
Fixes #265
2017-01-08 18:42:46 +00:00
Dan Brown
33c44d3c0f Merge pull request #263 from Abijeet/typo-fix
Typo fix
2017-01-08 13:55:44 +00:00
Dan Brown
0faa130cfd Fixed offset code blocks when editing in markdown.
Fixes #264
2017-01-08 13:31:53 +00:00
Abijeet Patro
af76580b98 Fixes typo causing the message not to be displayed 2017-01-03 22:18:13 +05:30
Abijeet Patro
b526d172d6 Merge pull request #5 from BookStackApp/master
Getting the latest from Bookstack to push the typo-fix
2017-01-03 22:16:39 +05:30
Dan Brown
f2917fc462 Added tests to cover social login actions
Closes #244
2017-01-02 14:56:58 +00:00
Dan Brown
7c8c4c2a05 Normalised page nav header inset when only small headers are used 2017-01-02 12:13:03 +00:00
Dan Brown
3595ac2551 Merge pull request #262 from BookStackApp/entity_repo_refactor
Entity repo refactor
2017-01-02 11:12:44 +00:00
Dan Brown
8453191dfb Finished refactor of entity repos
Removed entity-specific repos and standardised
the majority of repo calls to be applicable to
all entity types
2017-01-02 11:07:27 +00:00
Dan Brown
65796cfc7b Rewrote book children query 2017-01-01 21:21:11 +00:00
Dan Brown
bab27462ab Fixed issue where default user was over-fetched 2017-01-01 17:33:06 +00:00
Dan Brown
241278226f Refactored search and slug repo components 2017-01-01 16:57:47 +00:00
Dan Brown
7f9de2c8ab Started refactor to merge entity repos 2017-01-01 16:05:44 +00:00
Dan Brown
f91f33c236 Updated readme attribution and npm scripts 2017-01-01 12:51:23 +00:00
Dan Brown
3f0ef57d31 Added wkhtmltopdf support and done some style tweaks
Closes #248
2017-01-01 12:20:30 +00:00
Dan Brown
0eb90cb3b6 Fixed carbon locale setting 2016-12-31 14:38:04 +00:00
Dan Brown
9fe158b78a Merge pull request #255 from olexus/master
Add Carbon localization support
2016-12-31 14:36:01 +00:00
Dan Brown
b14222dabd Merge pull request #234 from BookStackApp/translations
Setup for translations
2016-12-31 14:33:25 +00:00
Dan Brown
a24f3d7d47 Merge branch 'master' into translations 2016-12-31 14:32:52 +00:00
Dan Brown
c9700e38e2 Created solution for JS translations
Also tidied up existing components and JS
2016-12-31 14:27:40 +00:00
Dan Brown
05316c90ba converted image picker to blade-based component
Also updated some other JS translations
2016-12-24 15:21:19 +00:00
Dan Brown
242dc21876 Converted toggle switch into a blade/jquery template 2016-12-22 19:41:32 +00:00
Dan Brown
08c4b9ac7c Standardised JS vars and imports/exports 2016-12-19 19:16:31 +00:00
olexus
f30f4579e9 Add Carbon localization support 2016-12-19 11:05:06 +03:00
Dan Brown
573357a08c Extracted text from logic files 2016-12-04 16:51:39 +00:00
Dan Brown
0775cd09a1 Extracted text for remaining views 2016-12-04 14:08:04 +00:00
Dan Brown
96075dee7b Extracted text from page views & standardised breadcrumbs 2016-12-03 18:35:40 +00:00
Dan Brown
066adf3cea Moved text for errors and form views.
Updated 404 page with additional links
2016-12-03 13:31:54 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
286f9b0c7d Moved page tags to sidebar
Fixed #238
2016-11-27 19:37:57 +00:00
Dan Brown
c403d05755 Fixed social login routes
Fixes #239
2016-11-27 19:11:15 +00:00
Dan Brown
57dc53ceff Extracted text from book & chapter views 2016-11-17 13:33:07 +00:00
Frederic Mohr
340d3f833b Update Ldap.php
This is a very crude workaround, a better solution is explained in the comments I added.
2016-11-15 11:10:12 +01:00
Dan Brown
694a9459c1 Moved text from auth views into lang files 2016-11-13 16:34:28 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
0fb1fc87c8 Enabled utf8 slugs
Prevents slug change when using only non-ascii chars
Allows use of more localised urls.

Closes #233
2016-11-12 17:16:52 +00:00
Dan Brown
d3c7aada89 Fixed attachments on draft pages 2016-11-12 14:21:54 +00:00
Dan Brown
e639600ba5 Renamed files to attachments 2016-11-12 14:12:26 +00:00
Dan Brown
d439fb459b Made language configurable via .env file 2016-11-12 13:30:00 +00:00
Dan Brown
9d8a176182 Merge branch 'localization-de' of git://github.com/robertlandes/BookStack into robertlandes-localization-de 2016-11-12 13:27:37 +00:00
Dan Brown
600055bc73 Fixed tag searches and added tag search regression test
Fixes #222
2016-11-12 13:21:16 +00:00
Robert Landes
e691b401c8 Added initial translation into German (formal)
This translation is based on the current english language files. It is a
transaltion into German (formal) language.  A language selector on the
Settings page would be a nice addition.
2016-11-12 14:16:07 +01:00
Dan Brown
672b15d36c Fixed attachment base-url usage and non-existant images
Images now self-delete if the original file does not exist.
Prevents simply getting non-fixable errors.

Also cleaned some JS.
2016-11-12 12:41:34 +00:00
Dan Brown
ac80723058 Merge fixes from branch 'v0.12' 2016-11-12 11:40:54 +00:00
Dan Brown
ab468bac3c Updated build and versioning system
Added versioning file instead of using git tags
(Step towards removing git as a dependancy in the future)

Updated gulpfile to fit with verisioning system and cleaned
up node dependancies.

Fixes #108
2016-10-30 17:44:00 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
2af0021c2b Fixed image tests after amends to url system 2016-10-30 12:58:01 +00:00
Dan Brown
0f2eaccb39 Added quick test to cover hypen breakage 2016-10-30 12:15:11 +00:00
Dan Brown
b251671e3f Amended search to not break on non-alpha-num chars
And also fixed exact term matches that contain non-alpha-num chars
Fixes #212
2016-10-30 12:08:22 +00:00
Dan Brown
c4eed37d8e Added custom head content into public pages
Closes #211
2016-10-30 11:46:23 +00:00
Dan Brown
8b43b91057 Improved password reset flow with notifications.
Also added links to sign-in/register.
Fixed links in emails sent out.

Fixes #210 and #218.
2016-10-30 11:36:57 +00:00
Dan Brown
91fe7f0bee Fixed PDF export table width
Closes #203

Signed-off-by: Dan Brown <ssddanbrown@googlemail.com>
2016-10-30 10:41:18 +00:00
Dan Brown
5cfb7b8de4 Altered 'ol' element padding to not clip numbering
Allows usage to 3-digits now
Closes #204
2016-10-30 10:23:49 +00:00
Dan Brown
6329a1842a Fixed issue with callouts overflowing page tags
Closes #179
2016-10-30 10:19:45 +00:00
Dan Brown
a6c6c6e300 Merge pull request #205 from ssddanbrown/attachments
Implementation of File Attachments
2016-10-23 18:01:20 +01:00
Dan Brown
30458405ce Page Attachments - Improved UI, Now initially complete
Closes #62
2016-10-23 17:55:48 +01:00
Dan Brown
91220239e5 Added in attachment tests 2016-10-23 15:25:04 +01:00
Dan Brown
7ee695d74a File upload deletion complete & added extension handling
Also fixed issue with file editing on JS side
2016-10-23 13:36:45 +01:00
Dan Brown
867fc8be64 Added basic attachment editing functionality 2016-10-11 20:39:11 +01:00
Dan Brown
89509b487a Added attachment creation from link/name 2016-10-10 21:13:18 +01:00
Dan Brown
ac0b29fb6d Added view, deletion and permissions for files 2016-10-10 20:30:27 +01:00
Dan Brown
673c74ddfc Started work on attachments
Created base models and started user-facing controls.
2016-10-09 18:58:22 +01:00
Dan Brown
3b7d223b0c Updated and added tests for new default user system
Closes #138
2016-09-29 17:07:58 +01:00
Dan Brown
b662670efc Prevented guest users creating draft pages. 2016-09-29 15:56:57 +01:00
Dan Brown
771626b6ec Started work on making the public role/user configurable
Create a new 'public' guest user and made the public
role visible on role setting screens.
2016-09-29 12:43:46 +01:00
Dan Brown
f15cc5bdfa Separated revision preview and diff & fixed chosen diff html
Closes #8
2016-09-29 10:10:46 +01:00
Dan Brown
fff5bbcee4 Merge branch 'diff' of git://github.com/younes0/BookStack into younes0-diff 2016-09-29 09:32:40 +01:00
Dan Brown
42d8e9e5bd Improved numeric term search capabilities
Prevented a quoted term also being added to fuzzy searches
and also added check to see if the term is numeric to check if
an exact match is required.

Closes #200
2016-09-29 09:13:15 +01:00
Dan Brown
da10c50945 Added app name header display setting + extracted setting text
Closes #194
2016-09-22 18:53:22 +01:00
Dan Brown
24523cf31d Added page autosave request failure notification
Closes #192
2016-09-18 15:10:27 +01:00
Dan Brown
1d681e53e4 Added page navigation and tweaked header styles
Changed header selection in editor to be more descriptive and
to provide a wider range of styles.

Closes #68
2016-09-18 14:49:36 +01:00
Dan Brown
e0235fda8b Made registration gravatar/email requests fail gracefully
* Extracted any email confirmation text into langs.
* Added new notification on confirmation email send fail.

Closes #187
2016-09-17 21:33:55 +01:00
Dan Brown
9dc9724e15 Laravel 5.3 upgrade (#189)
* Started move to laravel 5.3

* Started updating login & registration flows for laravel 5.3 update

* Updated app emails to notification system

* Fixed registations bugs and removed email confirmation model

* Fixed large portion of laravel post-upgrade issues

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

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

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

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

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

Closes #25
2016-02-07 10:17:38 +00:00
Dan Brown
8e8d582bc6 Updated app requirements & Added some friendlier errors 2016-02-03 20:55:37 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
e87db96fc0 Updated folder permissions installation instructions & fixed issue with handling image saving on user creation 2016-02-02 07:34:48 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
4bb7f0613f Fixed issue with initial user not having a password 2016-02-01 18:30:50 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
080acf0a62 Added plaintext & basic PDF page Export 2016-01-31 17:53:30 +00:00
Dan Brown
ea2e16cabb Added page HTML export 2016-01-20 22:13:13 +00:00
Dan Brown
7bcd967fd9 Increased LDAP testing and fixed any Auth-based bugs found 2016-01-17 15:20:07 +00:00
Dan Brown
bb87401d10 Improved image upload error handling and fixed bad user-avatar rewriting 2016-01-17 15:19:26 +00:00
Dan Brown
0821672e70 Cleaned tests up, Started LDAP tests, Created LDAP wrapper 2016-01-15 23:21:47 +00:00
Dan Brown
14feef3679 Updated user interfaces for LDAP and added email from LDAP 2016-01-13 22:22:30 +00:00
Dan Brown
1c8c9e65c5 Got LDAP auth working to a functional state 2016-01-11 22:41:05 +00:00
Dan Brown
14ca31768c Updated laravel to 5.2 and started ldap implementation 2016-01-09 19:24:21 +00:00
Dan Brown
e27a630a09 Updated readme with social & update instructions 2016-01-02 16:24:09 +00:00
Dan Brown
9319f99a3d Updated readme with installation clarification 2016-01-02 15:02:19 +00:00
Dan Brown
d6739c1158 Merge branch 'master' of github.com:ssddanbrown/BookStack 2016-01-02 14:51:36 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
d3709de035 Added more tests to increase test coverage 2016-01-02 14:48:35 +00:00
Dan Brown
7178c66cf5 Fix error with database migrate command 2016-01-01 21:46:55 +00:00
Dan Brown
f60a0c3b76 Improved 404 page and updated tests for empty search 2016-01-01 09:03:40 +00:00
Dan Brown
9a470b07fd Added public build folder and support for a demo mode 2015-12-31 17:57:34 +00:00
Dan Brown
0d8ca22487 Swapped vue for angular js 2015-12-31 17:32:03 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
e519236d94 Removed external fonts from page forms and remove initial form flicker 2015-12-31 17:19:29 +00:00
Dan Brown
5bf811a488 Sanitized user-text from angular & fixed some z-index bugs on pages 2015-12-30 21:38:20 +00:00
Dan Brown
3300edc51a Changed google cdn libs/fonts to local content. No external access is now required 2015-12-30 21:15:41 +00:00
Dan Brown
a592eaeb91 Updated page pointer to sit near mouse location and extracted page js into browserify bundle 2015-12-30 20:48:57 +00:00
Dan Brown
cca3533d35 Improved error messages for image uploads and formatted much js 2015-12-30 19:57:17 +00:00
Dan Brown
46c2e8b14e Moved all vuejs parts over to angular 2015-12-30 18:38:18 +00:00
Dan Brown
3347b3b2f5 Started transfer to angularjs 2015-12-29 16:39:25 +00:00
Dan Brown
445f939822 Fixed issue with searching invalid chars and page-content compiliation 2015-12-29 15:37:13 +00:00
Dan Brown
05c4b2089c Accounted for non-existant entities 2015-12-28 17:19:23 +00:00
Dan Brown
6e75bcdc37 Updated tinymce 2015-12-28 16:16:27 +00:00
Dan Brown
4db692309b Standardized vue component http access and fixed some small bugs 2015-12-28 15:58:13 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
98d9d8d71b Changed public template to use new system 2015-12-16 17:29:13 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
2a4ff6fb6a Implemented custom asset versioning to make creating releases easier 2015-12-16 17:09:44 +00:00
Dan Brown
f4b342a575 Updated readme with fixes and attribution 2015-12-16 14:54:29 +00:00
Dan Brown
497747e621 Fixed issue with deleted users in page revision list & extended tests to cover. 2015-12-15 19:53:46 +00:00
Dan Brown
af33156369 Fixed name retrieval on missing users and added tests to cover along with some test helper methods 2015-12-15 19:27:36 +00:00
Dan Brown
123dc11583 Fixed issue with users that have no avatar 2015-12-14 20:30:40 +00:00
Dan Brown
93223fcd3d Cleaned up gravatar image importing 2015-12-14 20:13:32 +00:00
Dan Brown
8f7c642f32 Added custom user avatars 2015-12-09 22:30:55 +00:00
Dan Brown
db3acabc66 Added an image service and facade, Cleaned Image Model 2015-12-09 19:50:17 +00:00
Dan Brown
a3188d349c Added mulit image-type compatability to manager & app and added scaled image selection 2015-12-08 22:04:59 +00:00
Dan Brown
32d5b12d27 Fixed bad s3 stock url creation 2015-12-08 21:01:37 +00:00
Dan Brown
c88096b7e2 Revamped image system to use driver-agnotstic storage and be more efficent 2015-12-07 23:00:34 +00:00
Dan Brown
46c905df8a Added search name weighting. Closes #27. 2015-12-05 15:11:48 +00:00
Dan Brown
c32d70abc4 Added custom meta titles to many pages. Closes #30. 2015-12-05 14:41:51 +00:00
Dan Brown
f1c2866fbc Added Popular books list with relevant tests 2015-12-02 20:22:41 +00:00
Dan Brown
9f435553dc Updated composer, Fixed sidebar tree list & Added extra pages to Dummy seeder. Fixes #28, Fixes #29 2015-12-02 19:10:05 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
5e21ecc526 Fixed crash on public entitiy viewing 2015-12-01 21:14:39 +00:00
Dan Brown
9969698783 Floating page content bugfix and prevent incompatable image uploads. Fixes #26. 2015-12-01 20:26:09 +00:00
Dan Brown
b498b60cdd Updated readme with simpler instructions 2015-12-01 19:53:43 +00:00
Dan Brown
b97d5704e1 Fixed some lib paths and seperated out duplicated notifications 2015-11-30 22:23:48 +00:00
438 changed files with 25261 additions and 5303 deletions

View File

@@ -3,24 +3,60 @@ APP_ENV=production
APP_DEBUG=false
APP_KEY=SomeRandomString
# The below url has to be set if using social auth options
# or if you are not using BookStack at the root path of your domain.
# APP_URL=http://bookstack.dev
# Database details
DB_HOST=localhost
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database__user_password
DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
STORAGE_S3_KEY=false
STORAGE_S3_SECRET=false
STORAGE_S3_REGION=false
STORAGE_S3_BUCKET=false
# Storage URL
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
# URL for social login redirects, NO TRAILING SLASH
APP_URL=http://bookstack.dev
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Mail settings
MAIL_DRIVER=smtp

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

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

18
.gitignore vendored
View File

@@ -5,11 +5,19 @@ Homestead.yaml
/public/dist
.idea
/public/plugins
/public/css
/public/js
/public/uploads
/public/css/*.map
/public/js/*.map
/public/bower
/public/build
/storage/images
_ide_helper.php
/storage/debugbar
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs

28
.travis.yml Normal file
View File

@@ -0,0 +1,28 @@
dist: trusty
sudo: false
language: php
php:
- 7.0
cache:
directories:
- $HOME/.composer/cache
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- 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
- php artisan migrate --force -n --database=mysql_testing
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
after_failure:
- cat storage/logs/laravel.log
script:
- phpunit

21
LICENSE Normal file
View File

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

View File

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

36
app/Attachment.php Normal file
View File

@@ -0,0 +1,36 @@
<?php namespace BookStack;
class Attachment extends Ownable
{
protected $fillable = ['name', 'order'];
/**
* Get the downloadable file name for this upload.
* @return mixed|string
*/
public function getFileName()
{
if (str_contains($this->name, '.')) return $this->name;
return $this->name . '.' . $this->extension;
}
/**
* Get the page this file was uploaded to.
* @return Page
*/
public function page()
{
return $this->belongsTo(Page::class, 'uploaded_to');
}
/**
* Get the url of this file.
* @return string
*/
public function getUrl()
{
return baseUrl('/attachments/' . $this->id);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Activity;
use Illuminate\Console\Command;
class ClearActivity extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-activity';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear user activity from the system';
protected $activity;
/**
* Create a new command instance.
*
* @param Activity $activity
*/
public function __construct(Activity $activity)
{
$this->activity = $activity;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->activity->newQuery()->truncate();
$this->comment('System activity cleared');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-revisions
{--a|all : Include active update drafts in deletion}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear page revisions';
protected $pageRevision;
/**
* Create a new command instance.
*
* @param PageRevision $pageRevision
*/
public function __construct(PageRevision $pageRevision)
{
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
$this->comment('Revisions deleted');
}
}

View File

@@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
class ResetViews extends Command
class ClearViews extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'views:reset';
protected $signature = 'bookstack:clear-views';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset all view-counts for all entities.';
protected $description = 'Clear all view-counts for all entities.';
/**
* Create a new command instance.
@@ -37,5 +37,6 @@ class ResetViews extends Command
public function handle()
{
\Views::resetAll();
$this->comment('Views cleared');
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

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

View File

@@ -13,8 +13,10 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
\BookStack\Console\Commands\ClearViews::class,
\BookStack\Console\Commands\ClearActivity::class,
\BookStack\Console\Commands\ClearRevisions::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
];
/**
@@ -25,7 +27,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
//
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class EmailConfirmation extends Model
{
protected $fillable = ['user_id', 'token'];
public function user()
{
return $this->belongsTo('BookStack\User');
}
}

View File

@@ -1,29 +1,10 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
abstract class Entity extends Model
class Entity extends Ownable
{
/**
* Relation for the user that created this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
{
return $this->belongsTo('BookStack\User', 'created_by');
}
/**
* Relation for the user that updated this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function updatedBy()
{
return $this->belongsTo('BookStack\User', 'updated_by');
}
protected $fieldsToSearch = ['name', 'description'];
/**
* Compares this entity to another given entity.
@@ -47,11 +28,7 @@ abstract class Entity extends Model
if ($matches) return true;
if ($entity->isA('chapter') && $this->isA('book')) {
return $entity->book_id === $this->id;
}
if ($entity->isA('page') && $this->isA('book')) {
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
return $entity->book_id === $this->id;
}
@@ -68,25 +45,64 @@ abstract class Entity extends Model
*/
public function activity()
{
return $this->morphMany('BookStack\Activity', 'entity')->orderBy('created_at', 'desc');
return $this->morphMany(Activity::class, 'entity')->orderBy('created_at', 'desc');
}
/**
* Get View objects for this entity.
* @return mixed
*/
public function views()
{
return $this->morphMany('BookStack\View', 'viewable');
return $this->morphMany(View::class, 'viewable');
}
/**
* Get just the views for the current user.
* @return mixed
* Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function userViews()
public function tags()
{
return $this->views()->where('user_id', '=', auth()->user()->id);
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get this entities restrictions.
*/
public function permissions()
{
return $this->morphMany(EntityPermission::class, 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
{
return $this->permissions()->where('role_id', '=', $role_id)
->where('action', '=', $action)->count() > 0;
}
/**
* Check if this entity has live (active) restrictions in place.
* @param $role_id
* @param $action
* @return bool
*/
public function hasActiveRestriction($role_id, $action)
{
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
}
/**
* Get the entity jointPermissions this is connected to.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function jointPermissions()
{
return $this->morphMany(JointPermission::class, 'entity');
}
/**
@@ -97,16 +113,43 @@ abstract class Entity extends Model
*/
public static function isA($type)
{
return static::getName() === strtolower($type);
return static::getType() === strtolower($type);
}
/**
* Gets the class name.
* Get entity type.
* @return mixed
*/
public static function getType()
{
return strtolower(static::getClassName());
}
/**
* Get an instance of an entity of the given type.
* @param $type
* @return Entity
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
}
return app('BookStack\\' . $className);
}
/**
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public static function getName()
public function getShortName($length = 25)
{
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
if (strlen($this->name) <= $length) return $this->name;
return substr($this->name, 0, $length - 3) . '...';
}
/**
@@ -116,33 +159,61 @@ abstract class Entity extends Model
* @param string[] array $wheres
* @return mixed
*/
public static function fullTextSearch($fieldsToSearch, $terms, $wheres = [])
public function fullTextSearchQuery($terms, $wheres = [])
{
$termString = '';
foreach ($terms as $term) {
$termString .= $term . '* ';
$exactTerms = [];
$fuzzyTerms = [];
$search = static::newQuery();
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
} else {
$term = '' . $term . '*';
if ($term !== '*') $fuzzyTerms[] = $term;
}
}
$fields = implode(',', $fieldsToSearch);
$search = static::whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
// Perform fulltext search if relevant terms exist.
if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
}
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) {
foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
if (!static::isA('book')) {
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if ($this->isA('chapter')) {
$search = $search->with('book');
}
if (static::isA('page')) {
$search = $search->with('chapter');
}
return $search->get();
return $search->orderBy($orderBy, 'desc');
}
/**
* Get the url for this item.
* @return string
*/
abstract public function getUrl();
}

18
app/EntityPermission.php Normal file
View File

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

View File

@@ -1,8 +0,0 @@
<?php
namespace BookStack\Events;
abstract class Event
{
//
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,215 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use BookStack\Repos\EntityRepo;
use BookStack\Services\AttachmentService;
use Illuminate\Http\Request;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $attachment;
protected $entityRepo;
/**
* AttachmentController constructor.
* @param AttachmentService $attachmentService
* @param Attachment $attachment
* @param EntityRepo $entityRepo
*/
public function __construct(AttachmentService $attachmentService, Attachment $attachment, EntityRepo $entityRepo)
{
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Endpoint at which attachments are uploaded to.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function upload(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$uploadedFile = $request->file('file');
try {
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($attachment);
}
/**
* Update an uploaded attachment.
* @param int $attachmentId
* @param Request $request
* @return mixed
*/
public function uploadUpdate($attachmentId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$uploadedFile = $request->file('file');
try {
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($attachment);
}
/**
* Update the details of an existing file.
* @param $attachmentId
* @param Request $request
* @return Attachment|mixed
*/
public function update($attachmentId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'url|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return response()->json($attachment);
}
/**
* Attach a link to a page.
* @param Request $request
* @return mixed
*/
public function attachLink(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|url|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$attachmentName = $request->get('name');
$link = $request->get('link');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
return response()->json($attachment);
}
/**
* Get the attachments for a specific page.
* @param $pageId
* @return mixed
*/
public function listForPage($pageId)
{
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
}
/**
* Update the attachment sorting.
* @param $pageId
* @param Request $request
* @return mixed
*/
public function sortForPage($pageId, Request $request)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
]);
$page = $this->entityRepo->getById('page', $pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
return response()->json(['message' => trans('entities.attachments_order_updated')]);
}
/**
* Get an attachment from storage.
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
*/
public function get($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
$this->checkOwnablePermission('page-view', $page);
if ($attachment->external) {
return redirect($attachment->path);
}
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return response($attachmentContents, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
]);
}
/**
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
*/
public function delete($attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
return response()->json(['message' => trans('entities.attachments_deleted')]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Password;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
/**
* Send a reset link to the given user.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function sendResetLinkEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
session()->flash('success', $message);
return back()->with('status', trans($response));
}
// If an error was returned by the password broker, we will get this message
// translated so we can notify a user of the problem. We'll redirect back
// to where the users came from so they can attempt this process again.
return back()->withErrors(
['email' => trans($response)]
);
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
parent::__construct();
}
public function username()
{
return config('auth.method') === 'standard' ? 'email' : 'username';
}
/**
* Overrides the action when a user is authenticated.
* If the user authenticated but does not exist in the user table we create them.
* @param Request $request
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
*/
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 && $user->email === null && !$request->has('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('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) {
throw new AuthException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
}
$user->save();
$this->userRepo->attachDefaultRole($user);
auth()->login($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
}
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
}

View File

@@ -2,80 +2,94 @@
namespace BookStack\Http\Controllers\Auth;
use Illuminate\Http\Request;
use BookStack\Exceptions\ConfirmationEmailException;
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;
use Illuminate\Http\Response;
use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Foundation\Auth\RegistersUsers;
class AuthController extends Controller
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Registration & Login Controller
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users, as well as the
| authentication of existing users. By default, this controller uses
| a simple trait to add these behaviors. Why don't you explore it?
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use AuthenticatesAndRegistersUsers, ThrottlesLogins;
protected $loginPath = '/login';
protected $redirectPath = '/';
protected $redirectAfterLogout = '/login';
use RegistersUsers;
protected $socialAuthService;
protected $emailConfirmationService;
protected $userRepo;
/**
* Create a new authentication controller instance.
* @param SocialAuthService $socialAuthService
* Where to redirect users after login / registration.
*
* @var string
*/
protected $redirectTo = '/';
protected $redirectPath = '/';
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin', 'getRegister', 'postRegister']]);
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
$this->redirectTo = baseUrl('/');
$this->redirectPath = baseUrl('/');
parent::__construct();
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
}
/**
* Check whether or not registrations are allowed in the app settings.
* @throws UserRegistrationException
*/
protected function checkRegistrationAllowed()
{
if (!\Setting::get('registration-enabled')) {
throw new UserRegistrationException('Registrations are currently disabled.', '/login');
if (!setting('registration-enabled')) {
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
}
}
/**
* Show the application registration form.
* @return \Illuminate\Http\Response
* @return Response
*/
public function getRegister()
{
@@ -86,9 +100,10 @@ class AuthController extends Controller
/**
* Handle a registration request for the application.
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
* @param Request|\Illuminate\Http\Request $request
* @return Response
* @throws UserRegistrationException
* @throws \Illuminate\Foundation\Validation\ValidationException
*/
public function postRegister(Request $request)
{
@@ -106,40 +121,34 @@ class AuthController extends Controller
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* Create a new user instance after a valid registration.
* @param array $data
* @return User
*/
protected function socialRegisterCallback($socialDriver)
protected function create(array $data)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
/**
* The registrations flow for all users.
* @param array $userData
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\ConfirmationEmailException
* @throws ConfirmationEmailException
*/
protected function registerUser(array $userData, $socialAccount = false)
{
if (\Setting::get('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', \Setting::get('registration-restrict')));
if (setting('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException('That email domain does not have access to this application', '/register');
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
}
@@ -148,21 +157,25 @@ class AuthController extends Controller
$newUser->socialAccounts()->save($socialAccount);
}
if (\Setting::get('registration-confirmation') || \Setting::get('registration-restrict')) {
$newUser->email_confirmed = false;
if (setting('registration-confirmation') || setting('registration-restrict')) {
$newUser->save();
$this->emailConfirmationService->sendConfirmation($newUser);
try {
$this->emailConfirmationService->sendConfirmation($newUser);
} catch (Exception $e) {
session()->flash('error', trans('auth.email_confirm_send_error'));
}
return redirect('/register/confirm');
}
$newUser->email_confirmed = true;
auth()->login($newUser);
session()->flash('success', 'Thanks for signing up! You are now registered and signed in.');
session()->flash('success', trans('auth.register_success'));
return redirect($this->redirectPath());
}
/**
* Show the page to tell the user to check thier email
* Show the page to tell the user to check their email
* and confirm their address.
*/
public function getRegisterConfirmation()
@@ -170,18 +183,6 @@ class AuthController extends Controller
return view('auth/register-confirm');
}
/**
* View the confirmation email as a standard web page.
* @param $token
* @return \Illuminate\View\View
* @throws UserRegistrationException
*/
public function viewConfirmEmail($token)
{
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
return view('emails/email-confirmation', ['token' => $confirmation->token]);
}
/**
* Confirms an email via a token and logs the user into the system.
* @param $token
@@ -194,8 +195,8 @@ class AuthController extends Controller
$user = $confirmation->user;
$user->email_confirmed = true;
$user->save();
auth()->login($confirmation->user);
session()->flash('success', 'Your email has been confirmed!');
auth()->login($user);
session()->flash('success', trans('auth.email_confirm_success'));
$this->emailConfirmationService->deleteConfirmationsByUser($user);
return redirect($this->redirectPath);
}
@@ -221,39 +222,21 @@ class AuthController extends Controller
'email' => 'required|email|exists:users,email'
]);
$user = $this->userRepo->getByEmail($request->get('email'));
try {
$this->emailConfirmationService->sendConfirmation($user);
} catch (Exception $e) {
session()->flash('error', trans('auth.email_confirm_send_error'));
return redirect('/register/confirm');
}
$this->emailConfirmationService->sendConfirmation($user);
\Session::flash('success', 'Confirmation email resent, Please check your inbox.');
session()->flash('success', trans('auth.email_confirm_resent'));
return redirect('/register/confirm');
}
/**
* Show the application login form.
* @return \Illuminate\Http\Response
*/
public function getLogin()
{
if (view()->exists('auth.authenticate')) {
return view('auth.authenticate');
}
$socialDrivers = $this->socialAuthService->getActiveDrivers();
return view('auth.login', ['socialDrivers' => $socialDrivers]);
}
/**
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function getSocialLogin($socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
}
/**
* Redirect to the social site for authentication initended to register.
* Redirect to the social site for authentication intended to register.
* @param $socialDriver
* @return mixed
*/
@@ -280,7 +263,7 @@ class AuthController extends Controller
return $this->socialRegisterCallback($socialDriver);
}
} else {
throw new SocialSignInException('No action defined', '/login');
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
return redirect()->back();
}
@@ -295,4 +278,24 @@ class AuthController extends Controller
return $this->socialAuthService->detachSocialAccount($socialDriver);
}
}
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function socialRegisterCallback($socialDriver)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
// Create an array of the user data to create a new user instance
$userData = [
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
}
}

View File

@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
class PasswordController extends Controller
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
@@ -23,10 +23,27 @@ class PasswordController extends Controller
protected $redirectTo = '/';
/**
* Create a new password controller instance.
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
parent::__construct();
}
}
/**
* Get the response for a successful password reset.
*
* @param string $response
* @return \Illuminate\Http\Response
*/
protected function sendResetResponse($response)
{
$message = trans('auth.reset_password_success');
session()->flash('success', $message);
return redirect($this->redirectPath())
->with('status', trans($response));
}
}

View File

@@ -1,59 +1,55 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Illuminate\Http\Response;
use Views;
class BookController extends Controller
{
protected $bookRepo;
protected $pageRepo;
protected $chapterRepo;
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param BookRepo $bookRepo
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(BookRepo $bookRepo, PageRepo $pageRepo, ChapterRepo $chapterRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->bookRepo = $bookRepo;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Display a listing of the book.
*
* @return Response
*/
public function index()
{
$books = $this->bookRepo->getAllPaginated(10);
$recents = $this->signedIn ? $this->bookRepo->getRecentlyViewed(10, 0) : false;
return view('books/index', ['books' => $books, 'recents' => $recents]);
$books = $this->entityRepo->getAllPaginated('book', 10);
$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]);
}
/**
* Show the form for creating a new book.
*
* @return Response
*/
public function create()
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books/create');
}
@@ -65,66 +61,59 @@ class BookController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('book-create');
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->bookRepo->newFromInput($request->all());
$book->slug = $this->bookRepo->findSuitableSlug($book->name);
$book->created_by = Auth::user()->id;
$book->updated_by = Auth::user()->id;
$book->save();
$book = $this->entityRepo->createFromInput('book', $request->all());
Activity::add($book, 'book_create', $book->id);
return redirect($book->getUrl());
}
/**
* Display the specified book.
*
* @param $slug
* @return Response
*/
public function show($slug)
{
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
$bookChildren = $this->bookRepo->getChildren($book);
$this->setPageTitle($book->getShortName());
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Show the form for editing the specified book.
*
* @param $slug
* @return Response
*/
public function edit($slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
*
* @param Request $request
* @param $slug
* @return Response
*/
public function update(Request $request, $slug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($slug);
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book->fill($request->all());
$book->slug = $this->bookRepo->findSuitableSlug($book->name, $book->id);
$book->updated_by = Auth::user()->id;
$book->save();
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -136,8 +125,9 @@ class BookController extends Controller
*/
public function showDelete($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]);
}
@@ -148,52 +138,68 @@ class BookController extends Controller
*/
public function sort($bookSlug)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = $this->bookRepo->getChildren($book);
$books = $this->bookRepo->getAll();
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
}
/**
* Shows the sort box for a single book.
* Used via AJAX when loading in extra books to a sort.
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function getSortItem($bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$bookChildren = $this->bookRepo->getChildren($book);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Saves an array of sort mapping to pages and chapters.
*
* @param string $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function saveSort($bookSlug, Request $request)
{
$this->checkPermission('book-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->has('sort-tree')) {
return redirect($book->getUrl());
}
$sortedBooks = [];
// Sort pages and chapters
$sortedBooks = [];
$updatedModels = collect();
$sortMap = json_decode($request->get('sort-tree'));
$defaultBookId = $book->id;
foreach ($sortMap as $index => $bookChild) {
$id = $bookChild->id;
// Loop through contents of provided map and update entities accordingly
foreach ($sortMap as $bookChild) {
$priority = $bookChild->sort;
$id = intval($bookChild->id);
$isPage = $bookChild->type == 'page';
$bookId = $this->bookRepo->exists($bookChild->book) ? $bookChild->book : $defaultBookId;
$model = $isPage ? $this->pageRepo->getById($id) : $this->chapterRepo->getById($id);
$isPage ? $this->pageRepo->changeBook($bookId, $model) : $this->chapterRepo->changeBook($bookId, $model);
$model->priority = $index;
if ($isPage) {
$model->chapter_id = ($bookChild->parentChapter === false) ? 0 : $bookChild->parentChapter;
$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);
// 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;
$model->save();
$updatedModels->push($model);
}
$model->save();
// Store involved books to be sorted later
if (!in_array($bookId, $sortedBooks)) {
$sortedBooks[] = $bookId;
}
@@ -201,26 +207,104 @@ class BookController extends Controller
// Add activity for books
foreach ($sortedBooks as $bookId) {
$updatedBook = $this->bookRepo->getById($bookId);
$updatedBook = $this->entityRepo->getById('book', $bookId);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}
/**
* Remove the specified book from storage.
*
* @param $bookSlug
* @return Response
*/
public function destroy($bookSlug)
{
$this->checkPermission('book-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
Activity::removeEntity($book);
$this->bookRepo->destroyBySlug($bookSlug);
$this->entityRepo->destroyBook($book);
return redirect('/books');
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
'book' => $book,
'roles' => $roles
]);
}
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $book);
session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
}
/**
* Export a book as a PDF file.
* @param string $bookSlug
* @return mixed
*/
public function exportPdf($bookSlug)
{
$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'
]);
}
/**
* Export a book as a contained HTML file.
* @param string $bookSlug
* @return mixed
*/
public function exportHtml($bookSlug)
{
$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'
]);
}
/**
* Export a book as a plain text file.
* @param $bookSlug
* @return mixed
*/
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'
]);
}
}

View File

@@ -1,36 +1,34 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use Illuminate\Http\Response;
use Views;
class ChapterController extends Controller
{
protected $bookRepo;
protected $chapterRepo;
protected $userRepo;
protected $entityRepo;
protected $exportService;
/**
* ChapterController constructor.
* @param $bookRepo
* @param $chapterRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Show the form for creating a new chapter.
* @param $bookSlug
@@ -38,8 +36,9 @@ class ChapterController extends Controller
*/
public function create($bookSlug)
{
$this->checkPermission('chapter-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]);
}
@@ -51,18 +50,16 @@ class ChapterController extends Controller
*/
public function store($bookSlug, Request $request)
{
$this->checkPermission('chapter-create');
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->newFromInput($request->all());
$chapter->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id);
$chapter->priority = $this->bookRepo->getNewPriority($book);
$chapter->created_by = auth()->user()->id;
$chapter->updated_by = auth()->user()->id;
$book->chapters()->save($chapter);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$input = $request->all();
$input['priority'] = $this->entityRepo->getNewBookPriority($book);
$chapter = $this->entityRepo->createFromInput('chapter', $input, $book);
Activity::add($chapter, 'chapter_create', $book->id);
return redirect($chapter->getUrl());
}
@@ -75,11 +72,19 @@ class ChapterController extends Controller
*/
public function show($bookSlug, $chapterSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$sidebarTree = $this->bookRepo->getChildren($book);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$sidebarTree = $this->entityRepo->getBookChildren($chapter->book);
Views::add($chapter);
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
$this->setPageTitle($chapter->getShortName());
$pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
]);
}
/**
@@ -90,10 +95,10 @@ class ChapterController extends Controller
*/
public function edit($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
return view('chapters/edit', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -105,14 +110,15 @@ class ChapterController extends Controller
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$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->slug = $this->chapterRepo->findSuitableSlug($chapter->name, $book->id, $chapter->id);
$chapter->updated_by = auth()->user()->id;
$chapter->updated_by = user()->id;
$chapter->save();
Activity::add($chapter, 'chapter_update', $book->id);
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
@@ -124,10 +130,10 @@ class ChapterController extends Controller
*/
public function showDelete($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
return view('chapters/delete', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -138,11 +144,148 @@ class ChapterController extends Controller
*/
public function destroy($bookSlug, $chapterSlug)
{
$this->checkPermission('chapter-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
$this->checkOwnablePermission('chapter-delete', $chapter);
Activity::addMessage('chapter_delete', $book->id, $chapter->name);
$this->chapterRepo->destroy($chapter);
$this->entityRepo->destroyChapter($chapter);
return redirect($book->getUrl());
}
/**
* Show the page for moving a chapter.
* @param $bookSlug
* @param $chapterSlug
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showMove($bookSlug, $chapterSlug) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [
'chapter' => $chapter,
'book' => $chapter->book
]);
}
/**
* Perform the move action for a chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return mixed
* @throws \BookStack\Exceptions\NotFoundException
*/
public function move($bookSlug, $chapterSlug, Request $request) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
return redirect($chapter->getUrl());
}
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
$parent = false;
if ($entityType == 'book') {
$parent = $this->entityRepo->getById('book', $entityId);
}
if ($parent === false || $parent === null) {
session()->flash('error', trans('errors.selected_book_not_found'));
return redirect()->back();
}
$this->entityRepo->changeBook('chapter', $parent->id, $chapter, true);
Activity::add($chapter, 'chapter_move', $chapter->book->id);
session()->flash('success', trans('entities.chapter_move_success', ['bookName' => $parent->name]));
return redirect($chapter->getUrl());
}
/**
* Show the Restrictions view.
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
'chapter' => $chapter,
'roles' => $roles
]);
}
/**
* Set the restrictions for this chapter.
* @param $bookSlug
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $chapter);
session()->flash('success', trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
}
/**
* Exports a chapter to pdf .
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $chapterSlug)
{
$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'
]);
}
/**
* Export a chapter to a self-contained HTML file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $chapterSlug)
{
$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'
]);
}
/**
* Export a chapter to a simple plaintext .txt file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
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'
]);
}
}

View File

@@ -2,13 +2,12 @@
namespace BookStack\Http\Controllers;
use HttpRequestException;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use BookStack\User;
abstract class Controller extends BaseController
@@ -29,37 +28,88 @@ abstract class Controller extends BaseController
*/
public function __construct()
{
// Get a user instance for the current user
$user = auth()->user();
if (!$user) $user = User::getDefault();
$this->middleware(function ($request, $next) {
// Share variables with views
view()->share('signedIn', auth()->check());
view()->share('currentUser', $user);
// Get a user instance for the current user
$user = user();
// Share variables with controllers
$this->currentUser = $user;
$this->signedIn = auth()->check();
// Share variables with controllers
$this->currentUser = $user;
$this->signedIn = auth()->check();
// Share variables with views
view()->share('signedIn', $this->signedIn);
view()->share('currentUser', $user);
return $next($request);
});
}
/**
* Stops the application and shows a permission error if
* the application is in demo mode.
*/
protected function preventAccessForDemoUsers()
{
if (config('app.env') === 'demo') $this->showPermissionError();
}
/**
* Adds the page title into the view.
* @param $title
*/
public function setPageTitle($title)
{
view()->share('pageTitle', $title);
}
/**
* On a permission error redirect to home and display.
* the error as a notification.
*/
protected function showPermissionError()
{
if (request()->wantsJson()) {
$response = response()->json(['error' => trans('errors.permissionJson')], 403);
} else {
$response = redirect('/');
session()->flash('error', trans('errors.permission'));
}
throw new HttpResponseException($response);
}
/**
* Checks for a permission.
*
* @param $permissionName
* @param string $permissionName
* @return bool|\Illuminate\Http\RedirectResponse
*/
protected function checkPermission($permissionName)
{
if (!$this->currentUser || !$this->currentUser->can($permissionName)) {
Session::flash('error', trans('errors.permission'));
throw new HttpResponseException(
redirect('/')
);
if (!user() || !user()->can($permissionName)) {
$this->showPermissionError();
}
return true;
}
/**
* Check the current user's permissions against an ownable item.
* @param $permission
* @param Ownable $ownable
* @return bool
*/
protected function checkOwnablePermission($permission, Ownable $ownable)
{
if (userCan($permission, $ownable)) return true;
return $this->showPermissionError();
}
/**
* Check if a user has a permission or bypass if the callback is true.
* @param $permissionName
* @param $callback
* @return bool
*/
protected function checkPermissionOr($permissionName, $callback)
{
$callbackResult = $callback();
@@ -67,4 +117,33 @@ abstract class Controller extends BaseController
return true;
}
/**
* Send back a json error message.
* @param string $messageText
* @param int $statusCode
* @return mixed
*/
protected function jsonError($messageText = "", $statusCode = 500)
{
return response()->json(['message' => $messageText], $statusCode);
}
/**
* Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return response()->json(['validation' => $errors], 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
}

View File

@@ -1,41 +1,79 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\EntityRepo;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
{
protected $activityService;
protected $bookRepo;
protected $entityRepo;
/**
* HomeController constructor.
* @param BookRepo $bookRepo
* @param EntityRepo $entityRepo
*/
public function __construct(BookRepo $bookRepo)
public function __construct(EntityRepo $entityRepo)
{
$this->bookRepo = $bookRepo;
$this->entityRepo = $entityRepo;
parent::__construct();
}
/**
* Display the homepage.
*
* @return Response
*/
public function index()
{
$activity = Activity::latest();
$recents = $this->signedIn ? Views::getUserRecentlyViewed(10, 0) : $this->bookRepo->getLatest(10);
return view('home', ['activity' => $activity, 'recents' => $recents]);
$activity = Activity::latest(10);
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
return view('home', [
'activity' => $activity,
'recents' => $recents,
'recentlyCreatedPages' => $recentlyCreatedPages,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages
]);
}
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getTranslations() {
$locale = trans()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
} else {
$translations = [
// Get only translations which might be used in JS
'common' => trans('common'),
'components' => trans('components'),
'entities' => trans('entities'),
'errors' => trans('errors')
];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], 'en'),
'components' => trans('components', [], 'en'),
'entities' => trans('entities', [], 'en'),
'errors' => trans('errors', [], 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120);
}
return response($resp, 200, [
'Content-Type' => 'application/javascript'
]);
}
}

View File

@@ -1,12 +1,10 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Intervention\Image\Facades\Image as ImageTool;
use Illuminate\Support\Facades\DB;
use BookStack\Image;
use BookStack\Repos\PageRepo;
@@ -14,188 +12,166 @@ class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file)
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Get all images, Paginated
* Get all images for a specific type, Paginated
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAll($page = 0)
public function getAllByType($type, $page = 0)
{
$pageSize = 30;
$images = $this->image->orderBy('created_at', 'desc')
->skip($page * $pageSize)->take($pageSize)->get();
foreach ($images as $image) {
$this->loadSizes($image);
}
$hasMore = $this->image->orderBy('created_at', 'desc')
->skip(($page + 1) * $pageSize)->take($pageSize)->count() > 0;
return response()->json([
'images' => $images,
'hasMore' => $hasMore
$imgData = $this->imageRepo->getPaginatedByType($type, $page);
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType($type, $page = 0, Request $request)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
return response()->json($imgData);
}
/**
* Loads the standard thumbnail sizes for an image.
* @param Image $image
* Get all images for a user.
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
private function loadSizes(Image $image)
public function getAllForUserType($page = 0)
{
$image->thumbnail = $this->getThumbnail($image, 150, 150);
$image->display = $this->getThumbnail($image, 840, 0, true);
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
return response()->json($imgData);
}
/**
* Get the thumbnail for an image.
* If $keepRatio is true only the width will be used.
* @param $image
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* Get gallery images with a specific filter such as book or page
* @param $filter
* @param int $page
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getThumbnail($image, $width = 220, $height = 220, $keepRatio = false)
public function getGalleryFiltered($filter, $page = 0, Request $request)
{
$explodedPath = explode('/', $image->url);
$dirPrefix = $keepRatio ? 'scaled-' : 'thumbs-';
array_splice($explodedPath, 4, 0, [$dirPrefix . $width . '-' . $height]);
$thumbPath = implode('/', $explodedPath);
$thumbFilePath = public_path() . $thumbPath;
$this->validate($request, [
'page_id' => 'required|integer'
]);
// Return the thumbnail url path if already exists
if (file_exists($thumbFilePath)) {
return $thumbPath;
}
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
// Otherwise create the thumbnail
$thumb = ImageTool::make(public_path() . $image->url);
if($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$thumb->fit($width, $height);
}
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
// Create thumbnail folder if it does not exist
if (!file_exists(dirname($thumbFilePath))) {
mkdir(dirname($thumbFilePath), 0775, true);
}
//Save Thumbnail
$thumb->save($thumbFilePath);
return $thumbPath;
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function upload(Request $request)
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create');
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'is_image'
]);
$imageUpload = $request->file('file');
$name = str_replace(' ', '-', $imageUpload->getClientOriginalName());
$storageName = substr(sha1(time()), 0, 10) . '-' . $name;
$imagePath = '/uploads/images/' . Date('Y-m-M') . '/';
$storagePath = public_path() . $imagePath;
$fullPath = $storagePath . $storageName;
while (file_exists($fullPath)) {
$storageName = substr(sha1(rand()), 0, 3) . $storageName;
$fullPath = $storagePath . $storageName;
try {
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
$imageUpload->move($storagePath, $storageName);
// Create and save image object
$this->image->name = $name;
$this->image->url = $imagePath . $storageName;
$this->image->created_by = auth()->user()->id;
$this->image->updated_by = auth()->user()->id;
$this->image->save();
$this->loadSizes($this->image);
return response()->json($this->image);
return response()->json($image);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
* @param $width
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
}
/**
* Update image details
* @param $imageId
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function update($imageId, Request $request)
{
$this->checkPermission('image-update');
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->image->findOrFail($imageId);
$image->fill($request->all());
$image->save();
$this->loadSizes($image);
return response()->json($this->image);
$image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Deletes an image and all thumbnail/image files
* @param PageRepo $pageRepo
* @param Request $request
* @param int $id
* @param EntityRepo $entityRepo
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(PageRepo $pageRepo, Request $request, $id)
public function destroy(EntityRepo $entityRepo, Request $request, $id)
{
$this->checkPermission('image-delete');
$image = $this->image->findOrFail($id);
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$pageSearch = $pageRepo->searchForImage($image->url);
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
if ($pageSearch !== false && !$isForced) {
return response()->json($pageSearch, 400);
}
// Delete files
$folder = public_path() . dirname($image->url);
$fileName = basename($image->url);
// Delete thumbnails
foreach (glob($folder . '/*') as $file) {
if (is_dir($file)) {
$thumbName = $file . '/' . $fileName;
if (file_exists($file)) {
unlink($thumbName);
}
// Remove thumb folder if empty
if (count(glob($file . '/*')) === 0) {
rmdir($file);
}
if (!$isForced) {
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {
return response()->json($pageSearch, 400);
}
}
// Delete file and database entry
unlink($folder . '/' . $fileName);
$image->delete();
// Delete parent folder if empty
if (count(glob($folder . '/*')) === 0) {
rmdir($folder);
}
return response()->json('Image Deleted');
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}

View File

@@ -1,76 +1,139 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
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\Support\Facades\Auth;
use BookStack\Http\Requests;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
use Illuminate\Http\Response;
use Views;
use GatherContent\Htmldiff\Htmldiff;
class PageController extends Controller
{
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $entityRepo;
protected $exportService;
protected $userRepo;
/**
* PageController constructor.
* @param PageRepo $pageRepo
* @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo
* @param EntityRepo $entityRepo
* @param ExportService $exportService
* @param UserRepo $userRepo
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->exportService = $exportService;
$this->userRepo = $userRepo;
parent::__construct();
}
/**
* Show the form for creating a new page.
*
* @param $bookSlug
* @param bool $chapterSlug
* @param string $bookSlug
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
*/
public function create($bookSlug, $chapterSlug = false)
public function create($bookSlug, $chapterSlug = null)
{
$this->checkPermission('page-create');
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $chapterSlug ? $this->chapterRepo->getBySlug($chapterSlug, $book->id) : false;
return view('pages/create', ['book' => $book, 'chapter' => $chapter]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
// Redirect to draft edit screen if signed in
if ($this->signedIn) {
$draft = $this->entityRepo->getDraftPage($book, $chapter);
return redirect($draft->getUrl());
}
// Otherwise show edit view
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
/**
* Store a newly created page in storage.
*
* Create a new page as a guest user.
* @param Request $request
* @param string $bookSlug
* @param string|null $chapterSlug
* @return mixed
* @throws NotFoundException
*/
public function createAsGuest(Request $request, $bookSlug, $chapterSlug = null)
{
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
$page = $this->entityRepo->getDraftPage($book, $chapter);
$this->entityRepo->publishPageDraft($page, [
'name' => $request->get('name'),
'html' => ''
]);
return redirect($page->getUrl('/edit'));
}
/**
* Show form to continue editing a draft page.
* @param string $bookSlug
* @param int $pageId
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function editDraft($bookSlug, $pageId)
{
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
'draftsEnabled' => $draftsEnabled
]);
}
/**
* Store a new page by changing a draft into a page.
* @param Request $request
* @param $bookSlug
* @param string $bookSlug
* @param int $pageId
* @return Response
*/
public function store(Request $request, $bookSlug)
public function store(Request $request, $bookSlug, $pageId)
{
$this->checkPermission('page-create');
$this->validate($request, [
'name' => 'required|string|max:255',
'html' => 'required|string',
'parent' => 'integer|exists:pages,id'
'name' => 'required|string|max:255'
]);
$input = $request->all();
$book = $this->bookRepo->getBySlug($bookSlug);
$chapterId = ($request->has('chapter') && $this->chapterRepo->idExists($request->get('chapter'))) ? $request->get('chapter') : null;
$input['priority'] = $this->bookRepo->getNewPriority($book);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$page = $this->pageRepo->saveNew($input, $book, $chapterId);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
} else {
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
}
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
Activity::add($page, 'page_create', $book->id);
return redirect($page->getUrl());
@@ -78,140 +141,453 @@ class PageController extends Controller
/**
* Display the specified page.
*
* @param $bookSlug
* @param $pageSlug
* If the page is not found via the slug the revisions are searched for a match.
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function show($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$sidebarTree = $this->bookRepo->getChildren($book);
try {
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
} catch (NotFoundException $e) {
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
if ($page === null) abort(404);
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
Views::add($page);
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
}
/**
* Get page from an ajax request.
* @param int $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function getPageAjax($pageId)
{
$page = $this->entityRepo->getById('page', $pageId);
return response()->json($page);
}
/**
* Show the form for editing the specified page.
*
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function edit($bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
return view('pages/edit', ['page' => $page, 'book' => $book, 'current' => $page]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
$page->isDraft = false;
// Check for active editing
$warnings = [];
if ($this->entityRepo->isPageEditingActive($page, 60)) {
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
}
// Check for a current draft version for this user
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
$page->name = $draft->name;
$page->html = $draft->html;
$page->markdown = $draft->markdown;
$page->isDraft = true;
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
}
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
'draftsEnabled' => $draftsEnabled
]);
}
/**
* Update the specified page in storage.
*
* @param Request $request
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
*/
public function update(Request $request, $bookSlug, $pageSlug)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$this->pageRepo->updatePage($page, $book->id, $request->all());
Activity::add($page, 'page_update', $book->id);
$this->validate($request, [
'name' => 'required|string|max:255'
]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
Activity::add($page, 'page_update', $page->book->id);
return redirect($page->getUrl());
}
/**
* Save a draft update as a revision.
* @param Request $request
* @param int $pageId
* @return \Illuminate\Http\JsonResponse
*/
public function saveDraft(Request $request, $pageId)
{
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
if (!$this->signedIn) {
return response()->json([
'status' => 'error',
'message' => trans('errors.guests_cannot_save_drafts'),
], 500);
}
$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
]);
}
/**
* Redirect from a special link url which
* uses the page id rather than the name.
* @param $pageId
* @param int $pageId
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function redirectFromLink($pageId)
{
$page = $this->pageRepo->getById($pageId);
$page = $this->entityRepo->getById('page', $pageId);
return redirect($page->getUrl());
}
/**
* Show the deletion page for the specified page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
*/
public function showDelete($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
return view('pages/delete', ['book' => $book, 'page' => $page, 'current' => $page]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
* Show the deletion page for the specified page.
* @param string $bookSlug
* @param int $pageId
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showDeleteDraft($bookSlug, $pageId)
{
$page = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
* Remove the specified page from storage.
*
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return Response
* @internal param int $id
*/
public function destroy($bookSlug, $pageSlug)
{
$this->checkPermission('page-delete');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$book = $page->book;
$this->checkOwnablePermission('page-delete', $page);
Activity::addMessage('page_delete', $book->id, $page->name);
$this->pageRepo->destroy($page);
session()->flash('success', trans('entities.pages_delete_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
/**
* Remove the specified draft page from storage.
* @param string $bookSlug
* @param int $pageId
* @return Response
* @throws NotFoundException
*/
public function destroyDraft($bookSlug, $pageId)
{
$page = $this->entityRepo->getById('page', $pageId, true);
$book = $page->book;
$this->checkOwnablePermission('page-update', $page);
session()->flash('success', trans('entities.pages_delete_draft_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
/**
* Shows the last revisions for this page.
* @param $bookSlug
* @param $pageSlug
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
*/
public function showRevisions($bookSlug, $pageSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
return view('pages/revisions', ['page' => $page, 'book' => $book, 'current' => $page]);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
* Shows a preview of a single revision
* @param $bookSlug
* @param $pageSlug
* @param $revisionId
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\View\View
*/
public function showRevision($bookSlug, $pageSlug, $revisionId)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$revision = $this->pageRepo->getRevisionById($revisionId);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$page->fill($revision->toArray());
return view('pages/revision', ['page' => $page, 'book' => $book]);
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
]);
}
/**
* Shows the changes of a single revision
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\View\View
*/
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$prev = $revision->getPrevious();
$prevContent = ($prev === null) ? '' : $prev->html;
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
]);
}
/**
* Restores a page using the content of the specified revision.
* @param $bookSlug
* @param $pageSlug
* @param $revisionId
* @param string $bookSlug
* @param string $pageSlug
* @param int $revisionId
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
{
$this->checkPermission('page-update');
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$page = $this->pageRepo->restoreRevision($page, $book, $revisionId);
Activity::add($page, 'page_restore', $book->id);
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
Activity::add($page, 'page_restore', $page->book->id);
return redirect($page->getUrl());
}
/**
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
}
/**
* Export a page to a self-contained HTML file.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
}
/**
* Export a page to a simple plaintext .txt file.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Http\Response
*/
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'
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => trans('entities.recently_created_pages'),
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Show the view to choose a new parent to move a page into.
* @param string $bookSlug
* @param string $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showMove($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
return view('pages/move', [
'book' => $page->book,
'page' => $page
]);
}
/**
* Does the action of moving the location of a page
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function move($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 === '') {
return redirect($page->getUrl());
}
$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->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
/**
* Set the permissions for this page.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
session()->flash('success', trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
}
}

View File

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

View File

@@ -1,32 +1,23 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
use BookStack\Http\Requests;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\BookRepo;
use BookStack\Repos\ChapterRepo;
use BookStack\Repos\PageRepo;
class SearchController extends Controller
{
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
protected $entityRepo;
protected $viewService;
/**
* SearchController constructor.
* @param $pageRepo
* @param $bookRepo
* @param $chapterRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
*/
public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
parent::__construct();
}
@@ -42,10 +33,77 @@ class SearchController extends Controller
return redirect()->back();
}
$searchTerm = $request->get('term');
$pages = $this->pageRepo->getBySearch($searchTerm);
$books = $this->bookRepo->getBySearch($searchTerm);
$chapters = $this->chapterRepo->getBySearch($searchTerm);
return view('search/all', ['pages' => $pages, 'books' => $books, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $pages,
'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the books in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchBooks(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $books,
'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm
]);
}
/**
@@ -61,10 +119,43 @@ class SearchController extends Controller
return redirect()->back();
}
$searchTerm = $request->get('term');
$whereTerm = [['book_id', '=', $bookId]];
$pages = $this->pageRepo->getBySearch($searchTerm, $whereTerm);
$chapters = $this->chapterRepo->getBySearch($searchTerm, $whereTerm);
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
* @param Request $request
* @return mixed
*/
public function searchEntitiesAjax(Request $request)
{
$entities = collect();
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains($entityType)) {
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
}
}

View File

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

View File

@@ -0,0 +1,58 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\TagRepo;
use Illuminate\Http\Request;
class TagController extends Controller
{
protected $tagRepo;
/**
* TagController constructor.
* @param $tagRepo
*/
public function __construct(TagRepo $tagRepo)
{
$this->tagRepo = $tagRepo;
parent::__construct();
}
/**
* Get all the Tags for a particular entity
* @param $entityType
* @param $entityId
* @return \Illuminate\Http\JsonResponse
*/
public function getForEntity($entityType, $entityId)
{
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
return response()->json($tags);
}
/**
* Get tag name suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getNameSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
return response()->json($suggestions);
}
/**
* Get tag value suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getValueSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$tagName = $request->has('name') ? $request->get('name') : false;
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}
}

View File

@@ -1,11 +1,8 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use BookStack\Http\Requests;
use Illuminate\Http\Response;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use BookStack\User;
@@ -18,7 +15,8 @@ class UserController extends Controller
/**
* UserController constructor.
* @param $user
* @param User $user
* @param UserRepo $userRepo
*/
public function __construct(User $user, UserRepo $userRepo)
{
@@ -29,138 +27,226 @@ class UserController extends Controller
/**
* Display a listing of the users.
*
* @param Request $request
* @return Response
*/
public function index()
public function index(Request $request)
{
$users = $this->user->all();
return view('users/index', ['users' => $users]);
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->has('order') ? $request->get('order') : 'asc',
'search' => $request->has('search') ? $request->get('search') : '',
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
}
/**
* Show the form for creating a new user.
*
* @return Response
*/
public function create()
{
$this->checkPermission('user-create');
return view('users/create');
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Store a newly created user in storage.
*
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$this->checkPermission('user-create');
$this->validate($request, [
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
'email' => 'required|email',
'password' => 'required|min:5',
'password-confirm' => 'required|same:password',
'role' => 'required|exists:roles,id'
]);
'email' => 'required|email|unique:users,email'
];
$authMethod = config('auth.method');
if ($authMethod === 'standard') {
$validationRules['password'] = 'required|min:5';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
$validationRules['external_auth_id'] = 'required';
}
$this->validate($request, $validationRules);
$user = $this->user->fill($request->all());
$user->password = bcrypt($request->get('password'));
if ($authMethod === 'standard') {
$user->password = bcrypt($request->get('password'));
} elseif ($authMethod === 'ldap') {
$user->external_auth_id = $request->get('external_auth_id');
}
$user->save();
$user->attachRoleId($request->get('role'));
return redirect('/users');
}
if ($request->has('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');
}
}
return redirect('/settings/users');
}
/**
* Show the form for editing the specified user.
*
* @param int $id
* @param SocialAuthService $socialAuthService
* @return Response
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOr('user-update', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->user->findOrFail($id);
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers]);
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Update the specified user in storage.
*
* @param Request $request
* @param int $id
* @return Response
*/
public function update(Request $request, $id)
{
$this->checkPermissionOr('user-update', function () use ($id) {
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $id,
'password' => 'min:5',
'password-confirm' => 'same:password',
'role' => 'exists:roles,id'
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
]);
$user = $this->user->findOrFail($id);
$user->fill($request->except('password'));
$user->fill($request->all());
if ($this->currentUser->can('user-update') && $request->has('role')) {
$user->attachRoleId($request->get('role'));
// Role updates
if (userCan('users-manage') && $request->has('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Password updates
if ($request->has('password') && $request->get('password') != '') {
$password = $request->get('password');
$user->password = bcrypt($password);
}
// External auth id updates
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
if ($request->has('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}
}
$user->save();
return redirect('/users');
session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
return redirect($redirectUrl);
}
/**
* Show the user delete page.
* @param $id
* @param int $id
* @return \Illuminate\View\View
*/
public function delete($id)
{
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->user->findOrFail($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
}
/**
* Remove the specified user from storage.
*
* @param int $id
* @return Response
*/
public function destroy($id)
{
$this->checkPermissionOr('user-delete', function () use ($id) {
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$user = $this->userRepo->getById($id);
// Delete social accounts
if($this->userRepo->isOnlyAdmin($user)) {
session()->flash('error', 'You cannot delete the only admin');
if ($this->userRepo->isOnlyAdmin($user)) {
session()->flash('error', trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
$user->socialAccounts()->delete();
$user->delete();
return redirect('/users');
if ($user->system_name === 'public') {
session()->flash('error', trans('errors.users_cannot_delete_guest'));
return redirect($user->getEditUrl());
}
$this->userRepo->destroy($user);
session()->flash('success', trans('settings.users_delete_success'));
return redirect('/settings/users');
}
/**
* Show the user profile page
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
'assetCounts' => $assetCounts
]);
}
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Http;
<?php namespace BookStack\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -9,15 +7,33 @@ class Kernel extends HttpKernel
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\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,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
@@ -26,6 +42,7 @@ 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,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,

View File

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

View File

@@ -0,0 +1,23 @@
<?php namespace BookStack\Http\Middleware;
use Carbon\Carbon;
use Closure;
class Localization
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
$locale = setting()->getUser(user(), 'language', $defaultLang);
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Http\Middleware;
<?php namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;
@@ -34,7 +32,8 @@ class RedirectIfAuthenticated
*/
public function handle($request, Closure $next)
{
if ($this->auth->check()) {
$requireConfirmation = setting('registration-confirmation');
if ($this->auth->check() && (!$requireConfirmation || ($requireConfirmation && $this->auth->user()->email_confirmed))) {
return redirect('/');
}

View File

@@ -1,106 +0,0 @@
<?php
// Authenticated routes...
Route::group(['middleware' => 'auth'], function () {
Route::group(['prefix' => 'books'], function () {
// Books
Route::get('/', 'BookController@index');
Route::get('/create', 'BookController@create');
Route::post('/', 'BookController@store');
Route::get('/{slug}/edit', 'BookController@edit');
Route::put('/{slug}', 'BookController@update');
Route::delete('/{id}', 'BookController@destroy');
Route::get('/{slug}/sort-item', 'BookController@getSortItem');
Route::get('/{slug}', 'BookController@show');
Route::get('/{slug}/delete', 'BookController@showDelete');
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
//Revisions
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
// Chapters
Route::get('/{bookSlug}/chapter/{chapterSlug}/create-page', 'PageController@create');
Route::get('/{bookSlug}/chapter/create', 'ChapterController@create');
Route::post('/{bookSlug}/chapter/create', 'ChapterController@store');
Route::get('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@show');
Route::put('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@update');
Route::get('/{bookSlug}/chapter/{chapterSlug}/edit', 'ChapterController@edit');
Route::get('/{bookSlug}/chapter/{chapterSlug}/delete', 'ChapterController@showDelete');
Route::delete('/{bookSlug}/chapter/{chapterSlug}', 'ChapterController@destroy');
});
// Uploads
Route::post('/upload/image', 'ImageController@upload');
// Users
Route::get('/users', 'UserController@index');
Route::get('/users/create', 'UserController@create');
Route::get('/users/{id}/delete', 'UserController@delete');
Route::post('/users/create', 'UserController@store');
Route::get('/users/{id}', 'UserController@edit');
Route::put('/users/{id}', 'UserController@update');
Route::delete('/users/{id}', 'UserController@destroy');
// Image routes
Route::get('/images/all', 'ImageController@getAll');
Route::put('/images/update/{imageId}', 'ImageController@update');
Route::delete('/images/{imageId}', 'ImageController@destroy');
Route::get('/images/all/{page}', 'ImageController@getAll');
// Links
Route::get('/link/{id}', 'PageController@redirectFromLink');
// Search
Route::get('/search/all', 'SearchController@searchAll');
Route::get('/search/book/{bookId}', 'SearchController@searchBook');
// Other Pages
Route::get('/', 'HomeController@index');
Route::get('/home', 'HomeController@index');
// Settings
Route::get('/settings', 'SettingController@index');
Route::post('/settings', 'SettingController@update');
});
// Login using social authentication
Route::get('/login/service/{socialDriver}', 'Auth\AuthController@getSocialLogin');
Route::get('/login/service/{socialDriver}/callback', 'Auth\AuthController@socialCallback');
Route::get('/login/service/{socialDriver}/detach', 'Auth\AuthController@detachSocialAccount');
// Login/Logout routes
Route::get('/login', 'Auth\AuthController@getLogin');
Route::post('/login', 'Auth\AuthController@postLogin');
Route::get('/logout', 'Auth\AuthController@getLogout');
Route::get('/register', 'Auth\AuthController@getRegister');
Route::get('/register/confirm', 'Auth\AuthController@getRegisterConfirmation');
Route::get('/register/confirm/awaiting', 'Auth\AuthController@showAwaitingConfirmation');
Route::post('/register/confirm/resend', 'Auth\AuthController@resendConfirmation');
Route::get('/register/confirm/{token}', 'Auth\AuthController@confirmEmail');
Route::get('/register/confirm/{token}/email', 'Auth\AuthController@viewConfirmEmail');
Route::get('/register/service/{socialDriver}', 'Auth\AuthController@socialRegister');
Route::post('/register', 'Auth\AuthController@postRegister');
// Password reset link request routes...
Route::get('/password/email', 'Auth\PasswordController@getEmail');
Route::post('/password/email', 'Auth\PasswordController@postEmail');
// Password reset routes...
Route::get('/password/reset/{token}', 'Auth\PasswordController@getReset');
Route::post('/password/reset', 'Auth\PasswordController@postReset');

View File

@@ -1,24 +1,21 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Images;
class Image extends Entity
class Image extends Ownable
{
protected $fillable = ['name'];
public function getFilePath()
{
return storage_path() . $this->url;
}
/**
* Get the url for this item.
* Get a thumbnail for this image.
* @param int $width
* @param int $height
* @param bool|false $keepRatio
* @return string
*/
public function getUrl()
public function getThumb($width, $height, $keepRatio = false)
{
return public_path() . $this->url;
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
}

View File

@@ -1,21 +0,0 @@
<?php
namespace BookStack\Jobs;
use Illuminate\Bus\Queueable;
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use Queueable;
}

24
app/JointPermission.php Normal file
View File

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

View File

19
app/Model.php Normal file
View File

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

View File

@@ -0,0 +1,49 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification
{
public $token;
/**
* Create a new notification instance.
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$appName = ['appName' => setting('app-name')];
return (new MailMessage)
->subject(trans('auth.email_confirm_subject', $appName))
->greeting(trans('auth.email_confirm_greeting', $appName))
->line(trans('auth.email_confirm_text'))
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace BookStack\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class ResetPassword extends Notification
{
/**
* The password reset token.
*
* @var string
*/
public $token;
/**
* Create a notification instance.
*
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's channels.
*
* @param mixed $notifiable
* @return array|string
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail()
{
return (new MailMessage)
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
->line(trans('auth.email_reset_text'))
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
->line(trans('auth.email_reset_not_requested'));
}
}

33
app/Ownable.php Normal file
View File

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

View File

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

View File

@@ -1,26 +1,50 @@
<?php
<?php namespace BookStack;
namespace BookStack;
use Illuminate\Database\Eloquent\Model;
class PageRevision extends Model
{
protected $fillable = ['name', 'html', 'text'];
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
/**
* Get the user that created the page revision
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function createdBy()
{
return $this->belongsTo('BookStack\User', 'created_by');
return $this->belongsTo(User::class, 'created_by');
}
/**
* Get the page this revision originates from.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
{
return $this->belongsTo('BookStack\Page');
return $this->belongsTo(Page::class);
}
public function getUrl()
/**
* Get the url for this revision.
* @param null|string $path
* @return string
*/
public function getUrl($path = null)
{
return $this->page->getUrl() . '/revisions/' . $this->id;
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) return $url . '/' . trim($path, '/');
return $url;
}
/**
* Get the previous revision for the same page if existing
* @return \BookStack\PageRevision|null
*/
public function getPrevious()
{
if ($id = static::where('page_id', '=', $this->page_id)->where('id', '<', $this->id)->max('id')) {
return static::find($id);
}
return null;
}
}

View File

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

View File

@@ -1,10 +1,9 @@
<?php
<?php namespace BookStack\Providers;
namespace BookStack\Providers;
use Illuminate\Support\Facades\Auth;
use BookStack\Services\SettingService;
use BookStack\Setting;
use Illuminate\Support\ServiceProvider;
use BookStack\User;
use Validator;
class AppServiceProvider extends ServiceProvider
{
@@ -15,7 +14,15 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
//
// Custom validation methods
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
\Blade::directive('icon', function($expression) {
return "<?php echo icon($expression); ?>";
});
}
/**
@@ -25,6 +32,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(SettingService::class, function($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace BookStack\Providers;
use Auth;
use BookStack\Services\LdapService;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
Auth::provider('ldap', function($app, array $config) {
return new LdapUserProvider($config['model'], $app[LdapService::class]);
});
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Broadcast::routes();
//
// /*
// * Authenticate the user's personal channel...
// */
// Broadcast::channel('BookStack.User.*', function ($user, $userId) {
// return (int) $user->id === (int) $userId;
// });
}
}

View File

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

View File

@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class EventServiceProvider extends ServiceProvider
{
@@ -13,21 +14,18 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'BookStack\Events\SomeEvent' => [
'BookStack\Listeners\EventListener',
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
],
];
/**
* Register any other events for your application.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @return void
*/
public function boot(DispatcherContract $events)
public function boot()
{
parent::boot($events);
//
parent::boot();
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace BookStack\Providers;
use BookStack\Role;
use BookStack\Services\LdapService;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
class LdapUserProvider implements UserProvider
{
/**
* The user model.
*
* @var string
*/
protected $model;
/**
* @var LdapService
*/
protected $ldapService;
/**
* LdapUserProvider constructor.
* @param $model
* @param LdapService $ldapService
*/
public function __construct($model, LdapService $ldapService)
{
$this->model = $model;
$this->ldapService = $ldapService;
}
/**
* Create a new instance of the model.
*
* @return \Illuminate\Database\Eloquent\Model
*/
public function createModel()
{
$class = '\\' . ltrim($this->model, '\\');
return new $class;
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
return $this->createModel()->newQuery()->find($identifier);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
return $model->newQuery()
->where($model->getAuthIdentifierName(), $identifier)
->where($model->getRememberTokenName(), $token)
->first();
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
if ($user->exists) {
$user->setRememberToken($token);
$user->save();
}
}
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get user via LDAP
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
if ($userDetails === null) return null;
// Search current user base by looking up a uid
$model = $this->createModel();
$currentUser = $model->newQuery()
->where('external_auth_id', $userDetails['uid'])
->first();
if ($currentUser !== null) return $currentUser;
$model->name = $userDetails['name'];
$model->external_auth_id = $userDetails['uid'];
$model->email = $userDetails['email'];
$model->email_confirmed = false;
return $model;
}
/**
* Validate a user against the given credentials.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
return $this->ldapService->validateUserCredentials($user, $credentials['username'], $credentials['password']);
}
}

View File

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

View File

@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Route;
class RouteServiceProvider extends ServiceProvider
{
@@ -19,26 +20,54 @@ class RouteServiceProvider extends ServiceProvider
/**
* Define your route model bindings, pattern filters, etc.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function boot(Router $router)
public function boot()
{
//
parent::boot($router);
parent::boot();
}
/**
* Define the routes for the application.
*
* @param \Illuminate\Routing\Router $router
* @return void
*/
public function map(Router $router)
public function map()
{
$router->group(['namespace' => $this->namespace], function ($router) {
require app_path('Http/routes.php');
$this->mapWebRoutes();
// $this->mapApiRoutes();
}
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* @return void
*/
protected function mapWebRoutes()
{
Route::group([
'middleware' => 'web',
'namespace' => $this->namespace,
], function ($router) {
require base_path('routes/web.php');
});
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapApiRoutes()
{
Route::group([
'middleware' => 'api',
'namespace' => $this->namespace,
'prefix' => 'api',
], function ($router) {
require base_path('routes/api.php');
});
}
}

View File

@@ -1,36 +0,0 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
class SocialiteServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
return new SocialiteManager($app);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['Laravel\Socialite\Contracts\Factory'];
}
}

View File

@@ -1,223 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use Illuminate\Support\Str;
use BookStack\Book;
use Views;
class BookRepo
{
protected $book;
protected $pageRepo;
protected $chapterRepo;
/**
* BookRepo constructor.
* @param Book $book
* @param PageRepo $pageRepo
* @param ChapterRepo $chapterRepo
*/
public function __construct(Book $book, PageRepo $pageRepo, ChapterRepo $chapterRepo)
{
$this->book = $book;
$this->pageRepo = $pageRepo;
$this->chapterRepo = $chapterRepo;
}
/**
* Get the book that has the given id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->book->findOrFail($id);
}
/**
* Get all books, Limited by count.
* @param int $count
* @return mixed
*/
public function getAll($count = 10)
{
return $this->book->orderBy('name', 'asc')->take($count)->get();
}
/**
* Get all books paginated.
* @param int $count
* @return mixed
*/
public function getAllPaginated($count = 10)
{
return $this->book->orderBy('name', 'asc')->paginate($count);
}
/**
* Get the latest books.
* @param int $count
* @return mixed
*/
public function getLatest($count = 10)
{
return $this->book->orderBy('created_at', 'desc')->take($count)->get();
}
/**
* Gets the most recently viewed for a user.
* @param int $count
* @param int $page
* @return mixed
*/
public function getRecentlyViewed($count = 10, $page = 0)
{
return Views::getUserRecentlyViewed($count, $page, $this->book);
}
/**
* Get a book by slug
* @param $slug
* @return mixed
*/
public function getBySlug($slug)
{
return $this->book->where('slug', '=', $slug)->first();
}
/**
* Checks if a book exists.
* @param $id
* @return bool
*/
public function exists($id)
{
return $this->book->where('id', '=', $id)->exists();
}
/**
* Get a new book instance from request input.
* @param $input
* @return Book
*/
public function newFromInput($input)
{
return $this->book->fill($input);
}
/**
* Count the amount of books that have a specific slug.
* @param $slug
* @return mixed
*/
public function countBySlug($slug)
{
return $this->book->where('slug', '=', $slug)->count();
}
/**
* Destroy a book identified by the given slug.
* @param $bookSlug
*/
public function destroyBySlug($bookSlug)
{
$book = $this->getBySlug($bookSlug);
foreach ($book->pages as $page) {
$this->pageRepo->destroy($page);
}
foreach ($book->chapters as $chapter) {
$this->chapterRepo->destroy($chapter);
}
$book->views()->delete();
$book->delete();
}
/**
* Get the next child element priority.
* @param Book $book
* @return int
*/
public function getNewPriority($book)
{
$lastElem = $this->getChildren($book)->pop();
return $lastElem ? $lastElem->priority + 1 : 0;
}
/**
* @param string $slug
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $currentId = false)
{
$query = $this->book->where('slug', '=', $slug);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Provides a suitable slug for the given book name.
* Ensures the returned slug is unique in the system.
* @param string $name
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $currentId = false)
{
$originalSlug = Str::slug($name);
$slug = $originalSlug;
$count = 2;
while ($this->doesSlugExist($slug, $currentId)) {
$slug = $originalSlug . '-' . $count;
$count++;
}
return $slug;
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @return mixed
*/
public function getChildren(Book $book)
{
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
$chapters = $book->chapters()->with('pages')->get();
$children = $pages->merge($chapters);
$bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
}
});
return $children->sortBy('priority');
}
/**
* Get books by search term.
* @param $term
* @return mixed
*/
public function getBySearch($term)
{
$terms = explode(' ', preg_quote(trim($term)));
$books = $this->book->fullTextSearch(['name', 'description'], $terms);
$words = join('|', $terms);
foreach ($books as $book) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $book->getExcerpt(100));
$book->searchSnippet = $result;
}
return $books;
}
}

View File

@@ -1,159 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use Illuminate\Support\Str;
use BookStack\Chapter;
class ChapterRepo
{
protected $chapter;
/**
* ChapterRepo constructor.
* @param $chapter
*/
public function __construct(Chapter $chapter)
{
$this->chapter = $chapter;
}
/**
* Check if an id exists.
* @param $id
* @return bool
*/
public function idExists($id)
{
return $this->chapter->where('id', '=', $id)->count() > 0;
}
/**
* Get a chapter by a specific id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->chapter->findOrFail($id);
}
/**
* Get all chapters.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->chapter->all();
}
/**
* Get a chapter that has the given slug within the given book.
* @param $slug
* @param $bookId
* @return mixed
*/
public function getBySlug($slug, $bookId)
{
return $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
}
/**
* Create a new chapter from request input.
* @param $input
* @return $this
*/
public function newFromInput($input)
{
return $this->chapter->fill($input);
}
/**
* Destroy a chapter and its relations by providing its slug.
* @param Chapter $chapter
*/
public function destroy(Chapter $chapter)
{
if (count($chapter->pages) > 0) {
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->save();
}
}
Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->delete();
}
/**
* Check if a chapter's slug exists.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->chapter->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) {
$query = $query->where('id', '!=', $currentId);
}
return $query->count() > 0;
}
/**
* Finds a suitable slug for the provided name.
* Checks database to prevent duplicate slugs.
* @param $name
* @param $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Get chapters by the given search term.
* @param $term
* @param array $whereTerms
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
{
$terms = explode(' ', preg_quote(trim($term)));
$chapters = $this->chapter->fullTextSearch(['name', 'description'], $terms, $whereTerms);
$words = join('|', $terms);
foreach ($chapters as $chapter) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $chapter->getExcerpt(100));
$chapter->searchSnippet = $result;
}
return $chapters;
}
/**
* Changes the book relation of this chapter.
* @param $bookId
* @param Chapter $chapter
* @return Chapter
*/
public function changeBook($bookId, Chapter $chapter)
{
$chapter->book_id = $bookId;
foreach ($chapter->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$chapter->slug = $this->findSuitableSlug($chapter->name, $bookId, $chapter->id);
$chapter->save();
return $chapter;
}
}

1215
app/Repos/EntityRepo.php Normal file

File diff suppressed because it is too large Load Diff

204
app/Repos/ImageRepo.php Normal file
View File

@@ -0,0 +1,204 @@
<?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
{
protected $image;
protected $imageService;
protected $restrictionService;
protected $page;
/**
* ImageRepo constructor.
* @param Image $image
* @param ImageService $imageService
* @param PermissionService $permissionService
* @param Page $page
*/
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
{
$this->image = $image;
$this->imageService = $imageService;
$this->restrictionService = $permissionService;
$this->page = $page;
}
/**
* Get an image with the given id.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->image->findOrFail($id);
}
/**
* Execute a paginated query, returning in a standard format.
* Also runs the query through the restriction system.
* @param $query
* @param int $page
* @param int $pageSize
* @return array
*/
private function returnPaginated($query, $page = 0, $pageSize = 24)
{
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
$returnImages = $images->take(24);
$returnImages->each(function ($image) {
$this->loadThumbs($image);
});
return [
'images' => $returnImages,
'hasMore' => $hasMore
];
}
/**
* Gets a load images paginated, filtered by image type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
{
$images = $this->image->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
}
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* Search for images by query, of a particular type.
* @param string $type
* @param int $page
* @param int $pageSize
* @param string $searchTerm
* @return array
*/
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
}
/**
* Get gallery images with a particular filter criteria such as
* being within the current book or page.
* @param int $pagination
* @param int $pageSize
* @param $filter
* @param $pageId
* @return array
*/
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
{
$images = $this->image->where('type', '=', 'gallery');
$page = $this->page->findOrFail($pageId);
if ($filter === 'page') {
$images = $images->where('uploaded_to', '=', $page->id);
} elseif ($filter === 'book') {
$validPageIds = $page->book->pages->pluck('id')->toArray();
$images = $images->whereIn('uploaded_to', $validPageIds);
}
return $this->returnPaginated($images, $pagination, $pageSize);
}
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param int $uploadedTo
* @return Image
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$this->loadThumbs($image);
return $image;
}
/**
* Update the details of an image via an array of properties.
* @param Image $image
* @param array $updateDetails
* @return Image
*/
public function updateImageDetails(Image $image, $updateDetails)
{
$image->fill($updateDetails);
$image->save();
$this->loadThumbs($image);
return $image;
}
/**
* Destroys an Image object along with its files and thumbnails.
* @param Image $image
* @return bool
*/
public function destroyImage(Image $image)
{
$this->imageService->destroyImage($image);
return true;
}
/**
* Load thumbnails onto an image object.
* @param Image $image
*/
private function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
'display' => $this->getThumbnail($image, 840, 0, true)
];
}
/**
* 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
*/
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 [];
}
}
}

View File

@@ -1,359 +0,0 @@
<?php namespace BookStack\Repos;
use Activity;
use BookStack\Book;
use BookStack\Chapter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use BookStack\Page;
use BookStack\PageRevision;
class PageRepo
{
protected $page;
protected $pageRevision;
/**
* PageRepo constructor.
* @param Page $page
* @param PageRevision $pageRevision
*/
public function __construct(Page $page, PageRevision $pageRevision)
{
$this->page = $page;
$this->pageRevision = $pageRevision;
}
/**
* Check if a page id exists.
* @param $id
* @return bool
*/
public function idExists($id)
{
return $this->page->where('page_id', '=', $id)->count() > 0;
}
/**
* Get a page via a specific ID.
* @param $id
* @return mixed
*/
public function getById($id)
{
return $this->page->findOrFail($id);
}
/**
* Get all pages.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAll()
{
return $this->page->all();
}
/**
* Get a page identified by the given slug.
* @param $slug
* @param $bookId
* @return mixed
*/
public function getBySlug($slug, $bookId)
{
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->first();
}
/**
* @param $input
* @return Page
*/
public function newFromInput($input)
{
$page = $this->page->fill($input);
return $page;
}
/**
* Count the pages with a particular slug within a book.
* @param $slug
* @param $bookId
* @return mixed
*/
public function countBySlug($slug, $bookId)
{
return $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId)->count();
}
/**
* Save a new page into the system.
* Input validation must be done beforehand.
* @param array $input
* @param Book $book
* @param int $chapterId
* @return Page
*/
public function saveNew(array $input, Book $book, $chapterId = null)
{
$page = $this->newFromInput($input);
$page->slug = $this->findSuitableSlug($page->name, $book->id);
if ($chapterId) $page->chapter_id = $chapterId;
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->created_by = auth()->user()->id;
$page->updated_by = auth()->user()->id;
$book->pages()->save($page);
return $page;
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* @param string $htmlText
* @return string
*/
protected function formatHtml($htmlText)
{
libxml_use_internal_errors(true);
$doc = new \DOMDocument();
$doc->loadHTML($htmlText);
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$lastId = false;
$idArray = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') continue;
// Overwrite id if not a bookstack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
do {
$id = 'bkmrk-' . substr(uniqid(), -5);
} while ($id == $lastId);
$lastId = $id;
$childNode->setAttribute('id', $id);
$idArray[] = $id;
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Gets pages by a search term.
* Highlights page content for showing in results.
* @param string $term
* @param array $whereTerms
* @return mixed
*/
public function getBySearch($term, $whereTerms = [])
{
$terms = explode(' ', preg_quote(trim($term)));
$pages = $this->page->fullTextSearch(['name', 'text'], $terms, $whereTerms);
// Add highlights to page text.
$words = join('|', $terms);
//lookahead/behind assertions ensures cut between words
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
foreach ($pages as $page) {
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
//delimiter between occurrences
$results = [];
foreach ($matches as $line) {
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
}
$matchLimit = 6;
if (count($results) > $matchLimit) {
$results = array_slice($results, 0, $matchLimit);
}
$result = join('... ', $results);
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
if (strlen($result) < 5) {
$result = $page->getExcerpt(80);
}
$page->searchSnippet = $result;
}
return $pages;
}
/**
* Search for image usage.
* @param $imageString
* @return mixed
*/
public function searchForImage($imageString)
{
$pages = $this->page->where('html', 'like', '%' . $imageString . '%')->get();
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
$page->text = '';
}
return count($pages) > 0 ? $pages : false;
}
/**
* Updates a page with any fillable data and saves it into the database.
* @param Page $page
* @param int $book_id
* @param string $input
* @return Page
*/
public function updatePage(Page $page, $book_id, $input)
{
// Save a revision before updating
if ($page->html !== $input['html'] || $page->name !== $input['name']) {
$this->saveRevision($page);
}
// Update with new details
$page->fill($input);
$page->slug = $this->findSuitableSlug($page->name, $book_id, $page->id);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->updated_by = auth()->user()->id;
$page->save();
return $page;
}
/**
* Restores a revision's content back into a page.
* @param Page $page
* @param Book $book
* @param int $revisionId
* @return Page
*/
public function restoreRevision(Page $page, Book $book, $revisionId)
{
$this->saveRevision($page);
$revision = $this->getRevisionById($revisionId);
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug($page->name, $book->id, $page->id);
$page->text = strip_tags($page->html);
$page->updated_by = auth()->user()->id;
$page->save();
return $page;
}
/**
* Saves a page revision into the system.
* @param Page $page
* @return $this
*/
private function saveRevision(Page $page)
{
$revision = $this->pageRevision->fill($page->toArray());
$revision->page_id = $page->id;
$revision->created_by = auth()->user()->id;
$revision->created_at = $page->updated_at;
$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();
}
return $revision;
}
/**
* Gets a single revision via it's id.
* @param $id
* @return mixed
*/
public function getRevisionById($id)
{
return $this->pageRevision->findOrFail($id);
}
/**
* Checks if a slug exists within a book already.
* @param $slug
* @param $bookId
* @param bool|false $currentId
* @return bool
*/
public function doesSlugExist($slug, $bookId, $currentId = false)
{
$query = $this->page->where('slug', '=', $slug)->where('book_id', '=', $bookId);
if ($currentId) $query = $query->where('id', '!=', $currentId);
return $query->count() > 0;
}
/**
* Changes the related book for the specified page.
* Changes the book id of any relations to the page that store the book id.
* @param int $bookId
* @param Page $page
* @return Page
*/
public function changeBook($bookId, Page $page)
{
$page->book_id = $bookId;
foreach ($page->activity as $activity) {
$activity->book_id = $bookId;
$activity->save();
}
$page->slug = $this->findSuitableSlug($page->name, $bookId, $page->id);
$page->save();
return $page;
}
/**
* Gets a suitable slug for the resource
* @param $name
* @param $bookId
* @param bool|false $currentId
* @return string
*/
public function findSuitableSlug($name, $bookId, $currentId = false)
{
$slug = Str::slug($name);
while ($this->doesSlugExist($slug, $bookId, $currentId)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Destroy a given page along with its dependencies.
* @param $page
*/
public function destroy($page)
{
Activity::removeEntity($page);
$page->views()->delete();
$page->revisions()->delete();
$page->delete();
}
}

View File

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

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

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

View File

@@ -1,23 +1,27 @@
<?php namespace BookStack\Repos;
use BookStack\Role;
use BookStack\User;
use Exception;
class UserRepo
{
protected $user;
protected $role;
protected $entityRepo;
/**
* UserRepo constructor.
* @param $user
* @param User $user
* @param Role $role
* @param EntityRepo $entityRepo
*/
public function __construct(User $user, Role $role)
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
{
$this->user = $user;
$this->role = $role;
$this->entityRepo = $entityRepo;
}
/**
@@ -38,6 +42,36 @@ class UserRepo
return $this->user->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsers()
{
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
{
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
$query->where(function($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);
});
}
return $query->paginate($count);
}
/**
* Creates a new user and attaches a role to them.
* @param array $data
@@ -46,16 +80,34 @@ class UserRepo
public function registerNew(array $data)
{
$user = $this->create($data);
$roleId = \Setting::get('registration-role');
$this->attachDefaultRole($user);
if ($roleId === false) {
$roleId = $this->role->getDefault()->id;
// 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');
}
}
$user->attachRoleId($roleId);
return $user;
}
/**
* Give a user the default role. Used when creating a new user.
* @param $user
*/
public function attachDefaultRole($user)
{
$roleId = setting('registration-role');
if ($roleId === false) $roleId = $this->role->first()->id;
$user->attachRoleId($roleId);
}
/**
* Checks if the give user is the only admin.
* @param User $user
@@ -63,15 +115,10 @@ class UserRepo
*/
public function isOnlyAdmin(User $user)
{
if ($user->role->name != 'admin') {
return false;
}
$adminRole = $this->role->where('name', '=', 'admin')->first();
if (count($adminRole->users) > 1) {
return false;
}
if (!$user->roles->pluck('name')->contains('admin')) return false;
$adminRole = $this->role->getRole('admin');
if ($adminRole->users->count() > 1) return false;
return true;
}
@@ -82,10 +129,88 @@ class UserRepo
*/
public function create(array $data)
{
return $this->user->create([
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password'])
'password' => bcrypt($data['password']),
'email_confirmed' => false
]);
}
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
*/
public function destroy(User $user)
{
$user->socialAccounts()->delete();
$user->delete();
}
/**
* Get the latest activity for a user.
* @param User $user
* @param int $count
* @param int $page
* @return array
*/
public function getActivity(User $user, $count = 20, $page = 0)
{
return \Activity::userActivity($user, $count, $page);
}
/**
* Get the recently created content for this given user.
* @param User $user
* @param int $count
* @return mixed
*/
public function getRecentlyCreated(User $user, $count = 20)
{
return [
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
];
}
/**
* Get asset created counts for the give user.
* @param User $user
* @return array
*/
public function getAssetCounts(User $user)
{
return [
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
];
}
/**
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAllRoles()
{
return $this->role->all();
}
/**
* Get all the roles which can be given restricted access to
* other entities in the system.
* @return mixed
*/
public function getRestrictableRoles()
{
return $this->role->where('system_name', '!=', 'admin')->get();
}
}

View File

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

23
app/RolePermission.php Normal file
View File

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

View File

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

View File

@@ -0,0 +1,201 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\FileUploadException;
use BookStack\Attachment;
use Exception;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService extends UploadService
{
/**
* Get an attachment from storage.
* @param Attachment $attachment
* @return string
*/
public function getAttachmentFromStorage(Attachment $attachment)
{
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
return $this->getStorage()->get($attachmentPath);
}
/**
* Store a new attachment upon user upload.
* @param UploadedFile $uploadedFile
* @param int $page_id
* @return Attachment
* @throws FileUploadException
*/
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
'name' => $attachmentName,
'path' => $attachmentPath,
'extension' => $uploadedFile->getClientOriginalExtension(),
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1
]);
return $attachment;
}
/**
* Store a upload, saving to a file and deleting any existing uploads
* attached to that file.
* @param UploadedFile $uploadedFile
* @param Attachment $attachment
* @return Attachment
* @throws FileUploadException
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attachment)
{
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
}
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
$attachment->external = false;
$attachment->extension = $uploadedFile->getClientOriginalExtension();
$attachment->save();
return $attachment;
}
/**
* Save a new File attachment from a given link and name.
* @param string $name
* @param string $link
* @param int $page_id
* @return Attachment
*/
public function saveNewFromLink($name, $link, $page_id)
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
return Attachment::forceCreate([
'name' => $name,
'path' => $link,
'external' => true,
'extension' => '',
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'order' => $largestExistingOrder + 1
]);
}
/**
* 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
* @param $pageId
*/
public function updateFileOrderWithinPage($attachmentList, $pageId)
{
foreach ($attachmentList as $index => $attachment) {
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
}
}
/**
* Update the details of a file.
* @param Attachment $attachment
* @param $requestData
* @return Attachment
*/
public function updateFile(Attachment $attachment, $requestData)
{
$attachment->name = $requestData['name'];
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
$attachment->path = $requestData['link'];
if (!$attachment->external) {
$this->deleteFileInStorage($attachment);
$attachment->external = true;
}
}
$attachment->save();
return $attachment;
}
/**
* Delete a File from the database and storage.
* @param Attachment $attachment
*/
public function deleteFile(Attachment $attachment)
{
if ($attachment->external) {
$attachment->delete();
return;
}
$this->deleteFileInStorage($attachment);
$attachment->delete();
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
* @param Attachment $attachment
*/
protected function deleteFileInStorage(Attachment $attachment)
{
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
$storage = $this->getStorage();
$dirPath = dirname($storedFilePath);
$storage->delete($storedFilePath);
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
}
/**
* Store a file in storage with the given filename
* @param $attachmentName
* @param UploadedFile $uploadedFile
* @return string
* @throws FileUploadException
*/
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
$uploadFileName = $attachmentName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$attachmentPath = $attachmentBasePath . $uploadFileName;
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
try {
$storage->put($attachmentStoragePath, $attachmentData);
} catch (Exception $e) {
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
}
return $attachmentPath;
}
}

View File

@@ -1,30 +1,27 @@
<?php namespace BookStack\Services;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Repos\UserRepo;
use Carbon\Carbon;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
use BookStack\EmailConfirmation;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Database\Connection as Database;
class EmailConfirmationService
{
protected $mailer;
protected $emailConfirmation;
protected $db;
protected $users;
/**
* EmailConfirmationService constructor.
* @param Mailer $mailer
* @param EmailConfirmation $emailConfirmation
* @param Database $db
* @param UserRepo $users
*/
public function __construct(Mailer $mailer, EmailConfirmation $emailConfirmation)
public function __construct(Database $db, UserRepo $users)
{
$this->mailer = $mailer;
$this->emailConfirmation = $emailConfirmation;
$this->db = $db;
$this->users = $users;
}
/**
@@ -36,45 +33,59 @@ class EmailConfirmationService
public function sendConfirmation(User $user)
{
if ($user->email_confirmed) {
throw new ConfirmationEmailException('Email has already been confirmed, Try logging in.', '/login');
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
}
$this->deleteConfirmationsByUser($user);
$token = $this->createEmailConfirmation($user);
$user->notify(new ConfirmEmail($token));
}
/**
* Creates a new email confirmation in the database and returns the token.
* @param User $user
* @return string
*/
public function createEmailConfirmation(User $user)
{
$token = $this->getToken();
$this->emailConfirmation->create([
$this->db->table('email_confirmations')->insert([
'user_id' => $user->id,
'token' => $token,
'token' => $token,
'created_at' => Carbon::now(),
'updated_at' => Carbon::now()
]);
$this->mailer->send('emails/email-confirmation', ['token' => $token], function (Message $message) use ($user) {
$appName = \Setting::get('app-name', 'BookStack');
$message->to($user->email, $user->name)->subject('Confirm your email on ' . $appName . '.');
});
return $token;
}
/**
* Gets an email confirmation by looking up the token,
* Ensures the token has not expired.
* @param string $token
* @return EmailConfirmation
* @return array|null|\stdClass
* @throws UserRegistrationException
*/
public function getEmailConfirmationFromToken($token)
{
$emailConfirmation = $this->emailConfirmation->where('token', '=', $token)->first();
// If not found
$emailConfirmation = $this->db->table('email_confirmations')->where('token', '=', $token)->first();
// If not found show error
if ($emailConfirmation === null) {
throw new UserRegistrationException('This confirmation token is not valid or has already been used, Please try registering again.', '/register');
throw new UserRegistrationException(trans('errors.email_confirmation_invalid'), '/register');
}
// If more than a day old
if (Carbon::now()->subDay()->gt($emailConfirmation->created_at)) {
$this->sendConfirmation($emailConfirmation->user);
throw new UserRegistrationException('The confirmation token has expired, A new confirmation email has been sent.', '/register/confirm');
if (Carbon::now()->subDay()->gt(new Carbon($emailConfirmation->created_at))) {
$user = $this->users->getById($emailConfirmation->user_id);
$this->sendConfirmation($user);
throw new UserRegistrationException(trans('errors.email_confirmation_expired'), '/register/confirm');
}
$emailConfirmation->user = $this->users->getById($emailConfirmation->user_id);
return $emailConfirmation;
}
/**
* Delete all email confirmations that belong to a user.
* @param User $user
@@ -82,7 +93,7 @@ class EmailConfirmationService
*/
public function deleteConfirmationsByUser(User $user)
{
return $this->emailConfirmation->where('user_id', '=', $user->id)->delete();
return $this->db->table('email_confirmations')->where('user_id', '=', $user->id)->delete();
}
/**
@@ -92,7 +103,7 @@ class EmailConfirmationService
protected function getToken()
{
$token = str_random(24);
while ($this->emailConfirmation->where('token', '=', $token)->exists()) {
while ($this->db->table('email_confirmations')->where('token', '=', $token)->exists()) {
$token = str_random(25);
}
return $token;

View File

@@ -0,0 +1,254 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class ExportService
{
protected $entityRepo;
/**
* ExportService constructor.
* @param $entityRepo
*/
public function __construct(EntityRepo $entityRepo)
{
$this->entityRepo = $entityRepo;
}
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
*/
public function pageToContainedHtml(Page $page)
{
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->containHtml($pageHtml);
}
/**
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->containHtml($html);
}
/**
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
*/
public function bookToContainedHtml(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->containHtml($html);
}
/**
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
*/
public function pageToPdf(Page $page)
{
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a book to a PDF file
* @param Book $book
* @return string
*/
public function bookToPdf(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
*/
protected function htmlToPdf($html)
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
}
return $pdf->output();
}
/**
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @return mixed|string
*/
protected function containHtml($htmlContent)
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$isLocal = strpos(trim($srcString), 'http') !== 0;
if ($isLocal) {
$pathString = public_path(trim($srcString, '/'));
} else {
$pathString = $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);
}
}
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
}
}
}
// Replace any relative links with system domain
return $htmlContent;
}
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to provide a nice final output.
* @param Page $page
* @return mixed
*/
public function pageToPlainText(Page $page)
{
$html = $this->entityRepo->renderPage($page);
$text = strip_tags($html);
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
$text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
$text = html_entity_decode($text);
// Add title
$text = $page->name . "\n\n" . $text;
return $text;
}
/**
* Convert a chapter into a plain text string.
* @param Chapter $chapter
* @return string
*/
public function chapterToPlainText(Chapter $chapter)
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
}
/**
* Convert a book into a plain text string.
* @param Book $book
* @return string
*/
public function bookToPlainText(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
$text .= $this->chapterToPlainText($bookChild);
} else {
$text .= $this->pageToPlainText($bookChild);
}
}
return $text;
}
}

View File

@@ -0,0 +1,14 @@
<?php namespace BookStack\Services\Facades;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor() { return 'images'; }
}

View File

@@ -0,0 +1,265 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Cache\Repository as Cache;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService
{
protected $imageTool;
protected $cache;
protected $storageUrl;
/**
* ImageService constructor.
* @param $imageTool
* @param $fileSystem
* @param $cache
*/
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->imageTool = $imageTool;
$this->cache = $cache;
parent::__construct($fileSystem);
}
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
* @param string $type
* @param int $uploadedTo
* @return mixed
* @throws ImageUploadException
*/
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
$imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath());
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
/**
* Gets an image from url and saves it to the database.
* @param $url
* @param string $type
* @param bool|string $imageName
* @return mixed
* @throws \Exception
*/
private function saveNewFromUrl($url, $type, $imageName = false)
{
$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]));
return $this->saveNew($imageName, $imageData, $type);
}
/**
* Saves a new image
* @param string $imageName
* @param string $imageData
* @param string $type
* @param int $uploadedTo
* @return Image
* @throws ImageUploadException
*/
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
{
$storage = $this->getStorage();
$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;
try {
$storage->put($fullPath, $imageData);
$storage->setVisibility($fullPath, 'public');
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
}
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
$imageDetails = [
'name' => $imageName,
'path' => $fullPath,
'url' => $this->getPublicUrl($fullPath),
'type' => $type,
'uploaded_to' => $uploadedTo
];
if (user()->id !== 0) {
$userId = user()->id;
$imageDetails['created_by'] = $userId;
$imageDetails['updated_by'] = $userId;
}
$image = Image::forceCreate($imageDetails);
return $image;
}
/**
* Get the storage path, Dependant of storage type.
* @param Image $image
* @return mixed|string
*/
protected function getPath(Image $image)
{
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
}
/**
* 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 Exception
* @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
$imagePath = $this->getPath($image);
$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();
if ($storage->exists($thumbFilePath)) {
return $this->getPublicUrl($thumbFilePath);
}
try {
$thumb = $this->imageTool->make($storage->get($imagePath));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
} else {
throw $e;
}
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$thumb->fit($width, $height);
}
$thumbData = (string)$thumb->encode();
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
/**
* Destroys an Image object along with its files and thumbnails.
* @param Image $image
* @return bool
*/
public function destroyImage(Image $image)
{
$storage = $this->getStorage();
$imageFolder = dirname($this->getPath($image));
$imageFileName = basename($this->getPath($image));
$allImages = collect($storage->allFiles($imageFolder));
$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);
}
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
* @return mixed
*/
public function saveUserGravatar(User $user, $size = 500)
{
$emailHash = md5(strtolower(trim($user->email)));
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
$image = $this->saveNewFromUrl($url, 'user', $imageName);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->save();
return $image;
}
/**
* Gets a public facing url for an image by checking relevant environment variables.
* @param string $filePath
* @return string
*/
private function getPublicUrl($filePath)
{
if ($this->storageUrl === null) {
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if ($storageUrl == false && config('filesystems.default') === 's3') {
$storageDetails = config('filesystems.disks.s3');
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
}
$this->storageUrl = $storageUrl;
}
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
}
}

97
app/Services/Ldap.php Normal file
View File

@@ -0,0 +1,97 @@
<?php namespace BookStack\Services;
/**
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
* @package BookStack\Services
*/
class Ldap
{
/**
* Connect to a LDAP server.
* @param string $hostName
* @param int $port
* @return resource
*/
public function connect($hostName, $port)
{
return ldap_connect($hostName, $port);
}
/**
* Set the value of a LDAP option for the given connection.
* @param resource $ldapConnection
* @param int $option
* @param mixed $value
* @return bool
*/
public function setOption($ldapConnection, $option, $value)
{
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Set the version number for the given ldap connection.
* @param $ldapConnection
* @param $version
* @return bool
*/
public function setVersion($ldapConnection, $version)
{
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
}
/**
* Search LDAP tree using the provided filter.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function search($ldapConnection, $baseDn, $filter, array $attributes = null)
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
/**
* Get entries from an ldap search result.
* @param resource $ldapConnection
* @param resource $ldapSearchResult
* @return array
*/
public function getEntries($ldapConnection, $ldapSearchResult)
{
return ldap_get_entries($ldapConnection, $ldapSearchResult);
}
/**
* Search and get entries immediately.
* @param resource $ldapConnection
* @param string $baseDn
* @param string $filter
* @param array|null $attributes
* @return resource
*/
public function searchAndGetEntries($ldapConnection, $baseDn, $filter, array $attributes = null)
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
return $this->getEntries($ldapConnection, $search);
}
/**
* Bind to LDAP directory.
* @param resource $ldapConnection
* @param string $bindRdn
* @param string $bindPassword
* @return bool
*/
public function bind($ldapConnection, $bindRdn = null, $bindPassword = null)
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}

View File

@@ -0,0 +1,153 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use Illuminate\Contracts\Auth\Authenticatable;
/**
* Class LdapService
* Handles any app-specific LDAP tasks.
* @package BookStack\Services
*/
class LdapService
{
protected $ldap;
protected $ldapConnection;
protected $config;
/**
* LdapService constructor.
* @param Ldap $ldap
*/
public function __construct(Ldap $ldap)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
* @param $userName
* @return array|null
* @throws LdapException
*/
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 = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
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;
$ldapConnection = $this->getConnection();
try {
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
} catch (\ErrorException $e) {
$ldapBind = false;
}
return $ldapBind;
}
/**
* Bind the system user to the LDAP connection using the given credentials
* otherwise anonymous access is attempted.
* @param $connection
* @throws LdapException
*/
protected function bindSystemUser($connection)
{
$ldapDn = $this->config['dn'];
$ldapPass = $this->config['pass'];
$isAnonymous = ($ldapDn === false || $ldapPass === false);
if ($isAnonymous) {
$ldapBind = $this->ldap->bind($connection);
} else {
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
}
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
}
/**
* Get the connection to the LDAP server.
* Creates a new connection if one does not exist.
* @return resource
* @throws LdapException
*/
protected function getConnection()
{
if ($this->ldapConnection !== null) return $this->ldapConnection;
// Check LDAP extension in installed
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// 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, '');
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
}
// Set any required options
if ($this->config['version']) {
$this->ldap->setVersion($ldapConnection, $this->config['version']);
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
* @param array $attrs
* @return string
*/
protected function buildFilter($filterString, array $attrs)
{
$newAttrs = [];
foreach ($attrs as $key => $attrText) {
$newKey = '${' . $key . '}';
$newAttrs[$newKey] = $attrText;
}
return strtr($filterString, $newAttrs);
}
}

View File

@@ -0,0 +1,653 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\JointPermission;
use BookStack\Ownable;
use BookStack\Page;
use BookStack\Role;
use BookStack\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class PermissionService
{
protected $currentAction;
protected $isAdminUser;
protected $userRoles = false;
protected $currentUserModel = false;
public $book;
public $chapter;
public $page;
protected $db;
protected $jointPermission;
protected $role;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param Connection $db
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->role = $role;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
}
/**
* Prepare the local entity cache and ensure it's empty
*/
protected function readyEntityCache()
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
}
/**
* Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) $book = false;
if (isset($this->entityCache['books'])) {
$this->entityCache['books']->put($bookId, $book);
}
return $book;
}
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return Book
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) $chapter = false;
if (isset($this->entityCache['chapters'])) {
$this->entityCache['chapters']->put($chapterId, $chapter);
}
return $chapter;
}
/**
* Get the roles for the current user;
* @return array|bool
*/
protected function getRoles()
{
if ($this->userRoles !== false) return $this->userRoles;
$roles = [];
if (auth()->guest()) {
$roles[] = $this->role->getSystemRole('public')->id;
return $roles;
}
foreach ($this->currentUser()->roles as $role) {
$roles[] = $role->id;
}
return $roles;
}
/**
* Re-generate all entity permission from scratch.
*/
public function buildJointPermissions()
{
$this->jointPermission->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get();
// Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
$this->createManyJointPermissions($chapters, $roles);
});
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
$this->createManyJointPermissions($pages, $roles);
});
}
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$roles = $this->role->get();
$entities = collect([$entity]);
if ($entity->isA('book')) {
$entities = $entities->merge($entity->chapters);
$entities = $entities->merge($entity->pages);
} elseif ($entity->isA('chapter')) {
$entities = $entities->merge($entity->pages);
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->get();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Build the entity jointPermissions for a particular role.
* @param Role $role
*/
public function buildJointPermissionForRole(Role $role)
{
$roles = collect([$role]);
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
}
/**
* Delete the entity jointPermissions attached to a particular role.
* @param Role $role
*/
public function deleteJointPermissionsForRole(Role $role)
{
$this->deleteManyJointPermissionsForRoles([$role]);
}
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Role[] $roles
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
foreach ($roles as $role) {
$role->jointPermissions()->delete();
}
}
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
$this->deleteManyJointPermissionsForEntities([$entity]);
}
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
/**
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param Collection $roles
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$jointPermissions = [];
foreach ($entities as $entity) {
foreach ($roles as $role) {
foreach ($this->getActions($entity) as $action) {
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
}
}
}
$this->jointPermission->insert($jointPermissions);
}
/**
* Get the actions related to an entity.
* @param $entity
* @return array
*/
protected function getActions($entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter')) {
$baseActions[] = 'page-create';
} else if ($entity->isA('book')) {
$baseActions[] = 'page-create';
$baseActions[] = 'chapter-create';
}
return $baseActions;
}
/**
* Create entity permission data for an entity and role
* for a particular action.
* @param Entity $entity
* @param Role $role
* @param $action
* @return array
*/
protected function createJointPermissionData(Entity $entity, Role $role, $action)
{
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
$explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction);
if ($role->system_name === 'admin') {
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
}
if ($entity->isA('book')) {
if (!$entity->restricted) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
$chapter = $this->getChapter($entity->chapter_id);
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
$acknowledgeChapter = ($chapter && $chapter->restricted);
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
} else {
$hasAccess = $entity->hasRestriction($role->id, $action);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
}
}
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
* @param Entity $entity
* @param Role $role
* @param $action
* @param $permissionAll
* @param $permissionOwn
* @return array
*/
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
{
$entityClass = get_class($entity);
return [
'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entityClass,
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
'created_by' => $entity->getRawAttribute('created_by')
];
}
/**
* Checks if an entity has a restriction set upon it.
* @param Ownable $ownable
* @param $permission
* @return bool
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
$this->currentAction = 'view';
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
return ($allPermission || ($isOwner && $ownPermission));
}
// Handle abnormal create jointPermissions
if ($action === 'create') {
$this->currentAction = $permission;
}
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
$this->clean();
return $q;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/**
* The general query filter to remove all entities
* that the current user does not have access to.
* @param $query
* @return mixed
*/
protected function entityRestrictionQuery($query)
{
$q = $query->where(function ($parentQuery) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getRoles())
->where('action', '=', $this->currentAction)
->where(function ($query) {
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
});
});
});
$this->clean();
return $q;
}
/**
* Get the children of a book in an efficient single query, Filtered by the permission system.
* @param integer $book_id
* @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageContentSelect = $fetchPageContent ? 'html' : "''";
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
$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);
});
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
return $query;
}
/**
* Add restrictions for a generic entity
* @param string $entityType
* @param Builder|Entity $query
* @param string $action
* @return mixed
*/
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
{
if (strtolower($entityType) === 'page') {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
$query->where('draft', '=', false);
if ($this->currentUser()) {
$query->orWhere(function ($query) {
$query->where('draft', '=', true)->where('created_by', '=', $this->currentUser()->id);
});
}
});
}
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
/**
* Filter items that have entities set a a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
});
});
});
$this->clean();
return $q;
}
/**
* Filters pages that are a direct relation to another item.
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$q = $query->where(function ($query) use ($tableDetails) {
$query->where(function ($query) use (&$tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where('entity_type', '=', 'Bookstack\\Page')
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
});
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
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
*/
private function currentUser()
{
if ($this->currentUserModel === false) {
$this->currentUserModel = user();
}
return $this->currentUserModel;
}
/**
* Clean the cached user elements.
*/
private function clean()
{
$this->currentUserModel = false;
$this->userRoles = false;
$this->isAdminUser = null;
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
@@ -15,6 +16,7 @@ class SettingService
protected $setting;
protected $cache;
protected $localCache = [];
protected $cachePrefix = 'setting-';
@@ -38,25 +40,48 @@ class SettingService
*/
public function get($key, $default = false)
{
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
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);
$this->localCache[$key] = $formatted;
return $formatted;
}
/**
* Get a user-specific setting from the database or cache.
* @param User $user
* @param $key
* @param bool $default
* @return bool|string
*/
public function getUser($user, $key, $default = false)
{
return $this->get($this->userKey($user->id, $key), $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
* @param $key
* @param $default
* @return mixed
*/
protected function getValueFromStore($key, $default)
{
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
// Check for an overriding value
$overrideValue = $this->getOverrideValue($key);
if ($overrideValue !== null) return $overrideValue;
// Check the cache
$cacheKey = $this->cachePrefix . $key;
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
if($settingObject !== null) {
if ($settingObject !== null) {
$value = $settingObject->value;
$this->cache->forever($cacheKey, $value);
return $value;
@@ -65,6 +90,10 @@ class SettingService
return $default;
}
/**
* Clear an item from the cache completely.
* @param $key
*/
protected function clearFromCache($key)
{
$cacheKey = $this->cachePrefix . $key;
@@ -99,6 +128,16 @@ class SettingService
return $setting !== null;
}
/**
* Check if a user setting is in the database.
* @param $key
* @return bool
*/
public function hasUser($key)
{
return $this->has($this->userKey($key));
}
/**
* Add a setting to the database.
* @param $key
@@ -116,6 +155,28 @@ class SettingService
return true;
}
/**
* Put a user-specific setting into the database.
* @param User $user
* @param $key
* @param $value
* @return bool
*/
public function putUser($user, $key, $value)
{
return $this->put($this->userKey($user->id, $key), $value);
}
/**
* Convert a setting key into a user-specific key.
* @param $key
* @return string
*/
protected function userKey($userId, $key = '')
{
return 'user:' . $userId . ':' . $key;
}
/**
* Removes a setting from the database.
* @param $key
@@ -131,14 +192,38 @@ class SettingService
return true;
}
/**
* Delete settings for a given user id.
* @param $userId
* @return mixed
*/
public function deleteUserSettings($userId)
{
return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
}
/**
* Gets a setting model from the database for the given key.
* @param $key
* @return mixed
*/
private function getSettingObjectByKey($key)
protected function getSettingObjectByKey($key)
{
return $this->setting->where('setting_key', '=', $key)->first();
}
/**
* Returns an override value for a setting based on certain app conditions.
* Used where certain configuration options overrule others.
* Returns null if no override value is available.
* @param $key
* @return bool|null
*/
protected function getOverrideValue($key)
{
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
return null;
}
}

View File

@@ -1,14 +1,11 @@
<?php namespace BookStack\Services;
use GuzzleHttp\Exception\ClientException;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Auth\AuthController;
use BookStack\Repos\UserRepo;
use BookStack\SocialAccount;
use BookStack\User;
class SocialAuthService
{
@@ -17,7 +14,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
/**
* SocialAuthService constructor.
@@ -73,12 +70,12 @@ class SocialAuthService
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException('This ' . $socialDriver . ' account is already in use, Try logging in via the ' . $socialDriver . ' option.', '/login');
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
}
if($this->userRepo->getByEmail($socialUser->getEmail())) {
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
$email = $socialUser->getEmail();
throw new UserRegistrationException('The email '. $email.' is already in use. If you already have an account you can connect your ' . $socialDriver .' account from your profile settings.', '/login');
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
}
return $socialUser;
@@ -101,9 +98,8 @@ class SocialAuthService
// Get any attached social accounts or users
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$user = $this->userRepo->getByEmail($socialUser->getEmail());
$isLoggedIn = auth()->check();
$currentUser = auth()->user();
$currentUser = user();
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
@@ -116,28 +112,28 @@ class SocialAuthService
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
\Session::flash('success', title_case($socialDriver) . ' account was successfully attached to your profile.');
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in and the social account exists and is already linked to the current user.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
\Session::flash('error', 'This ' . title_case($socialDriver) . ' account is already attached to your profile.');
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// When a user is logged in, A social account exists but the users do not match.
// Change the user that the social account is assigned to.
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
\Session::flash('success', 'This ' . title_case($socialDriver) . ' account is already used buy another user.');
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => title_case($socialDriver)]));
return redirect($currentUser->getEditUrl());
}
// Otherwise let the user know this social account is not used by anyone.
$message = 'This ' . $socialDriver . ' account is not linked to any users. Please attach it in your profile settings';
if (\Setting::get('registration-enabled')) {
$message .= ' or, If you do not yet have an account, You can register an account using the ' . $socialDriver . ' option';
$message = trans('errors.social_account_not_used', ['socialAccount' => title_case($socialDriver)]);
if (setting('registration-enabled')) {
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
}
@@ -159,8 +155,8 @@ class SocialAuthService
{
$driver = trim(strtolower($socialDriver));
if (!in_array($driver, $this->validSocialDrivers)) abort(404, 'Social Driver Not Found');
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured;
if (!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;
}
@@ -172,9 +168,10 @@ class SocialAuthService
*/
private function checkDriverConfigured($driver)
{
$upperName = strtoupper($driver);
$config = [env($upperName . '_APP_ID', false), env($upperName . '_APP_SECRET', false), env('APP_URL', false)];
return (!in_array(false, $config) && !in_array(null, $config));
$lowerName = strtolower($driver);
$configPrefix = 'services.' . $lowerName . '.';
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
return !in_array(false, $config) && !in_array(null, $config);
}
/**
@@ -184,16 +181,26 @@ class SocialAuthService
public function getActiveDrivers()
{
$activeDrivers = [];
foreach ($this->validSocialDrivers as $driverName) {
if ($this->checkDriverConfigured($driverName)) {
$activeDrivers[$driverName] = true;
foreach ($this->validSocialDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
}
}
return $activeDrivers;
}
/**
* @param string $socialDriver
* Get the presentational name for a driver.
* @param $driver
* @return mixed
*/
public function getDriverName($driver)
{
return config('services.' . strtolower($driver) . '.name');
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
* @return SocialAccount
*/
@@ -214,10 +221,9 @@ class SocialAuthService
*/
public function detachSocialAccount($socialDriver)
{
session();
auth()->user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
\Session::flash('success', $socialDriver . ' account successfully detached');
return redirect(auth()->user()->getEditUrl());
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());
}
}

View File

@@ -0,0 +1,64 @@
<?php namespace BookStack\Services;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
class UploadService
{
/**
* @var FileSystem
*/
protected $fileSystem;
/**
* @var FileSystemInstance
*/
protected $storageInstance;
/**
* FileService constructor.
* @param $fileSystem
*/
public function __construct(FileSystem $fileSystem)
{
$this->fileSystem = $fileSystem;
}
/**
* Get the storage that will be used for storing images.
* @return FileSystemInstance
*/
protected function getStorage()
{
if ($this->storageInstance !== null) return $this->storageInstance;
$storageType = config('filesystems.default');
$this->storageInstance = $this->fileSystem->disk($storageType);
return $this->storageInstance;
}
/**
* Check whether or not a folder is empty.
* @param $path
* @return bool
*/
protected function isFolderEmpty($path)
{
$files = $this->getStorage()->files($path);
$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

@@ -1,23 +1,22 @@
<?php namespace BookStack\Services;
use BookStack\Entity;
use BookStack\View;
class ViewService
{
protected $view;
protected $user;
protected $permissionService;
/**
* ViewService constructor.
* @param $view
* @param View $view
* @param PermissionService $permissionService
*/
public function __construct(View $view)
public function __construct(View $view, PermissionService $permissionService)
{
$this->view = $view;
$this->user = auth()->user();
$this->permissionService = $permissionService;
}
/**
@@ -27,7 +26,9 @@ class ViewService
*/
public function add(Entity $entity)
{
$view = $entity->views()->where('user_id', '=', $this->user->id)->first();
$user = user();
if ($user === null || $user->isDefault()) return 0;
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
$view->increment('views');
@@ -36,35 +37,59 @@ class ViewService
// Otherwise create new view count
$entity->views()->save($this->view->create([
'user_id' => $this->user->id,
'user_id' => $user->id,
'views' => 1
]));
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param bool|false|array $filterModel
*/
public function getPopular($count = 10, $page = 0, $filterModel = false)
{
$skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModel && is_array($filterModel)) {
$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');
}
/**
* Get all recently viewed entities for the current user.
* @param int $count
* @param int $page
* @param int $count
* @param int $page
* @param Entity|bool $filterModel
* @return mixed
*/
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
{
$skipCount = $count * $page;
$query = $this->view->where('user_id', '=', auth()->user()->id);
$user = user();
if ($user === null || $user->isDefault()) return collect();
if ($filterModel) $query->where('viewable_type', '=', get_class($filterModel));
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
$views = $query->with('viewable')->orderBy('updated_at', 'desc')->skip($skipCount)->take($count)->get();
$viewedEntities = $views->map(function ($item) {
return $item->viewable()->getResults();
});
return $viewedEntities;
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
$query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');
return $viewables;
}
/**
* Reset all view counts by deleting all views.
*/
@@ -73,5 +98,4 @@ class ViewService
$this->view->truncate();
}
}

View File

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

View File

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

19
app/Tag.php Normal file
View File

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

View File

@@ -1,34 +1,31 @@
<?php
namespace BookStack;
<?php namespace BookStack;
use BookStack\Notifications\ResetPassword;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Notifications\Notifiable;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword;
use Authenticatable, CanResetPassword, Notifiable;
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['name', 'email', 'password'];
protected $fillable = ['name', 'email', 'image_id'];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['password', 'remember_token'];
@@ -40,43 +37,67 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
protected $permissions;
/**
* Returns a default guest user.
* Returns the default public user.
* @return User
*/
public static function getDefault()
{
return new static([
'email' => 'guest',
'name' => 'Guest'
]);
return static::where('system_name', '=', 'public')->first();
}
/**
* Permissions and roles
* Check if the user is the default public user.
* @return bool
*/
public function isDefault()
{
return $this->system_name === 'public';
}
/**
* The roles that belong to the user.
* @return BelongsToMany
*/
public function roles()
{
return $this->belongsToMany('BookStack\Role');
}
public function getRoleAttribute()
{
return $this->roles()->with('permissions')->first();
if ($this->id === 0) return ;
return $this->belongsToMany(Role::class);
}
/**
* Loads the user's permissions from thier role.
* Check if the user has a role.
* @param $role
* @return mixed
*/
private function loadPermissions()
public function hasRole($role)
{
if (isset($this->permissions)) return;
return $this->roles->pluck('name')->contains($role);
}
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasSystemRole($role)
{
return $this->roles->pluck('system_name')->contains('admin');
}
/**
* Get all permissions belonging to a the current user.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function permissions($cache = true)
{
if(isset($this->permissions) && $cache) return $this->permissions;
$this->load('roles.permissions');
$permissions = $this->roles[0]->permissions;
$permissionsArray = $permissions->pluck('name')->all();
$this->permissions = $permissionsArray;
$permissions = $this->roles->map(function($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
}
/**
@@ -86,11 +107,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if ($this->email == 'guest') {
return false;
}
$this->loadPermissions();
return array_search($permissionName, $this->permissions) !== false;
if ($this->email === 'guest') return false;
return $this->permissions()->pluck('name')->contains($permissionName);
}
/**
@@ -108,17 +126,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function attachRoleId($id)
{
$this->roles()->sync([$id]);
$this->roles()->attach($id);
}
/**
* Get the social account associated with this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function socialAccounts()
{
return $this->hasMany('BookStack\SocialAccount');
return $this->hasMany(SocialAccount::class);
}
/**
@@ -138,15 +155,30 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Returns the user's avatar,
* Uses Gravatar as the avatar service.
*
* @param int $size
* @return string
*/
public function getAvatar($size = 50)
{
$emailHash = md5(strtolower(trim($this->email)));
return '//www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
$default = baseUrl('/user_avatar.png');
$imageId = $this->image_id;
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
try {
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
} catch (\Exception $err) {
$avatar = $default;
}
return $avatar;
}
/**
* Get the avatar for the user.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function avatar()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
@@ -155,6 +187,40 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getEditUrl()
{
return '/users/' . $this->id;
return baseUrl('/settings/users/' . $this->id);
}
/**
* Get the url that links to this user's profile.
* @return mixed
*/
public function getProfileUrl()
{
return baseUrl('/user/' . $this->id);
}
/**
* Get a shortened version of the user's name.
* @param int $chars
* @return string
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) return $this->name;
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) return $splitName[0];
return '';
}
/**
* Send the password reset notification.
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$this->notify(new ResetPassword($token));
}
}

View File

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

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