mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-06 00:59:39 +03:00
Compare commits
506 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b226e7d39 | ||
|
|
9865446267 | ||
|
|
173f728e4a | ||
|
|
9772b2f69d | ||
|
|
c0f4cf4b5c | ||
|
|
cc1f46cbf4 | ||
|
|
a641b4da2c | ||
|
|
4f85ce02c6 | ||
|
|
9eb65dcd78 | ||
|
|
bee5e2c7ca | ||
|
|
8f12c8bc99 | ||
|
|
2740603d99 | ||
|
|
3e870c30e1 | ||
|
|
8f0d08763a | ||
|
|
0e7166f7f6 | ||
|
|
7d9de23a25 | ||
|
|
eda9e89c55 | ||
|
|
82c6597a60 | ||
|
|
cd35e13024 | ||
|
|
4400ad7e8d | ||
|
|
610ee2c182 | ||
|
|
4fd5dbcfdd | ||
|
|
613228fab2 | ||
|
|
a61c9c5e98 | ||
|
|
2036618fbd | ||
|
|
ce6e25b341 | ||
|
|
73ebe571a1 | ||
|
|
a274406038 | ||
|
|
1a6293ce24 | ||
|
|
8db047de70 | ||
|
|
b005acdd6c | ||
|
|
822fea4303 | ||
|
|
ac110eb6b2 | ||
|
|
64785ed9da | ||
|
|
cac31b2074 | ||
|
|
2d306949b5 | ||
|
|
78e94bb003 | ||
|
|
622ea03c65 | ||
|
|
f1f59cf086 | ||
|
|
773be963ba | ||
|
|
ef9354a0cb | ||
|
|
39a205ed28 | ||
|
|
70f39757b1 | ||
|
|
c429cf7818 | ||
|
|
926abbe776 | ||
|
|
4fabef3a57 | ||
|
|
65ebffa002 | ||
|
|
a04064f981 | ||
|
|
7d19057e68 | ||
|
|
0de0507137 | ||
|
|
7a8954ee65 | ||
|
|
a3ad840bdd | ||
|
|
9b271e559f | ||
|
|
4597069083 | ||
|
|
a3f19ebe96 | ||
|
|
1af5bbf3f7 | ||
|
|
1278fb4969 | ||
|
|
9249addb5c | ||
|
|
78f9c01519 | ||
|
|
f696aa5eea | ||
|
|
7c86c26cd0 | ||
|
|
cfc0c593db | ||
|
|
bb43acef21 | ||
|
|
09c2814dc7 | ||
|
|
1c43602f4b | ||
|
|
5ef4cd80c3 | ||
|
|
e01f23583f | ||
|
|
b1ee1a856f | ||
|
|
4da72aa267 | ||
|
|
529971c534 | ||
|
|
83c8f73142 | ||
|
|
916a82616f | ||
|
|
d25cd83d8e | ||
|
|
efb6a6b457 | ||
|
|
f295ab87b4 | ||
|
|
ca8be9af3c | ||
|
|
0155525945 | ||
|
|
934a833818 | ||
|
|
3a402f6adc | ||
|
|
8a9505bf8c | ||
|
|
265f5db03f | ||
|
|
58fa7679bc | ||
|
|
992f03a3c0 | ||
|
|
57ea2e92ec | ||
|
|
9af636bd48 | ||
|
|
3dda622f0a | ||
|
|
7d951b842c | ||
|
|
94bf5b8fbb | ||
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
3d5899d28c | ||
|
|
917d7428d6 | ||
|
|
bcc01bd8ff | ||
|
|
2c34a99248 | ||
|
|
789d17ab3f | ||
|
|
58117bcf2d | ||
|
|
b5caaa73b7 | ||
|
|
7997300f96 | ||
|
|
888f435651 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
a8471b2c66 | ||
|
|
0627efe5e9 | ||
|
|
af7d62799c | ||
|
|
bb00c331e4 | ||
|
|
807f92b693 | ||
|
|
c5d31ea7b2 | ||
|
|
ef1bde8bb1 | ||
|
|
8897945609 | ||
|
|
9382d647d7 | ||
|
|
0d17d18d07 | ||
|
|
24eef03fb9 | ||
|
|
e51352e1a4 | ||
|
|
2dfb1ae3ee | ||
|
|
39928e1c63 | ||
|
|
40ca50e44f | ||
|
|
fc7b8c49fb | ||
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
fdabafffda | ||
|
|
e2df15fe20 | ||
|
|
54bac17ef0 | ||
|
|
7634ac4e12 | ||
|
|
c4f5ab12cf | ||
|
|
57a063cdfb | ||
|
|
1fa90e4f12 | ||
|
|
d62cdd58d3 | ||
|
|
ed6ec341df | ||
|
|
0cfff6ab6f | ||
|
|
7ca66c5d5e | ||
|
|
9cbea1eb08 | ||
|
|
1a2d374f24 | ||
|
|
e32929029b | ||
|
|
eb76e882c5 | ||
|
|
d326417edc | ||
|
|
a3a8fef6b2 | ||
|
|
0c16334426 | ||
|
|
600f8cd142 | ||
|
|
5c8c85a0ff | ||
|
|
7a6f21648a | ||
|
|
df0e03cd07 | ||
|
|
85db812fea | ||
|
|
fb5b5e138d | ||
|
|
3eaf03a7ac | ||
|
|
5420f3451c | ||
|
|
7d94da10fb | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
c7322a71f7 | ||
|
|
2c3523f6a1 | ||
|
|
dd6076049c | ||
|
|
ba8ba5c634 | ||
|
|
c2069f37cc | ||
|
|
1e0aa7ee2c | ||
|
|
27942f5ce8 | ||
|
|
d0ff79ea60 | ||
|
|
3de02566bf | ||
|
|
93fd869ba3 | ||
|
|
3ca149137e | ||
|
|
db9aa41096 | ||
|
|
bf8e7f3393 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
7a059a5e90 | ||
|
|
e5fc104aff | ||
|
|
d0ed165630 | ||
|
|
68ef6a842f | ||
|
|
c1f070a136 | ||
|
|
c2cc1ec5e5 | ||
|
|
386925ad8e | ||
|
|
243c1db408 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
9e7bcacf8c | ||
|
|
7be7d7d1e7 | ||
|
|
04c1d0e071 | ||
|
|
ab62e0f75b | ||
|
|
d85f99c87c | ||
|
|
c42b6aece9 | ||
|
|
7f8f3080c5 | ||
|
|
9cf4191079 | ||
|
|
b8e2d75014 | ||
|
|
f522f16526 | ||
|
|
b010d2663d | ||
|
|
a083ceaf44 | ||
|
|
95798a2eba | ||
|
|
43b6633183 | ||
|
|
c50ac022a8 | ||
|
|
a3d36237e2 | ||
|
|
a2be61f26d | ||
|
|
79f5b579d7 | ||
|
|
66ecee1e26 | ||
|
|
02e86ea18f | ||
|
|
723dbe1da7 | ||
|
|
65fe89441f | ||
|
|
2093122ac5 | ||
|
|
ab584c93bc | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
fc504a3d2c | ||
|
|
dd805503fb | ||
|
|
f24336f77a | ||
|
|
aa6a752e38 | ||
|
|
83b576eb19 | ||
|
|
c4e31a0d5e | ||
|
|
f8cdd6e80d | ||
|
|
f8b5a0fd50 | ||
|
|
6f4a6ab8ea | ||
|
|
9c4b6f36f1 | ||
|
|
140aed3586 | ||
|
|
cf87b78636 | ||
|
|
ec827da5a5 | ||
|
|
20528a2442 | ||
|
|
78886b1e67 | ||
|
|
d9debaf032 | ||
|
|
b3c47649b4 | ||
|
|
70be28d22c | ||
|
|
9df4dee1b2 | ||
|
|
60ffe6a993 | ||
|
|
0c880def5e | ||
|
|
2744b2a243 | ||
|
|
d4360d6347 | ||
|
|
175b1785c0 | ||
|
|
e4660a5ba2 | ||
|
|
e2fa6d83c6 | ||
|
|
a0d32e7b88 | ||
|
|
ced15b64a8 | ||
|
|
f0723b6ee7 | ||
|
|
2162da3a14 | ||
|
|
f02cfd8271 | ||
|
|
b6a0f9f069 | ||
|
|
5c9c1d1a4b | ||
|
|
ab4c5a55b8 | ||
|
|
43c2fc3c37 | ||
|
|
371033a0f2 | ||
|
|
06706a2d9c | ||
|
|
c548c06086 | ||
|
|
8e5067ee91 | ||
|
|
0310b8614e | ||
|
|
829fecd338 | ||
|
|
44a293f051 | ||
|
|
a92c35a7ac | ||
|
|
691db40a33 | ||
|
|
2ae89f2c32 | ||
|
|
9d37af9453 | ||
|
|
a5d2a26fcc | ||
|
|
c61c3bc608 | ||
|
|
1420f239fc | ||
|
|
3d0e1bc9db | ||
|
|
71ccb90ef4 | ||
|
|
c8564b7792 | ||
|
|
215c69acb2 | ||
|
|
c1f67372a7 | ||
|
|
b929c0adbb | ||
|
|
1e5951a75f | ||
|
|
a644f64c6b | ||
|
|
c8740c0171 | ||
|
|
91ee895a74 | ||
|
|
339d4ec355 | ||
|
|
615038ac6d | ||
|
|
3c57cbc567 | ||
|
|
da929d5edc | ||
|
|
124c4d0778 | ||
|
|
19d79b6a0f | ||
|
|
3a9caea846 | ||
|
|
34e6098687 | ||
|
|
98a1e57ba9 | ||
|
|
f31cdf7bff | ||
|
|
9a3e1490ff | ||
|
|
1f2fd58e28 | ||
|
|
bcf3a0f677 | ||
|
|
f40dedb451 | ||
|
|
266f6846b5 | ||
|
|
49ca9a9db8 | ||
|
|
e7a7d8cc1d | ||
|
|
34c2da4ab1 | ||
|
|
7497203014 | ||
|
|
d731a4f695 | ||
|
|
03f6be6d0e | ||
|
|
938b5b4d1d | ||
|
|
792b51b5dc | ||
|
|
a57eae20b0 | ||
|
|
1ffe212d83 | ||
|
|
e29c07b28d | ||
|
|
745d15d200 | ||
|
|
05cfe74904 | ||
|
|
4d4a57d1bf | ||
|
|
382f155f76 | ||
|
|
60030a774d | ||
|
|
a045e46571 | ||
|
|
44eaa65c3b | ||
|
|
26730e56ea | ||
|
|
7b6f8cb902 | ||
|
|
111835f402 | ||
|
|
3fc935d4bb | ||
|
|
b939785ece | ||
|
|
723628cfcf | ||
|
|
cf489453c9 | ||
|
|
6616065d82 | ||
|
|
2df82dd870 | ||
|
|
0ca8d7fc03 | ||
|
|
f36e6d9917 | ||
|
|
6a4b020dd8 | ||
|
|
b51ede2372 | ||
|
|
1a4797abc4 | ||
|
|
c09300c06f | ||
|
|
ae353bb3f4 | ||
|
|
54f5bf9437 | ||
|
|
a0bfdf0e5c | ||
|
|
7ef17bb394 | ||
|
|
48587d2c38 | ||
|
|
b0f4500c34 | ||
|
|
af032f8993 | ||
|
|
f177b02cae | ||
|
|
5323cb5224 | ||
|
|
0a22af7b14 | ||
|
|
b54702ab08 | ||
|
|
a98fc71720 | ||
|
|
9a05223e7d | ||
|
|
a7e3c26fe3 | ||
|
|
37de4e2e0a | ||
|
|
61a911dd39 | ||
|
|
cc5d0ef4cf | ||
|
|
7843d8f054 | ||
|
|
d759f9c121 | ||
|
|
f25e585008 | ||
|
|
4f96cd9164 | ||
|
|
7893e8229f | ||
|
|
795ef67712 | ||
|
|
88f6d3f241 | ||
|
|
8e87f01aa0 | ||
|
|
c4fdcfc5d1 | ||
|
|
cb8117e8df | ||
|
|
d547ed4a6b | ||
|
|
f40c389a15 | ||
|
|
bc1e84325c | ||
|
|
a0c605faae | ||
|
|
ba2033a8fb | ||
|
|
a7848b916b | ||
|
|
44c41e9e4d | ||
|
|
a663364223 | ||
|
|
72fda8f592 | ||
|
|
1aa9465611 | ||
|
|
4d3194d784 | ||
|
|
5404f22bf9 | ||
|
|
ccb2cb5b7c | ||
|
|
26ba056302 | ||
|
|
0dac9c68f0 | ||
|
|
3df6c9ac05 | ||
|
|
2db081938f | ||
|
|
d979570227 | ||
|
|
99c42033b1 | ||
|
|
7ba6962707 | ||
|
|
6eda1c1fb2 | ||
|
|
5a218d5056 | ||
|
|
8dbc5cf9c6 | ||
|
|
d33f136660 | ||
|
|
173dad345e | ||
|
|
47b0eb6324 | ||
|
|
c35c37008d | ||
|
|
aad2ee675c | ||
|
|
b8aabfffe8 | ||
|
|
ac8e124d01 | ||
|
|
857f8c2a95 | ||
|
|
71e81615a3 | ||
|
|
611d37da04 | ||
|
|
ee400eece6 | ||
|
|
da7c686541 | ||
|
|
d0a7a8b890 | ||
|
|
28c706fee3 | ||
|
|
0e799a3857 | ||
|
|
b91d6e2bfa | ||
|
|
44315233d7 | ||
|
|
91efa601be | ||
|
|
18f86fbf9b | ||
|
|
e5a96b0cb0 | ||
|
|
526be33ab2 | ||
|
|
831f441879 | ||
|
|
ea16ad7e94 | ||
|
|
ba6eb54552 | ||
|
|
47e3ef1be2 | ||
|
|
7791599fb5 | ||
|
|
bbfb330b92 | ||
|
|
20729a618f | ||
|
|
f705e7683b | ||
|
|
dc996adb20 | ||
|
|
14ea6c9de3 | ||
|
|
a64c638ccc | ||
|
|
359c067279 | ||
|
|
8bb6394b47 | ||
|
|
ea8083ac6e | ||
|
|
04367eb4c5 | ||
|
|
f160020941 | ||
|
|
75a795ab72 | ||
|
|
47a6c621ff | ||
|
|
024b0d8a64 | ||
|
|
83d77d5166 | ||
|
|
e53e4f85c7 | ||
|
|
a04a800258 | ||
|
|
e6ea53b3e6 | ||
|
|
fd67f61b43 | ||
|
|
92922288dd | ||
|
|
280f1a0a5b | ||
|
|
588fd7d165 | ||
|
|
857d9ed3f1 | ||
|
|
d27875bad1 | ||
|
|
de989ffa9a | ||
|
|
b43f997dab | ||
|
|
5e686bb624 | ||
|
|
99b14621f9 | ||
|
|
da9083bf1f | ||
|
|
8833b5bc3b | ||
|
|
33e35c9a8a | ||
|
|
e408067b10 | ||
|
|
4c580d1571 | ||
|
|
b493becadf | ||
|
|
c71f00b2ec | ||
|
|
e458411f91 | ||
|
|
564f4f7c74 | ||
|
|
4e82d93350 | ||
|
|
f1e1a745b0 | ||
|
|
4b4642c8ea | ||
|
|
2b603b0488 | ||
|
|
20bb76afdb | ||
|
|
cf04a0d818 | ||
|
|
66a746e297 | ||
|
|
a4d43ee24b | ||
|
|
2acef3c2ec | ||
|
|
9884cca00c | ||
|
|
f7793a70a9 | ||
|
|
ceba3d31fb | ||
|
|
3f3fad7113 | ||
|
|
d7e0d3e2d6 | ||
|
|
5ab0db9690 | ||
|
|
00308ad4ab | ||
|
|
6c09334ba0 | ||
|
|
65b2c90522 | ||
|
|
eecc08edde | ||
|
|
eb19aadc75 | ||
|
|
884664bfe9 | ||
|
|
8911e3f441 | ||
|
|
7d38c96a23 | ||
|
|
162d893143 | ||
|
|
4b36df08a8 | ||
|
|
0b01a77c16 | ||
|
|
bf8716bb22 | ||
|
|
d56e7e7c79 | ||
|
|
57754c8211 | ||
|
|
8aedba14a3 | ||
|
|
875a8bdaff | ||
|
|
53bcfe528d | ||
|
|
1c8102bb89 | ||
|
|
ebeca256f0 | ||
|
|
a042e22481 | ||
|
|
ef1b98019a | ||
|
|
c7a2d568bf | ||
|
|
66917520cb | ||
|
|
5e01c30882 | ||
|
|
f76a2a69f7 | ||
|
|
65ddd16532 | ||
|
|
c0680d5717 | ||
|
|
bd6a1a66d1 | ||
|
|
da37700ac2 | ||
|
|
3f7180fa99 | ||
|
|
20f9a50cee | ||
|
|
712ccd23c4 | ||
|
|
ee7e1122d3 | ||
|
|
c157dc3490 | ||
|
|
4824ef2760 | ||
|
|
b4da081552 | ||
|
|
df10b508d8 | ||
|
|
ec3aeb3315 | ||
|
|
68b1d87ebe | ||
|
|
483cb41665 | ||
|
|
34dc4a1b6d | ||
|
|
3e70c661a1 | ||
|
|
9e033709a7 | ||
|
|
82e671a06d | ||
|
|
474770af51 | ||
|
|
78be644332 | ||
|
|
06c81e69b9 | ||
|
|
3dc3d4a639 | ||
|
|
6d8b0605a0 | ||
|
|
349162ea13 | ||
|
|
bbd1384acb | ||
|
|
36daa09441 | ||
|
|
4c5566755f | ||
|
|
461977cf9a | ||
|
|
837cccd4d4 | ||
|
|
7a5442e81b | ||
|
|
704b808e9e | ||
|
|
6aa2bf9e27 | ||
|
|
a192b600fc | ||
|
|
b714652e10 | ||
|
|
ff7cbd14fc | ||
|
|
04197e393a | ||
|
|
a74d551bd6 | ||
|
|
aca37b8784 | ||
|
|
691027a522 | ||
|
|
e287d965f5 | ||
|
|
ea82c2f61b | ||
|
|
a7d9646b19 | ||
|
|
a34a07c610 | ||
|
|
034478409e | ||
|
|
fe438bdb45 | ||
|
|
6acd958927 |
14
.env.example
14
.env.example
@@ -12,11 +12,13 @@
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# Application URL
|
||||
# Remove the hash below and set a URL if using BookStack behind
|
||||
# a proxy or if using a third-party authentication option.
|
||||
# This must be the root URL that you want to host BookStack on.
|
||||
# All URL's in BookStack will be generated using this value.
|
||||
#APP_URL=https://example.com
|
||||
# All URLs in BookStack will be generated using this value
|
||||
# to ensure URLs generated are consistent and secure.
|
||||
# If you change this in the future you may need to run a command
|
||||
# to update stored URLs in the database. Command example:
|
||||
# php artisan bookstack:update-url https://old.example.com https://new.example.com
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
@@ -28,8 +30,8 @@ DB_PASSWORD=database_user_password
|
||||
# Can be 'smtp' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# Mail sender options
|
||||
MAIL_FROM_NAME=BookStack
|
||||
# Mail sender details
|
||||
MAIL_FROM_NAME="BookStack"
|
||||
MAIL_FROM=bookstack@example.com
|
||||
|
||||
# SMTP mail options
|
||||
|
||||
@@ -51,7 +51,7 @@ DB_USERNAME=database_username
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Mail system to use
|
||||
# Can be 'smtp', 'mail' or 'sendmail'
|
||||
# Can be 'smtp' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# Mail sending options
|
||||
@@ -195,10 +195,12 @@ LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
LDAP_START_TLS=false
|
||||
LDAP_TLS_INSECURE=false
|
||||
LDAP_ID_ATTRIBUTE=uid
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
||||
LDAP_THUMBNAIL_ATTRIBUTE=null
|
||||
LDAP_FOLLOW_REFERRALS=true
|
||||
LDAP_DUMP_USER_DETAILS=false
|
||||
|
||||
@@ -221,6 +223,7 @@ SAML2_IDP_x509=null
|
||||
SAML2_ONELOGIN_OVERRIDES=null
|
||||
SAML2_DUMP_USER_DETAILS=false
|
||||
SAML2_AUTOLOAD_METADATA=false
|
||||
SAML2_IDP_AUTHNCONTEXT=true
|
||||
|
||||
# SAML group sync configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||
@@ -245,16 +248,29 @@ AVATAR_URL=
|
||||
DRAWIO=true
|
||||
|
||||
# Default item listing view
|
||||
# Used for public visitors and user's without a preference
|
||||
# Can be 'list' or 'grid'
|
||||
# Used for public visitors and user's without a preference.
|
||||
# Can be 'list' or 'grid'.
|
||||
APP_VIEWS_BOOKS=list
|
||||
APP_VIEWS_BOOKSHELVES=grid
|
||||
APP_VIEWS_BOOKSHELF=grid
|
||||
|
||||
# Use dark mode by default
|
||||
# Will be overriden by any user/session preference.
|
||||
APP_DEFAULT_DARK_MODE=false
|
||||
|
||||
# Page revision limit
|
||||
# Number of page revisions to keep in the system before deleting old revisions.
|
||||
# If set to 'false' a limit will not be enforced.
|
||||
REVISION_LIMIT=50
|
||||
|
||||
# Recycle Bin Lifetime
|
||||
# The number of days that content will remain in the recycle bin before
|
||||
# being considered for auto-removal. It is not a guarantee that content will
|
||||
# be removed after this time.
|
||||
# Set to 0 for no recycle bin functionality.
|
||||
# Set to -1 for unlimited recycle bin lifetime.
|
||||
RECYCLE_BIN_LIFETIME=30
|
||||
|
||||
# Allow <script> tags in page content
|
||||
# Note, if set to 'true' the page editor may still escape scripts.
|
||||
ALLOW_CONTENT_SCRIPTS=false
|
||||
@@ -265,6 +281,18 @@ ALLOW_CONTENT_SCRIPTS=false
|
||||
# Contents of the robots.txt file can be overridden, making this option obsolete.
|
||||
ALLOW_ROBOTS=null
|
||||
|
||||
# Allow server-side fetches to be performed to potentially unknown
|
||||
# and user-provided locations. Primarily used in exports when loading
|
||||
# in externally referenced assets.
|
||||
# Can be 'true' or 'false'.
|
||||
ALLOW_UNTRUSTED_SERVER_FETCHING=false
|
||||
|
||||
# A list of hosts that BookStack can be iframed within.
|
||||
# Space separated if multiple. BookStack host domain is auto-inferred.
|
||||
# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
|
||||
# Setting this option will also auto-adjust cookies to be SameSite=None.
|
||||
ALLOWED_IFRAME_HOSTS=null
|
||||
|
||||
# The default and maximum item-counts for listing API requests.
|
||||
API_DEFAULT_ITEM_COUNT=100
|
||||
API_MAX_ITEM_COUNT=500
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [ssddanbrown]
|
||||
17
.github/ISSUE_TEMPLATE/api_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/api_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: New API Endpoint or Feature
|
||||
about: Request a new endpoint or API feature be added
|
||||
labels: ":nut_and_bolt: API Request"
|
||||
---
|
||||
|
||||
#### API Endpoint or Feature
|
||||
|
||||
Clearly describe what you'd like to have added to the API.
|
||||
|
||||
#### Use-Case
|
||||
|
||||
Explain the use-case that you're working-on that requires the above request.
|
||||
|
||||
#### Additional Context
|
||||
|
||||
If required, add any other context about the feature request here.
|
||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discord chat support
|
||||
url: https://discord.gg/ztkBqR2
|
||||
about: Realtime support / chat with the community and the team.
|
||||
|
||||
- name: Debugging & Common Issues
|
||||
url: https://www.bookstackapp.com/docs/admin/debugging/
|
||||
about: Find details on how to debug issues and view common issues with thier resolutions.
|
||||
60
.github/translators.txt
vendored
60
.github/translators.txt
vendored
@@ -49,6 +49,12 @@ Name :: Languages
|
||||
@jzoy :: Simplified Chinese
|
||||
@ististudio :: Korean
|
||||
@leomartinez :: Spanish Argentina
|
||||
@geins :: German
|
||||
@Ereza :: Catalan
|
||||
@benediktvolke :: German
|
||||
@Baptistou :: French
|
||||
@arcoai :: Spanish
|
||||
@Jokuna :: Korean
|
||||
cipi1965 :: Italian
|
||||
Mykola Ronik (Mantikor) :: Ukrainian
|
||||
furkanoyk :: Turkish
|
||||
@@ -123,3 +129,57 @@ Jakub Bouček (jakubboucek) :: Czech
|
||||
Marco (cdrfun) :: German
|
||||
10935336 :: Chinese Simplified
|
||||
孟繁阳 (FanyangMeng) :: Chinese Simplified
|
||||
Andrej Močan (andrejm) :: Slovenian
|
||||
gilane9_ :: Arabic
|
||||
Raed alnahdi (raednahdi) :: Arabic
|
||||
Xiphoseer :: German
|
||||
MerlinSVK (merlinsvk) :: Slovak
|
||||
Kauê Sena (kaue.sena.ks) :: Portuguese, Brazilian
|
||||
MatthieuParis :: French
|
||||
Douradinho :: Portuguese, Brazilian
|
||||
Gaku Yaguchi (tama11) :: Japanese
|
||||
johnroyer :: Chinese Traditional
|
||||
jackaaa :: Chinese Traditional
|
||||
Irfan Hukama Arsyad (IrfanArsyad) :: Indonesian
|
||||
Jeff Huang (s8321414) :: Chinese Traditional
|
||||
Luís Tiago Favas (starkyller) :: Portuguese
|
||||
semirte :: Bosnian
|
||||
aarchijs :: Latvian
|
||||
Martins Pilsetnieks (pilsetnieks) :: Latvian
|
||||
Yonatan Magier (yonatanmgr) :: Hebrew
|
||||
FastHogi :: German Informal; German
|
||||
Ole Anders (Swoy) :: Norwegian Bokmal
|
||||
Atlochowski (atlochowski) :: Polish
|
||||
Simon (DefaultSimon) :: Slovenian
|
||||
Reinis Mednis (Mednis) :: Latvian
|
||||
toisho (toishoki) :: Turkish
|
||||
nikservik :: Ukrainian; Russian; Polish
|
||||
HenrijsS :: Latvian
|
||||
Pascal R-B (pborgner) :: German
|
||||
Boris (Ginfred) :: Russian
|
||||
Jonas Anker Rasmussen (jonasanker) :: Danish
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
|
||||
kometchtech :: Japanese
|
||||
Auri (Atalonica) :: Catalan
|
||||
Francesco Franchina (ffranchina) :: Italian
|
||||
Aimrane Kds (aimrane.kds) :: Arabic
|
||||
whenwesober :: Indonesian
|
||||
Rem (remkovdhoef) :: Dutch
|
||||
syn7ax69 :: Bulgarian; Turkish
|
||||
Blaade :: French
|
||||
Behzad HosseinPoor (behzad.hp) :: Persian
|
||||
Ole Aldric (Swoy) :: Norwegian Bokmal
|
||||
fharis arabia (raednahdi) :: Arabic
|
||||
Alexander Predl (Harveyhase68) :: German
|
||||
Rem (Rem9000) :: Dutch
|
||||
Michał Stelmach (stelmach-web) :: Polish
|
||||
arniom :: French
|
||||
REMOVED_USER :: Turkish
|
||||
林祖年 (contagion) :: Chinese Traditional
|
||||
Siamak Guodarzi (siamakgoudarzi88) :: Persian
|
||||
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
|
||||
Nathanaël (nathanaelhoun) :: French
|
||||
A Ibnu Hibban (abd.ibnuhibban) :: Indonesian
|
||||
Frost-ZX :: Chinese Simplified
|
||||
Kuzma Simonov (ovmach) :: Russian
|
||||
Vojtěch Krystek (acantophis) :: Czech
|
||||
|
||||
23
.github/workflows/phpunit.yml
vendored
23
.github/workflows/phpunit.yml
vendored
@@ -2,24 +2,27 @@ name: phpunit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!l10n_master'
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@@ -38,7 +41,7 @@ jobs:
|
||||
- name: Setup Database
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
||||
23
.github/workflows/test-migrations.yml
vendored
23
.github/workflows/test-migrations.yml
vendored
@@ -2,24 +2,27 @@ name: test-migrations
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!l10n_master'
|
||||
branches-ignore:
|
||||
- l10n_master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php: [7.2, 7.4]
|
||||
php: ['7.3', '7.4', '8.0']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
@@ -38,7 +41,7 @@ jobs:
|
||||
- name: Create database & user
|
||||
run: |
|
||||
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
|
||||
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
|
||||
|
||||
|
||||
@@ -3,49 +3,59 @@
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @property string $key
|
||||
* @property User $user
|
||||
* @property string $type
|
||||
* @property User $user
|
||||
* @property Entity $entity
|
||||
* @property string $extra
|
||||
* @property string $detail
|
||||
* @property string $entity_type
|
||||
* @property int $entity_id
|
||||
* @property int $user_id
|
||||
* @property int $book_id
|
||||
* @property int $entity_id
|
||||
* @property int $user_id
|
||||
*/
|
||||
class Activity extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the entity for this activity.
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
if ($this->entity_type === '') {
|
||||
$this->entity_type = null;
|
||||
}
|
||||
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user this activity relates to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function user()
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns text from the language files, Looks up by using the
|
||||
* activity key.
|
||||
* Returns text from the language files, Looks up by using the activity key.
|
||||
*/
|
||||
public function getText()
|
||||
public function getText(): string
|
||||
{
|
||||
return trans('activities.' . $this->key);
|
||||
return trans('activities.' . $this->type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this activity is intended to be for an entity.
|
||||
*/
|
||||
public function isForEntity(): bool
|
||||
{
|
||||
return Str::startsWith($this->type, [
|
||||
'page_', 'chapter_', 'book_', 'bookshelf_',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +63,6 @@ class Activity extends Model
|
||||
*/
|
||||
public function isSimilarTo(Activity $activityB): bool
|
||||
{
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,63 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Entity;
|
||||
use Illuminate\Support\Collection;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ActivityService
|
||||
{
|
||||
protected $activity;
|
||||
protected $user;
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
*/
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->user = user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add activity data to database.
|
||||
* Add activity data to database for an entity.
|
||||
*/
|
||||
public function add(Entity $entity, string $activityKey, ?int $bookId = null)
|
||||
public function addForEntity(Entity $entity, string $type)
|
||||
{
|
||||
$activity = $this->newActivityForUser($activityKey, $bookId);
|
||||
$activity = $this->newActivityForUser($type);
|
||||
$entity->activity()->save($activity);
|
||||
$this->setNotification($activityKey);
|
||||
$this->setNotification($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a activity history with a message, without binding to a entity.
|
||||
* Add a generic activity event to the database.
|
||||
*
|
||||
* @param string|Loggable $detail
|
||||
*/
|
||||
public function addMessage(string $activityKey, string $message, ?int $bookId = null)
|
||||
public function add(string $type, $detail = '')
|
||||
{
|
||||
$this->newActivityForUser($activityKey, $bookId)->forceFill([
|
||||
'extra' => $message
|
||||
])->save();
|
||||
if ($detail instanceof Loggable) {
|
||||
$detail = $detail->logDescriptor();
|
||||
}
|
||||
|
||||
$this->setNotification($activityKey);
|
||||
$activity = $this->newActivityForUser($type);
|
||||
$activity->detail = $detail;
|
||||
$activity->save();
|
||||
$this->setNotification($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new activity instance for the current user.
|
||||
*/
|
||||
protected function newActivityForUser(string $key, ?int $bookId = null): Activity
|
||||
protected function newActivityForUser(string $type): Activity
|
||||
{
|
||||
return $this->activity->newInstance()->forceFill([
|
||||
'key' => strtolower($key),
|
||||
'user_id' => $this->user->id,
|
||||
'book_id' => $bookId ?? 0,
|
||||
'type' => strtolower($type),
|
||||
'user_id' => user()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -61,15 +66,13 @@ class ActivityService
|
||||
* and instead uses the 'extra' field with the entities name.
|
||||
* Used when an entity is deleted.
|
||||
*/
|
||||
public function removeEntity(Entity $entity): Collection
|
||||
public function removeEntity(Entity $entity)
|
||||
{
|
||||
$activities = $entity->activity()->get();
|
||||
$entity->activity()->update([
|
||||
'extra' => $entity->name,
|
||||
'entity_id' => 0,
|
||||
'entity_type' => '',
|
||||
'detail' => $entity->name,
|
||||
'entity_id' => null,
|
||||
'entity_type' => null,
|
||||
]);
|
||||
return $activities;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +81,7 @@ class ActivityService
|
||||
public function latest(int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['user', 'entity'])
|
||||
->skip($count * $page)
|
||||
@@ -94,17 +97,30 @@ class ActivityService
|
||||
*/
|
||||
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
|
||||
{
|
||||
/** @var [string => int[]] $queryIds */
|
||||
$queryIds = [$entity->getMorphClass() => [$entity->id]];
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->newQuery()->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
|
||||
->where('entity_id', '=', $entity->id);
|
||||
$queryIds[(new Chapter())->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
|
||||
}
|
||||
if ($entity->isA('book') || $entity->isA('chapter')) {
|
||||
$queryIds[(new Page())->getMorphClass()] = $entity->pages()->visible()->pluck('id');
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['entity', 'user.avatar'])
|
||||
$query = $this->activity->newQuery();
|
||||
$query->where(function (Builder $query) use ($queryIds) {
|
||||
foreach ($queryIds as $morphClass => $idArr) {
|
||||
$query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
|
||||
$innerQuery->where('entity_type', '=', $morphClass)
|
||||
->whereIn('entity_id', $idArr);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$activity = $query->orderBy('created_at', 'desc')
|
||||
->with(['entity' => function (Relation $query) {
|
||||
$query->withTrashed();
|
||||
}, 'user.avatar'])
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get();
|
||||
@@ -118,7 +134,7 @@ class ActivityService
|
||||
public function userActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
$activityList = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
|
||||
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
->where('user_id', '=', $user->id)
|
||||
->skip($count * $page)
|
||||
@@ -130,7 +146,9 @@ class ActivityService
|
||||
|
||||
/**
|
||||
* Filters out similar activity.
|
||||
*
|
||||
* @param Activity[] $activities
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function filterSimilar(iterable $activities): array
|
||||
@@ -152,9 +170,9 @@ class ActivityService
|
||||
/**
|
||||
* Flashes a notification message to the session if an appropriate message is available.
|
||||
*/
|
||||
protected function setNotification(string $activityKey)
|
||||
protected function setNotification(string $type)
|
||||
{
|
||||
$notificationTextKey = 'activities.' . $activityKey . '_notification';
|
||||
$notificationTextKey = 'activities.' . $type . '_notification';
|
||||
if (trans()->has($notificationTextKey)) {
|
||||
$message = trans($notificationTextKey);
|
||||
session()->flash('success', $message);
|
||||
@@ -172,7 +190,7 @@ class ActivityService
|
||||
return;
|
||||
}
|
||||
|
||||
$message = str_replace("%u", $username, $message);
|
||||
$message = str_replace('%u', $username, $message);
|
||||
$channel = config('logging.failed_login.channel');
|
||||
Log::channel($channel)->warning($message);
|
||||
}
|
||||
|
||||
56
app/Actions/ActivityType.php
Normal file
56
app/Actions/ActivityType.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
class ActivityType
|
||||
{
|
||||
const PAGE_CREATE = 'page_create';
|
||||
const PAGE_UPDATE = 'page_update';
|
||||
const PAGE_DELETE = 'page_delete';
|
||||
const PAGE_RESTORE = 'page_restore';
|
||||
const PAGE_MOVE = 'page_move';
|
||||
|
||||
const CHAPTER_CREATE = 'chapter_create';
|
||||
const CHAPTER_UPDATE = 'chapter_update';
|
||||
const CHAPTER_DELETE = 'chapter_delete';
|
||||
const CHAPTER_MOVE = 'chapter_move';
|
||||
|
||||
const BOOK_CREATE = 'book_create';
|
||||
const BOOK_UPDATE = 'book_update';
|
||||
const BOOK_DELETE = 'book_delete';
|
||||
const BOOK_SORT = 'book_sort';
|
||||
|
||||
const BOOKSHELF_CREATE = 'bookshelf_create';
|
||||
const BOOKSHELF_UPDATE = 'bookshelf_update';
|
||||
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||
|
||||
const COMMENTED_ON = 'commented_on';
|
||||
const PERMISSIONS_UPDATE = 'permissions_update';
|
||||
|
||||
const SETTINGS_UPDATE = 'settings_update';
|
||||
const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
|
||||
|
||||
const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
|
||||
const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
|
||||
const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
|
||||
|
||||
const USER_CREATE = 'user_create';
|
||||
const USER_UPDATE = 'user_update';
|
||||
const USER_DELETE = 'user_delete';
|
||||
|
||||
const API_TOKEN_CREATE = 'api_token_create';
|
||||
const API_TOKEN_UPDATE = 'api_token_update';
|
||||
const API_TOKEN_DELETE = 'api_token_delete';
|
||||
|
||||
const ROLE_CREATE = 'role_create';
|
||||
const ROLE_UPDATE = 'role_update';
|
||||
const ROLE_DELETE = 'role_delete';
|
||||
|
||||
const AUTH_PASSWORD_RESET = 'auth_password_reset_request';
|
||||
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
|
||||
const AUTH_LOGIN = 'auth_login';
|
||||
const AUTH_REGISTER = 'auth_register';
|
||||
|
||||
const MFA_SETUP_METHOD = 'mfa_setup_method';
|
||||
const MFA_REMOVE_METHOD = 'mfa_remove_method';
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
use BookStack\Ownable;
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property string text
|
||||
@@ -8,31 +12,32 @@ use BookStack\Ownable;
|
||||
* @property int|null parent_id
|
||||
* @property int local_id
|
||||
*/
|
||||
class Comment extends Ownable
|
||||
class Comment extends Model
|
||||
{
|
||||
use HasCreatorAndUpdater;
|
||||
|
||||
protected $fillable = ['text', 'parent_id'];
|
||||
protected $appends = ['created', 'updated'];
|
||||
|
||||
/**
|
||||
* Get the entity that this comment belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
* Get the entity that this comment belongs to.
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment has been updated since creation.
|
||||
* @return bool
|
||||
*/
|
||||
public function isUpdated()
|
||||
public function isUpdated(): bool
|
||||
{
|
||||
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created date as a relative diff.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCreatedAttribute()
|
||||
@@ -42,6 +47,7 @@ class Comment extends Ownable
|
||||
|
||||
/**
|
||||
* Get updated date as a relative diff.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedAttribute()
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
use BookStack\Entities\Entity;
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Facades\Activity as ActivityService;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
* Class CommentRepo.
|
||||
*/
|
||||
class CommentRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
* @var Comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
@@ -44,6 +45,8 @@ class CommentRepo
|
||||
$comment->parent_id = $parent_id;
|
||||
|
||||
$entity->comments()->save($comment);
|
||||
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
@@ -56,6 +59,7 @@ class CommentRepo
|
||||
$comment->text = $text;
|
||||
$comment->html = $this->commentToHtml($text);
|
||||
$comment->save();
|
||||
|
||||
return $comment;
|
||||
}
|
||||
|
||||
@@ -73,8 +77,8 @@ class CommentRepo
|
||||
public function commentToHtml(string $commentText): string
|
||||
{
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'max_nesting_level' => 10,
|
||||
'html_input' => 'strip',
|
||||
'max_nesting_level' => 10,
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
@@ -87,6 +91,7 @@ class CommentRepo
|
||||
protected function getNextLocalId(Entity $entity): int
|
||||
{
|
||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||
|
||||
return ($comments->local_id ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Actions/Favourite.php
Normal file
19
app/Actions/Favourite.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Favourite extends Model
|
||||
{
|
||||
protected $fillable = ['user_id'];
|
||||
|
||||
/**
|
||||
* Get the related model that can be favourited.
|
||||
*/
|
||||
public function favouritable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,36 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
* @package BookStack
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'value', 'order'];
|
||||
protected $hidden = ['id', 'entity_id', 'entity_type'];
|
||||
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
* Get the entity that this tag belongs to.
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name search for this tag name.
|
||||
*/
|
||||
public function nameUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) . '%5D');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name and value search for this tag's values.
|
||||
*/
|
||||
public function valueUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) . '%3D' . urlencode($this->value) . '%5D');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use DB;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TagRepo
|
||||
{
|
||||
|
||||
protected $tag;
|
||||
protected $permissionService;
|
||||
|
||||
@@ -26,7 +27,9 @@ class TagRepo
|
||||
*/
|
||||
public function getNameSuggestions(?string $searchTerm): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('name');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
|
||||
@@ -35,6 +38,7 @@ class TagRepo
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
|
||||
return $query->get(['name'])->pluck('name');
|
||||
}
|
||||
|
||||
@@ -45,7 +49,9 @@ class TagRepo
|
||||
*/
|
||||
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
|
||||
{
|
||||
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
|
||||
$query = $this->tag->newQuery()
|
||||
->select('*', DB::raw('count(*) as count'))
|
||||
->groupBy('value');
|
||||
|
||||
if ($searchTerm) {
|
||||
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
|
||||
@@ -58,11 +64,12 @@ class TagRepo
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
|
||||
return $query->get(['value'])->pluck('value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* Save an array of tags to an entity.
|
||||
*/
|
||||
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
|
||||
{
|
||||
@@ -85,6 +92,7 @@ class TagRepo
|
||||
{
|
||||
$name = trim($input['name']);
|
||||
$value = isset($input['value']) ? trim($input['value']) : '';
|
||||
|
||||
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,58 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* Class View
|
||||
* Views are stored per-item per-person within the database.
|
||||
* They can be used to find popular items or recently viewed items
|
||||
* at a per-person level. They do not record every view instance as an
|
||||
* activity. Only the latest and original view times could be recognised.
|
||||
*
|
||||
* @property int $views
|
||||
* @property int $user_id
|
||||
*/
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['user_id', 'views'];
|
||||
|
||||
/**
|
||||
* Get all owning viewable models.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function viewable()
|
||||
public function viewable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the current user's view count for the given viewable model.
|
||||
*/
|
||||
public static function incrementFor(Viewable $viewable): int
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @var View $view */
|
||||
$view = $viewable->views()->firstOrNew([
|
||||
'user_id' => $user->id,
|
||||
], ['views' => 0]);
|
||||
|
||||
$view->forceFill(['views' => $view->views + 1])->save();
|
||||
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all views from the system.
|
||||
*/
|
||||
public static function clearAll()
|
||||
{
|
||||
static::query()->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use DB;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
protected $view;
|
||||
protected $permissionService;
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the given entity.
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
$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');
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
// Otherwise create new view count
|
||||
$entity->views()->save($this->view->newInstance([
|
||||
'user_id' => $user->id,
|
||||
'views' => 1
|
||||
]));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param string|array $filterModels
|
||||
* @param string $action - used for permission checking
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
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 Entity|bool $filterModel
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) {
|
||||
$query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
|
||||
}
|
||||
$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.
|
||||
*/
|
||||
public function resetAll()
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php namespace BookStack\Api;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Api;
|
||||
|
||||
use BookStack\Http\Controllers\Api\ApiController;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use ReflectionClass;
|
||||
@@ -10,19 +14,37 @@ use ReflectionMethod;
|
||||
|
||||
class ApiDocsGenerator
|
||||
{
|
||||
|
||||
protected $reflectionClasses = [];
|
||||
protected $controllerClasses = [];
|
||||
|
||||
/**
|
||||
* Load the docs form the cache if existing
|
||||
* otherwise generate and store in the cache.
|
||||
*/
|
||||
public static function generateConsideringCache(): Collection
|
||||
{
|
||||
$appVersion = trim(file_get_contents(base_path('version')));
|
||||
$cacheKey = 'api-docs::' . $appVersion;
|
||||
if (Cache::has($cacheKey) && config('app.env') === 'production') {
|
||||
$docs = Cache::get($cacheKey);
|
||||
} else {
|
||||
$docs = (new static())->generate();
|
||||
Cache::put($cacheKey, $docs, 60 * 24);
|
||||
}
|
||||
|
||||
return $docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate API documentation.
|
||||
*/
|
||||
public function generate(): Collection
|
||||
protected function generate(): Collection
|
||||
{
|
||||
$apiRoutes = $this->getFlatApiRoutes();
|
||||
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
|
||||
$apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
|
||||
$apiRoutes = $apiRoutes->groupBy('base_model');
|
||||
|
||||
return $apiRoutes;
|
||||
}
|
||||
|
||||
@@ -38,6 +60,7 @@ class ApiDocsGenerator
|
||||
$exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
|
||||
$route["example_{$exampleType}"] = $exampleContent;
|
||||
}
|
||||
|
||||
return $route;
|
||||
});
|
||||
}
|
||||
@@ -52,13 +75,15 @@ class ApiDocsGenerator
|
||||
$comment = $method->getDocComment();
|
||||
$route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
|
||||
$route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
|
||||
|
||||
return $route;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load body params and their rules by inspecting the given class and method name.
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
*
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
|
||||
{
|
||||
@@ -73,6 +98,7 @@ class ApiDocsGenerator
|
||||
foreach ($rules as $param => $ruleString) {
|
||||
$rules[$param] = explode('|', $ruleString);
|
||||
}
|
||||
|
||||
return count($rules) > 0 ? $rules : null;
|
||||
}
|
||||
|
||||
@@ -83,11 +109,13 @@ class ApiDocsGenerator
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
|
||||
|
||||
return implode(' ', $matches[1] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reflection method from the given class name and method name.
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
|
||||
@@ -112,16 +140,16 @@ class ApiDocsGenerator
|
||||
[$controller, $controllerMethod] = explode('@', $route->action['uses']);
|
||||
$baseModelName = explode('.', explode('/', $route->uri)[1])[0];
|
||||
$shortName = $baseModelName . '-' . $controllerMethod;
|
||||
|
||||
return [
|
||||
'name' => $shortName,
|
||||
'uri' => $route->uri,
|
||||
'method' => $route->methods[0],
|
||||
'controller' => $controller,
|
||||
'controller_method' => $controllerMethod,
|
||||
'name' => $shortName,
|
||||
'uri' => $route->uri,
|
||||
'method' => $route->methods[0],
|
||||
'controller' => $controller,
|
||||
'controller_method' => $controllerMethod,
|
||||
'controller_method_kebab' => Str::kebab($controllerMethod),
|
||||
'base_model' => $baseModelName,
|
||||
'base_model' => $baseModelName,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<?php namespace BookStack\Api;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Api;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ApiToken extends Model
|
||||
/**
|
||||
* Class ApiToken.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $token_id
|
||||
* @property string $secret
|
||||
* @property string $name
|
||||
* @property Carbon $expires_at
|
||||
* @property User $user
|
||||
*/
|
||||
class ApiToken extends Model implements Loggable
|
||||
{
|
||||
protected $fillable = ['name', 'expires_at'];
|
||||
protected $casts = [
|
||||
'expires_at' => 'date:Y-m-d'
|
||||
'expires_at' => 'date:Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -28,4 +41,12 @@ class ApiToken extends Model
|
||||
{
|
||||
return Carbon::now()->addYears(100)->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Api;
|
||||
|
||||
use BookStack\Auth\Access\LoginService;
|
||||
use BookStack\Exceptions\ApiAuthException;
|
||||
use Illuminate\Auth\GuardHelpers;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
@@ -12,7 +13,6 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class ApiTokenGuard implements Guard
|
||||
{
|
||||
|
||||
use GuardHelpers;
|
||||
|
||||
/**
|
||||
@@ -20,9 +20,14 @@ class ApiTokenGuard implements Guard
|
||||
*/
|
||||
protected $request;
|
||||
|
||||
/**
|
||||
* @var LoginService
|
||||
*/
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* The last auth exception thrown in this request.
|
||||
*
|
||||
* @var ApiAuthException
|
||||
*/
|
||||
protected $lastAuthException;
|
||||
@@ -30,11 +35,12 @@ class ApiTokenGuard implements Guard
|
||||
/**
|
||||
* ApiTokenGuard constructor.
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
public function __construct(Request $request, LoginService $loginService)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
@@ -47,6 +53,7 @@ class ApiTokenGuard implements Guard
|
||||
}
|
||||
|
||||
$user = null;
|
||||
|
||||
try {
|
||||
$user = $this->getAuthorisedUserFromRequest();
|
||||
} catch (ApiAuthException $exception) {
|
||||
@@ -54,19 +61,20 @@ class ApiTokenGuard implements Guard
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current user is authenticated. If not, throw an exception.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||
*
|
||||
* @throws ApiAuthException
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
if (! is_null($user = $this->user())) {
|
||||
if (!is_null($user = $this->user())) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -79,6 +87,7 @@ class ApiTokenGuard implements Guard
|
||||
|
||||
/**
|
||||
* Check the API token in the request and fetch a valid authorised user.
|
||||
*
|
||||
* @throws ApiAuthException
|
||||
*/
|
||||
protected function getAuthorisedUserFromRequest(): Authenticatable
|
||||
@@ -93,11 +102,16 @@ class ApiTokenGuard implements Guard
|
||||
|
||||
$this->validateToken($token, $secret);
|
||||
|
||||
if ($this->loginService->awaitingEmailConfirmation($token->user)) {
|
||||
throw new ApiAuthException(trans('errors.email_confirmation_awaiting'));
|
||||
}
|
||||
|
||||
return $token->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the format of the token header value string.
|
||||
*
|
||||
* @throws ApiAuthException
|
||||
*/
|
||||
protected function validateTokenHeaderValue(string $authToken): void
|
||||
@@ -114,6 +128,7 @@ class ApiTokenGuard implements Guard
|
||||
/**
|
||||
* Validate the given secret against the given token and ensure the token
|
||||
* currently has access to the instance API.
|
||||
*
|
||||
* @throws ApiAuthException
|
||||
*/
|
||||
protected function validateToken(?ApiToken $token, string $secret): void
|
||||
@@ -163,4 +178,4 @@ class ApiTokenGuard implements Guard
|
||||
{
|
||||
$this->user = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Api;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Api;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@@ -6,7 +8,6 @@ use Illuminate\Http\Request;
|
||||
|
||||
class ListingResponseBuilder
|
||||
{
|
||||
|
||||
protected $query;
|
||||
protected $request;
|
||||
protected $fields;
|
||||
@@ -18,7 +19,7 @@ class ListingResponseBuilder
|
||||
'lt' => '<',
|
||||
'gte' => '>=',
|
||||
'lte' => '<=',
|
||||
'like' => 'like'
|
||||
'like' => 'like',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ class ListingResponseBuilder
|
||||
$data = $this->fetchData($filteredQuery);
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'data' => $data,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
@@ -54,6 +55,7 @@ class ListingResponseBuilder
|
||||
{
|
||||
$query = $this->countAndOffsetQuery($query);
|
||||
$query = $this->sortQuery($query);
|
||||
|
||||
return $query->get($this->fields);
|
||||
}
|
||||
|
||||
@@ -95,6 +97,7 @@ class ListingResponseBuilder
|
||||
}
|
||||
|
||||
$queryOperator = $this->filterOperators[$filterOperator];
|
||||
|
||||
return [$field, $queryOperator, $value];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace BookStack;
|
||||
|
||||
class Application extends \Illuminate\Foundation\Application
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the path to the application configuration files.
|
||||
*
|
||||
* @param string $path Optionally, a path to append to the config path
|
||||
* @param string $path Optionally, a path to append to the config path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function configPath($path = '')
|
||||
@@ -18,6 +18,6 @@ class Application extends \Illuminate\Foundation\Application
|
||||
. 'app'
|
||||
. DIRECTORY_SEPARATOR
|
||||
. 'Config'
|
||||
. ($path ? DIRECTORY_SEPARATOR.$path : $path);
|
||||
. ($path ? DIRECTORY_SEPARATOR . $path : $path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
@@ -12,7 +14,7 @@ class EmailConfirmationService extends UserTokenService
|
||||
/**
|
||||
* Create new confirmation for a user,
|
||||
* Also removes any existing old ones.
|
||||
* @param User $user
|
||||
*
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
public function sendConfirmation(User $user)
|
||||
@@ -29,9 +31,8 @@ class EmailConfirmationService extends UserTokenService
|
||||
|
||||
/**
|
||||
* Check if confirmation is required in this instance.
|
||||
* @return bool
|
||||
*/
|
||||
public function confirmationRequired() : bool
|
||||
public function confirmationRequired(): bool
|
||||
{
|
||||
return setting('registration-confirmation')
|
||||
|| setting('registration-restrict');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ExternalAuthService
|
||||
{
|
||||
@@ -19,6 +19,7 @@ class ExternalAuthService
|
||||
}
|
||||
|
||||
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
|
||||
|
||||
return in_array($roleName, $groupNames);
|
||||
}
|
||||
|
||||
@@ -57,7 +58,7 @@ class ExternalAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the groups to the user roles for the current user
|
||||
* Sync the groups to the user roles for the current user.
|
||||
*/
|
||||
public function syncWithGroups(User $user, array $userGroups): void
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
||||
class ExternalBaseUserProvider implements UserProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* The user model.
|
||||
*
|
||||
@@ -17,7 +16,8 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
* @param $model
|
||||
*
|
||||
* @param $model
|
||||
*/
|
||||
public function __construct(string $model)
|
||||
{
|
||||
@@ -32,13 +32,15 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
public function createModel()
|
||||
{
|
||||
$class = '\\' . ltrim($this->model, '\\');
|
||||
return new $class;
|
||||
|
||||
return new $class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a user by their unique identifier.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @param mixed $identifier
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveById($identifier)
|
||||
@@ -49,8 +51,9 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
/**
|
||||
* Retrieve a user by their unique identifier and "remember me" token.
|
||||
*
|
||||
* @param mixed $identifier
|
||||
* @param string $token
|
||||
* @param mixed $identifier
|
||||
* @param string $token
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByToken($identifier, $token)
|
||||
@@ -58,12 +61,12 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the "remember me" token for the given user in storage.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param string $token
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param string $token
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateRememberToken(Authenticatable $user, $token)
|
||||
@@ -74,13 +77,15 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
/**
|
||||
* Retrieve a user by the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|null
|
||||
*/
|
||||
public function retrieveByCredentials(array $credentials)
|
||||
{
|
||||
// Search current user base by looking up a uid
|
||||
$model = $this->createModel();
|
||||
|
||||
return $model->newQuery()
|
||||
->where('external_auth_id', $credentials['external_auth_id'])
|
||||
->first();
|
||||
@@ -89,8 +94,9 @@ class ExternalBaseUserProvider implements UserProvider
|
||||
/**
|
||||
* Validate a user against the given credentials.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param array $credentials
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validateCredentials(Authenticatable $user, array $credentials)
|
||||
|
||||
@@ -15,8 +15,6 @@ use Illuminate\Contracts\Session\Session;
|
||||
* guard with 'remember' functionality removed. Basic auth and event emission
|
||||
* has also been removed to keep this simple. Designed to be extended by external
|
||||
* Auth Guards.
|
||||
*
|
||||
* @package Illuminate\Auth
|
||||
*/
|
||||
class ExternalBaseSessionGuard implements StatefulGuard
|
||||
{
|
||||
@@ -86,7 +84,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
// If we've already retrieved the user for the current request we can just
|
||||
// return it back immediately. We do not want to fetch the user data on
|
||||
// every call to this method because that would be tremendously slow.
|
||||
if (! is_null($this->user)) {
|
||||
if (!is_null($this->user)) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
@@ -94,7 +92,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
|
||||
// First we will try to load the user using the
|
||||
// identifier in the session if one exists.
|
||||
if (! is_null($id)) {
|
||||
if (!is_null($id)) {
|
||||
$this->user = $this->provider->retrieveById($id);
|
||||
}
|
||||
|
||||
@@ -120,7 +118,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Log a user into the application without sessions or cookies.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function once(array $credentials = [])
|
||||
@@ -137,12 +136,13 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Log the given user ID into the application without sessions or cookies.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param mixed $id
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||
*/
|
||||
public function onceUsingId($id)
|
||||
{
|
||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
||||
if (!is_null($user = $this->provider->retrieveById($id))) {
|
||||
$this->setUser($user);
|
||||
|
||||
return $user;
|
||||
@@ -154,7 +154,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
@@ -162,12 +163,12 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function attempt(array $credentials = [], $remember = false)
|
||||
@@ -178,26 +179,24 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Log the given user ID into the application.
|
||||
*
|
||||
* @param mixed $id
|
||||
* @param bool $remember
|
||||
* @param mixed $id
|
||||
* @param bool $remember
|
||||
*
|
||||
* @return \Illuminate\Contracts\Auth\Authenticatable|false
|
||||
*/
|
||||
public function loginUsingId($id, $remember = false)
|
||||
{
|
||||
if (! is_null($user = $this->provider->retrieveById($id))) {
|
||||
$this->login($user, $remember);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Always return false as to disable this method,
|
||||
// Logins should route through LoginService.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a user into the application.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param bool $remember
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param bool $remember
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function login(AuthenticatableContract $user, $remember = false)
|
||||
@@ -210,7 +209,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Update the session with the given ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function updateSession($id)
|
||||
@@ -264,7 +264,7 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'login_'.$this->name.'_'.sha1(static::class);
|
||||
return 'login_' . $this->name . '_' . sha1(static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,7 +290,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
/**
|
||||
* Set the current user.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
* @param \Illuminate\Contracts\Auth\Authenticatable $user
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setUser(AuthenticatableContract $user)
|
||||
@@ -301,5 +302,4 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,31 +5,28 @@ namespace BookStack\Auth\Access\Guards;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\RegistrationService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
use Illuminate\Contracts\Session\Session;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
|
||||
protected $ldapService;
|
||||
|
||||
/**
|
||||
* LdapSessionGuard constructor.
|
||||
*/
|
||||
public function __construct($name,
|
||||
public function __construct(
|
||||
$name,
|
||||
UserProvider $provider,
|
||||
Session $session,
|
||||
LdapService $ldapService,
|
||||
RegistrationService $registrationService
|
||||
)
|
||||
{
|
||||
) {
|
||||
$this->ldapService = $ldapService;
|
||||
parent::__construct($name, $provider, $session, $registrationService);
|
||||
}
|
||||
@@ -38,8 +35,10 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*
|
||||
* @throws LdapException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
{
|
||||
@@ -47,7 +46,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
|
||||
if (isset($userDetails['uid'])) {
|
||||
$this->lastAttempted = $this->provider->retrieveByCredentials([
|
||||
'external_auth_id' => $userDetails['uid']
|
||||
'external_auth_id' => $userDetails['uid'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -58,10 +57,12 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @return bool
|
||||
* @param bool $remember
|
||||
*
|
||||
* @throws LoginAttemptException
|
||||
* @throws LdapException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function attempt(array $credentials = [], $remember = false)
|
||||
{
|
||||
@@ -71,7 +72,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
$user = null;
|
||||
if (isset($userDetails['uid'])) {
|
||||
$this->lastAttempted = $user = $this->provider->retrieveByCredentials([
|
||||
'external_auth_id' => $userDetails['uid']
|
||||
'external_auth_id' => $userDetails['uid'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -92,12 +93,19 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
$this->ldapService->syncGroups($user, $username);
|
||||
}
|
||||
|
||||
// Attach avatar if non-existent
|
||||
if (is_null($user->avatar)) {
|
||||
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
|
||||
}
|
||||
|
||||
$this->login($user, $remember);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user from the given ldap credentials and login credentials
|
||||
* Create a new user from the given ldap credentials and login credentials.
|
||||
*
|
||||
* @throws LoginAttemptEmailNeededException
|
||||
* @throws LoginAttemptException
|
||||
* @throws UserRegistrationException
|
||||
@@ -111,13 +119,15 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
}
|
||||
|
||||
$details = [
|
||||
'name' => $ldapUserDetails['name'],
|
||||
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
||||
'name' => $ldapUserDetails['name'],
|
||||
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
||||
'external_auth_id' => $ldapUserDetails['uid'],
|
||||
'password' => Str::random(32),
|
||||
'password' => Str::random(32),
|
||||
];
|
||||
|
||||
return $this->registrationService->registerUser($details, null, false);
|
||||
}
|
||||
$user = $this->registrationService->registerUser($details, null, false);
|
||||
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
namespace BookStack\Auth\Access\Guards;
|
||||
|
||||
/**
|
||||
* Saml2 Session Guard
|
||||
* Saml2 Session Guard.
|
||||
*
|
||||
* The saml2 login process is async in nature meaning it does not fit very well
|
||||
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
|
||||
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
|
||||
* version of SessionGuard.
|
||||
*
|
||||
* @package BookStack\Auth\Access\Guards
|
||||
*/
|
||||
class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||
{
|
||||
@@ -18,6 +16,7 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||
* Validate a user's credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function validate(array $credentials = [])
|
||||
@@ -29,12 +28,12 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
|
||||
* Attempt to authenticate a user using the given credentials.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @param bool $remember
|
||||
* @param bool $remember
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function attempt(array $credentials = [], $remember = false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -22,9 +24,11 @@ class Ldap
|
||||
|
||||
/**
|
||||
* Set the value of a LDAP option for the given connection.
|
||||
*
|
||||
* @param resource $ldapConnection
|
||||
* @param int $option
|
||||
* @param mixed $value
|
||||
* @param int $option
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setOption($ldapConnection, $option, $value)
|
||||
@@ -32,10 +36,20 @@ class Ldap
|
||||
return ldap_set_option($ldapConnection, $option, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start TLS on the given LDAP connection.
|
||||
*/
|
||||
public function startTls($ldapConnection): bool
|
||||
{
|
||||
return ldap_start_tls($ldapConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the version number for the given ldap connection.
|
||||
*
|
||||
* @param $ldapConnection
|
||||
* @param $version
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function setVersion($ldapConnection, $version)
|
||||
@@ -45,10 +59,12 @@ class Ldap
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -58,8 +74,10 @@ class Ldap
|
||||
|
||||
/**
|
||||
* Get entries from an ldap search result.
|
||||
*
|
||||
* @param resource $ldapConnection
|
||||
* @param resource $ldapSearchResult
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEntries($ldapConnection, $ldapSearchResult)
|
||||
@@ -69,23 +87,28 @@ class Ldap
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -95,8 +118,10 @@ class Ldap
|
||||
|
||||
/**
|
||||
* Explode a LDAP dn string into an array of components.
|
||||
*
|
||||
* @param string $dn
|
||||
* @param int $withAttrib
|
||||
* @param int $withAttrib
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function explodeDn(string $dn, int $withAttrib)
|
||||
@@ -106,12 +131,14 @@ class Ldap
|
||||
|
||||
/**
|
||||
* Escape a string for use in an LDAP filter.
|
||||
*
|
||||
* @param string $value
|
||||
* @param string $ignore
|
||||
* @param int $flags
|
||||
* @param int $flags
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function escape(string $value, string $ignore = "", int $flags = 0)
|
||||
public function escape(string $value, string $ignore = '', int $flags = 0)
|
||||
{
|
||||
return ldap_escape($value, $ignore, $flags);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use ErrorException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
@@ -11,24 +15,26 @@ use ErrorException;
|
||||
*/
|
||||
class LdapService extends ExternalAuthService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $userAvatars;
|
||||
protected $config;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->userAvatars = $userAvatars;
|
||||
$this->config = config('services.ldap');
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if groups should be synced.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSyncGroups()
|
||||
@@ -38,6 +44,7 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Search for attributes for a specific user on the ldap.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getUserWithAttributes(string $userName, array $attributes): ?array
|
||||
@@ -69,6 +76,7 @@ class LdapService extends ExternalAuthService
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails(string $userName): ?array
|
||||
@@ -76,10 +84,13 @@ class LdapService extends ExternalAuthService
|
||||
$idAttr = $this->config['id_attribute'];
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$displayNameAttr = $this->config['display_name_attribute'];
|
||||
$thumbnailAttr = $this->config['thumbnail_attribute'];
|
||||
|
||||
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
|
||||
$user = $this->getUserWithAttributes($userName, array_filter([
|
||||
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
|
||||
]));
|
||||
|
||||
if ($user === null) {
|
||||
if (is_null($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,11 +100,12 @@ class LdapService extends ExternalAuthService
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
||||
];
|
||||
|
||||
if ($this->config['dump_user_details']) {
|
||||
throw new JsonDebugException([
|
||||
'details_from_ldap' => $user,
|
||||
'details_from_ldap' => $user,
|
||||
'details_bookstack_parsed' => $formatted,
|
||||
]);
|
||||
}
|
||||
@@ -129,6 +141,7 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Check if the given credentials are valid for the given user.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(?array $ldapUserDetails, string $password): bool
|
||||
@@ -138,6 +151,7 @@ class LdapService extends ExternalAuthService
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUserDetails['dn'], $password);
|
||||
} catch (ErrorException $e) {
|
||||
@@ -150,7 +164,9 @@ class LdapService extends ExternalAuthService
|
||||
/**
|
||||
* 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)
|
||||
@@ -173,8 +189,10 @@ class LdapService extends ExternalAuthService
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
*
|
||||
* @throws LdapException
|
||||
*
|
||||
* @return resource
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
@@ -187,8 +205,8 @@ class LdapService extends ExternalAuthService
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
||||
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
|
||||
// Disable certificate verification.
|
||||
// This option works globally and must be set before a connection is created.
|
||||
if ($this->config['tls_insecure']) {
|
||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||
}
|
||||
@@ -205,7 +223,16 @@ class LdapService extends ExternalAuthService
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
// Start and verify TLS if it's enabled
|
||||
if ($this->config['start_tls']) {
|
||||
$started = $this->ldap->startTls($ldapConnection);
|
||||
if (!$started) {
|
||||
throw new LdapException('Could not start TLS connection');
|
||||
}
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
@@ -225,6 +252,7 @@ class LdapService extends ExternalAuthService
|
||||
// Otherwise, extract the port out
|
||||
$hostName = $serverNameParts[0];
|
||||
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
||||
|
||||
return ['host' => $hostName, 'port' => $ldapPort];
|
||||
}
|
||||
|
||||
@@ -238,11 +266,13 @@ class LdapService extends ExternalAuthService
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $this->ldap->escape($attrText);
|
||||
}
|
||||
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the groups a user is a part of on ldap.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserGroups(string $userName): array
|
||||
@@ -256,11 +286,13 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
$userGroups = $this->groupFilter($user);
|
||||
$userGroups = $this->getGroupsRecursive($userGroups, []);
|
||||
|
||||
return $userGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent groups of an array of groups.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupsRecursive(array $groupsArray, array $checked): array
|
||||
@@ -287,6 +319,7 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Get the parent groups of a single group.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupGroups(string $groupName): array
|
||||
@@ -320,7 +353,7 @@ class LdapService extends ExternalAuthService
|
||||
$count = 0;
|
||||
|
||||
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
|
||||
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
|
||||
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
@@ -335,6 +368,7 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Sync the LDAP groups to the user roles for the current user.
|
||||
*
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function syncGroups(User $user, string $username)
|
||||
@@ -342,4 +376,22 @@ class LdapService extends ExternalAuthService
|
||||
$userLdapGroups = $this->getUserGroups($username);
|
||||
$this->syncWithGroups($user, $userLdapGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and attach an avatar image, if found in the ldap details, and attach
|
||||
* to the given user model.
|
||||
*/
|
||||
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
|
||||
{
|
||||
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = $ldapUserDetails['avatar'];
|
||||
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
|
||||
} catch (\Exception $exception) {
|
||||
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
app/Auth/Access/LoginService.php
Normal file
164
app/Auth/Access/LoginService.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Access\Mfa\MfaSession;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
|
||||
class LoginService
|
||||
{
|
||||
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
||||
|
||||
protected $mfaSession;
|
||||
protected $emailConfirmationService;
|
||||
|
||||
public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
|
||||
{
|
||||
$this->mfaSession = $mfaSession;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the given user into the system.
|
||||
* Will start a login of the given user but will prevent if there's
|
||||
* a reason to (MFA or Unconfirmed Email).
|
||||
* Returns a boolean to indicate the current login result.
|
||||
*
|
||||
* @throws StoppedAuthenticationException
|
||||
*/
|
||||
public function login(User $user, string $method, bool $remember = false): void
|
||||
{
|
||||
if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
|
||||
$this->setLastLoginAttemptedForUser($user, $method, $remember);
|
||||
|
||||
throw new StoppedAuthenticationException($user, $this);
|
||||
}
|
||||
|
||||
$this->clearLastLoginAttempted();
|
||||
auth()->login($user, $remember);
|
||||
Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}");
|
||||
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
|
||||
|
||||
// Authenticate on all session guards if a likely admin
|
||||
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
|
||||
$guards = ['standard', 'ldap', 'saml2'];
|
||||
foreach ($guards as $guard) {
|
||||
auth($guard)->login($user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reattempt a system login after a previous stopped attempt.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reattemptLoginFor(User $user)
|
||||
{
|
||||
if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
|
||||
throw new Exception('Login reattempt user does align with current session state');
|
||||
}
|
||||
|
||||
$lastLoginDetails = $this->getLastLoginAttemptDetails();
|
||||
$this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last user that was attempted to be logged in.
|
||||
* Only exists if the last login attempt had correct credentials
|
||||
* but had been prevented by a secondary factor.
|
||||
*/
|
||||
public function getLastLoginAttemptUser(): ?User
|
||||
{
|
||||
$id = $this->getLastLoginAttemptDetails()['user_id'];
|
||||
|
||||
return User::query()->where('id', '=', $id)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of the last login attempt.
|
||||
* Checks upon a ttl of about 1 hour since that last attempted login.
|
||||
*
|
||||
* @return array{user_id: ?string, method: ?string, remember: bool}
|
||||
*/
|
||||
protected function getLastLoginAttemptDetails(): array
|
||||
{
|
||||
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||
if (!$value) {
|
||||
return ['user_id' => null, 'method' => null];
|
||||
}
|
||||
|
||||
[$id, $method, $remember, $time] = explode(':', $value);
|
||||
$hourAgo = time() - (60 * 60);
|
||||
if ($time < $hourAgo) {
|
||||
$this->clearLastLoginAttempted();
|
||||
|
||||
return ['user_id' => null, 'method' => null];
|
||||
}
|
||||
|
||||
return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last login attempted user.
|
||||
* Must be only used when credentials are correct and a login could be
|
||||
* achieved but a secondary factor has stopped the login.
|
||||
*/
|
||||
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
|
||||
{
|
||||
session()->put(
|
||||
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
|
||||
implode(':', [$user->id, $method, $remember, time()])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the last login attempted session value.
|
||||
*/
|
||||
protected function clearLastLoginAttempted(): void
|
||||
{
|
||||
session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if MFA verification is needed.
|
||||
*/
|
||||
public function needsMfaVerification(User $user): bool
|
||||
{
|
||||
return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given user is awaiting email confirmation.
|
||||
*/
|
||||
public function awaitingEmailConfirmation(User $user): bool
|
||||
{
|
||||
return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt the login of a user using the given credentials.
|
||||
* Meant to mirror Laravel's default guard 'attempt' method
|
||||
* but in a manner that always routes through our login system.
|
||||
* May interrupt the flow if extra authentication requirements are imposed.
|
||||
*
|
||||
* @throws StoppedAuthenticationException
|
||||
*/
|
||||
public function attempt(array $credentials, string $method, bool $remember = false): bool
|
||||
{
|
||||
$result = auth()->attempt($credentials, $remember);
|
||||
if ($result) {
|
||||
$user = auth()->user();
|
||||
auth()->logout();
|
||||
$this->login($user, $method, $remember);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
62
app/Auth/Access/Mfa/BackupCodeService.php
Normal file
62
app/Auth/Access/Mfa/BackupCodeService.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Mfa;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BackupCodeService
|
||||
{
|
||||
/**
|
||||
* Generate a new set of 16 backup codes.
|
||||
*/
|
||||
public function generateNewSet(): array
|
||||
{
|
||||
$codes = [];
|
||||
while (count($codes) < 16) {
|
||||
$code = Str::random(5) . '-' . Str::random(5);
|
||||
if (!in_array($code, $codes)) {
|
||||
$codes[] = strtolower($code);
|
||||
}
|
||||
}
|
||||
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given code matches one of the available options.
|
||||
*/
|
||||
public function inputCodeExistsInSet(string $code, string $codeSet): bool
|
||||
{
|
||||
$cleanCode = $this->cleanInputCode($code);
|
||||
$codes = json_decode($codeSet);
|
||||
|
||||
return in_array($cleanCode, $codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given input code from the given available options.
|
||||
* Will return a JSON string containing the codes.
|
||||
*/
|
||||
public function removeInputCodeFromSet(string $code, string $codeSet): string
|
||||
{
|
||||
$cleanCode = $this->cleanInputCode($code);
|
||||
$codes = json_decode($codeSet);
|
||||
$pos = array_search($cleanCode, $codes, true);
|
||||
array_splice($codes, $pos, 1);
|
||||
|
||||
return json_encode($codes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of codes in the given set.
|
||||
*/
|
||||
public function countCodesInSet(string $codeSet): int
|
||||
{
|
||||
return count(json_decode($codeSet));
|
||||
}
|
||||
|
||||
protected function cleanInputCode(string $code): string
|
||||
{
|
||||
return strtolower(str_replace(' ', '-', trim($code)));
|
||||
}
|
||||
}
|
||||
60
app/Auth/Access/Mfa/MfaSession.php
Normal file
60
app/Auth/Access/Mfa/MfaSession.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Mfa;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
|
||||
class MfaSession
|
||||
{
|
||||
/**
|
||||
* Check if MFA is required for the given user.
|
||||
*/
|
||||
public function isRequiredForUser(User $user): bool
|
||||
{
|
||||
// TODO - Test both these cases
|
||||
return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given user is pending MFA setup.
|
||||
* (MFA required but not yet configured).
|
||||
*/
|
||||
public function isPendingMfaSetup(User $user): bool
|
||||
{
|
||||
return $this->isRequiredForUser($user) && !$user->mfaValues()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a role of the given user enforces MFA.
|
||||
*/
|
||||
protected function userRoleEnforcesMfa(User $user): bool
|
||||
{
|
||||
return $user->roles()
|
||||
->where('mfa_enforced', '=', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current MFA session has already been verified for the given user.
|
||||
*/
|
||||
public function isVerifiedForUser(User $user): bool
|
||||
{
|
||||
return session()->get($this->getMfaVerifiedSessionKey($user)) === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the current session as MFA-verified.
|
||||
*/
|
||||
public function markVerifiedForUser(User $user): void
|
||||
{
|
||||
session()->put($this->getMfaVerifiedSessionKey($user), 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session key in which the MFA verification status is stored.
|
||||
*/
|
||||
protected function getMfaVerifiedSessionKey(User $user): string
|
||||
{
|
||||
return 'mfa-verification-passed:' . $user->id;
|
||||
}
|
||||
}
|
||||
76
app/Auth/Access/Mfa/MfaValue.php
Normal file
76
app/Auth/Access/Mfa/MfaValue.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Mfa;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property string $method
|
||||
* @property string $value
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
*/
|
||||
class MfaValue extends Model
|
||||
{
|
||||
protected static $unguarded = true;
|
||||
|
||||
const METHOD_TOTP = 'totp';
|
||||
const METHOD_BACKUP_CODES = 'backup_codes';
|
||||
|
||||
/**
|
||||
* Get all the MFA methods available.
|
||||
*/
|
||||
public static function allMethods(): array
|
||||
{
|
||||
return [self::METHOD_TOTP, self::METHOD_BACKUP_CODES];
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a new MFA value for the given user and method
|
||||
* using the provided value.
|
||||
*/
|
||||
public static function upsertWithValue(User $user, string $method, string $value): void
|
||||
{
|
||||
/** @var MfaValue $mfaVal */
|
||||
$mfaVal = static::query()->firstOrNew([
|
||||
'user_id' => $user->id,
|
||||
'method' => $method,
|
||||
]);
|
||||
$mfaVal->setValue($value);
|
||||
$mfaVal->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Easily get the decrypted MFA value for the given user and method.
|
||||
*/
|
||||
public static function getValueForUser(User $user, string $method): ?string
|
||||
{
|
||||
/** @var MfaValue $mfaVal */
|
||||
$mfaVal = static::query()
|
||||
->where('user_id', '=', $user->id)
|
||||
->where('method', '=', $method)
|
||||
->first();
|
||||
|
||||
return $mfaVal ? $mfaVal->getValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the value attribute upon access.
|
||||
*/
|
||||
protected function getValue(): string
|
||||
{
|
||||
return decrypt($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the value attribute upon access.
|
||||
*/
|
||||
protected function setValue($value): void
|
||||
{
|
||||
$this->value = encrypt($value);
|
||||
}
|
||||
}
|
||||
72
app/Auth/Access/Mfa/TotpService.php
Normal file
72
app/Auth/Access/Mfa/TotpService.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Mfa;
|
||||
|
||||
use BaconQrCode\Renderer\Color\Rgb;
|
||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
use BaconQrCode\Renderer\ImageRenderer;
|
||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||
use BaconQrCode\Writer;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use PragmaRX\Google2FA\Support\Constants;
|
||||
|
||||
class TotpService
|
||||
{
|
||||
protected $google2fa;
|
||||
|
||||
public function __construct(Google2FA $google2fa)
|
||||
{
|
||||
$this->google2fa = $google2fa;
|
||||
// Use SHA1 as a default, Personal testing of other options in 2021 found
|
||||
// many apps lack support for other algorithms yet still will scan
|
||||
// the code causing a confusing UX.
|
||||
$this->google2fa->setAlgorithm(Constants::SHA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new totp secret key.
|
||||
*/
|
||||
public function generateSecret(): string
|
||||
{
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
return $this->google2fa->generateSecretKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a TOTP URL from secret key.
|
||||
*/
|
||||
public function generateUrl(string $secret): string
|
||||
{
|
||||
return $this->google2fa->getQRCodeUrl(
|
||||
setting('app-name'),
|
||||
user()->email,
|
||||
$secret
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a QR code to display a TOTP URL.
|
||||
*/
|
||||
public function generateQrCodeSvg(string $url): string
|
||||
{
|
||||
$color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167));
|
||||
|
||||
return (new Writer(
|
||||
new ImageRenderer(
|
||||
new RendererStyle(192, 0, null, null, $color),
|
||||
new SvgImageBackEnd()
|
||||
)
|
||||
))->writeString($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the user provided code is valid for the secret.
|
||||
* The secret must be known, not user-provided.
|
||||
*/
|
||||
public function verifyCode(string $code, string $secret): bool
|
||||
{
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
return $this->google2fa->verifyKey($secret, $code);
|
||||
}
|
||||
}
|
||||
37
app/Auth/Access/Mfa/TotpValidationRule.php
Normal file
37
app/Auth/Access/Mfa/TotpValidationRule.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access\Mfa;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class TotpValidationRule implements Rule
|
||||
{
|
||||
protected $secret;
|
||||
protected $totpService;
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
* Takes the TOTP secret that must be system provided, not user provided.
|
||||
*/
|
||||
public function __construct(string $secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->totpService = app()->make(TotpService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
return $this->totpService->verifyCode($value, $this->secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return trans('validation.totp');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Exception;
|
||||
|
||||
class RegistrationService
|
||||
{
|
||||
|
||||
protected $userRepo;
|
||||
protected $emailConfirmationService;
|
||||
|
||||
@@ -23,6 +28,7 @@ class RegistrationService
|
||||
|
||||
/**
|
||||
* Check whether or not registrations are allowed in the app settings.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function ensureRegistrationAllowed()
|
||||
@@ -40,11 +46,13 @@ class RegistrationService
|
||||
{
|
||||
$authMethod = config('auth.method');
|
||||
$authMethodsWithRegistration = ['standard'];
|
||||
|
||||
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* The registrations flow for all users.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
||||
@@ -68,6 +76,9 @@ class RegistrationService
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
|
||||
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser);
|
||||
|
||||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
$newUser->save();
|
||||
@@ -77,9 +88,9 @@ class RegistrationService
|
||||
session()->flash('sent-email-confirmation', true);
|
||||
} catch (Exception $e) {
|
||||
$message = trans('auth.email_confirm_send_error');
|
||||
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $newUser;
|
||||
@@ -88,6 +99,7 @@ class RegistrationService
|
||||
/**
|
||||
* Ensure that the given email meets any active email domain registration restrictions.
|
||||
* Throws if restrictions are active and the email does not match an allowed domain.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function ensureEmailDomainAllowed(string $userEmail): void
|
||||
@@ -99,11 +111,11 @@ class RegistrationService
|
||||
}
|
||||
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1);
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
||||
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\SamlException;
|
||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -19,34 +22,37 @@ class Saml2Service extends ExternalAuthService
|
||||
{
|
||||
protected $config;
|
||||
protected $registrationService;
|
||||
protected $user;
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* Saml2Service constructor.
|
||||
*/
|
||||
public function __construct(RegistrationService $registrationService, User $user)
|
||||
public function __construct(RegistrationService $registrationService, LoginService $loginService)
|
||||
{
|
||||
$this->config = config('saml2');
|
||||
$this->registrationService = $registrationService;
|
||||
$this->user = $user;
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a login flow.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function login(): array
|
||||
{
|
||||
$toolKit = $this->getToolkit();
|
||||
$returnRoute = url('/saml2/acs');
|
||||
|
||||
return [
|
||||
'url' => $toolKit->login($returnRoute, [], false, false, true),
|
||||
'id' => $toolKit->getLastRequestID(),
|
||||
'id' => $toolKit->getLastRequestID(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a logout flow.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function logout(): array
|
||||
@@ -74,6 +80,7 @@ class Saml2Service extends ExternalAuthService
|
||||
* Process the ACS response from the idp and return the
|
||||
* matching, or new if registration active, user matched to the idp.
|
||||
* Returns null if not authenticated.
|
||||
*
|
||||
* @throws Error
|
||||
* @throws SamlException
|
||||
* @throws ValidationError
|
||||
@@ -88,7 +95,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new Error(
|
||||
'Invalid ACS Response: '.implode(', ', $errors)
|
||||
'Invalid ACS Response: ' . implode(', ', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +111,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Process a response for the single logout service.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function processSlsResponse(?string $requestId): ?string
|
||||
@@ -115,11 +123,12 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new Error(
|
||||
'Invalid SLS Response: '.implode(', ', $errors)
|
||||
'Invalid SLS Response: ' . implode(', ', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
$this->actionLogout();
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
@@ -134,6 +143,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Get the metadata for this service provider.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public function metadata(): string
|
||||
@@ -145,7 +155,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new Error(
|
||||
'Invalid SP metadata: '.implode(', ', $errors),
|
||||
'Invalid SP metadata: ' . implode(', ', $errors),
|
||||
Error::METADATA_SP_INVALID
|
||||
);
|
||||
}
|
||||
@@ -155,6 +165,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Load the underlying Onelogin SAML2 toolkit.
|
||||
*
|
||||
* @throws Error
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -174,6 +185,7 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
$spSettings = $this->loadOneloginServiceProviderDetails();
|
||||
$settings = array_replace_recursive($settings, $spSettings, $metaDataSettings, $overrides);
|
||||
|
||||
return new Auth($settings);
|
||||
}
|
||||
|
||||
@@ -183,18 +195,18 @@ class Saml2Service extends ExternalAuthService
|
||||
protected function loadOneloginServiceProviderDetails(): array
|
||||
{
|
||||
$spDetails = [
|
||||
'entityId' => url('/saml2/metadata'),
|
||||
'entityId' => url('/saml2/metadata'),
|
||||
'assertionConsumerService' => [
|
||||
'url' => url('/saml2/acs'),
|
||||
],
|
||||
'singleLogoutService' => [
|
||||
'url' => url('/saml2/sls')
|
||||
'url' => url('/saml2/sls'),
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'baseurl' => url('/saml2'),
|
||||
'sp' => $spDetails
|
||||
'sp' => $spDetails,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -207,7 +219,7 @@ class Saml2Service extends ExternalAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the display name
|
||||
* Calculate the display name.
|
||||
*/
|
||||
protected function getUserDisplayName(array $samlAttributes, string $defaultValue): string
|
||||
{
|
||||
@@ -257,9 +269,9 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
return [
|
||||
'external_id' => $externalId,
|
||||
'name' => $this->getUserDisplayName($samlAttributes, $externalId),
|
||||
'email' => $email,
|
||||
'saml_id' => $samlID,
|
||||
'name' => $this->getUserDisplayName($samlAttributes, $externalId),
|
||||
'email' => $email,
|
||||
'saml_id' => $samlID,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -293,6 +305,7 @@ class Saml2Service extends ExternalAuthService
|
||||
$data = $data[0];
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -311,19 +324,20 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
/**
|
||||
* Get the user from the database for the specified details.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function getOrRegisterUser(array $userDetails): ?User
|
||||
{
|
||||
$user = $this->user->newQuery()
|
||||
$user = User::query()
|
||||
->where('external_auth_id', '=', $userDetails['external_id'])
|
||||
->first();
|
||||
|
||||
if (is_null($user)) {
|
||||
$userData = [
|
||||
'name' => $userDetails['name'],
|
||||
'email' => $userDetails['email'],
|
||||
'password' => Str::random(32),
|
||||
'name' => $userDetails['name'],
|
||||
'email' => $userDetails['email'],
|
||||
'password' => Str::random(32),
|
||||
'external_auth_id' => $userDetails['external_id'],
|
||||
];
|
||||
|
||||
@@ -336,9 +350,11 @@ class Saml2Service extends ExternalAuthService
|
||||
/**
|
||||
* Process the SAML response for a user. Login the user when
|
||||
* they exist, optionally registering them automatically.
|
||||
*
|
||||
* @throws SamlException
|
||||
* @throws JsonDebugException
|
||||
* @throws UserRegistrationException
|
||||
* @throws StoppedAuthenticationException
|
||||
*/
|
||||
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
||||
{
|
||||
@@ -347,8 +363,8 @@ class Saml2Service extends ExternalAuthService
|
||||
|
||||
if ($this->config['dump_user_details']) {
|
||||
throw new JsonDebugException([
|
||||
'id_from_idp' => $samlID,
|
||||
'attrs_from_idp' => $samlAttributes,
|
||||
'id_from_idp' => $samlID,
|
||||
'attrs_from_idp' => $samlAttributes,
|
||||
'attrs_after_parsing' => $userDetails,
|
||||
]);
|
||||
}
|
||||
@@ -371,7 +387,8 @@ class Saml2Service extends ExternalAuthService
|
||||
$this->syncWithGroups($user, $groups);
|
||||
}
|
||||
|
||||
auth()->login($user);
|
||||
$this->loginService->login($user, 'saml2');
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,110 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use Laravel\Socialite\Contracts\Provider;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
|
||||
protected $userRepo;
|
||||
/**
|
||||
* The core socialite library used.
|
||||
*
|
||||
* @var Socialite
|
||||
*/
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
|
||||
/**
|
||||
* @var LoginService
|
||||
*/
|
||||
protected $loginService;
|
||||
|
||||
/**
|
||||
* The default built-in social drivers we support.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $validSocialDrivers = [
|
||||
'google',
|
||||
'github',
|
||||
'facebook',
|
||||
'slack',
|
||||
'twitter',
|
||||
'azure',
|
||||
'okta',
|
||||
'gitlab',
|
||||
'twitch',
|
||||
'discord',
|
||||
];
|
||||
|
||||
/**
|
||||
* Callbacks to run when configuring a social driver
|
||||
* for an initial redirect action.
|
||||
* Array is keyed by social driver name.
|
||||
* Callbacks are passed an instance of the driver.
|
||||
*
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
protected $configureForRedirectCallbacks = [];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
|
||||
public function __construct(Socialite $socialite, LoginService $loginService)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
$this->socialite = $socialite;
|
||||
$this->socialAccount = $socialAccount;
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the social login path.
|
||||
*
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function startLogIn(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the social registration process
|
||||
* Start the social registration process.
|
||||
*
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function startRegister(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the social registration process on callback.
|
||||
*
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
||||
{
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
|
||||
$email = $socialUser->getEmail();
|
||||
|
||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||
}
|
||||
|
||||
@@ -72,16 +113,19 @@ class SocialAuthService
|
||||
|
||||
/**
|
||||
* Get the social user details via the social driver.
|
||||
*
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function getSocialUser(string $socialDriver): SocialUser
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
|
||||
return $this->socialite->driver($driver)->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the login process on a oAuth callback.
|
||||
*
|
||||
* @throws SocialSignInAccountNotUsed
|
||||
*/
|
||||
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
||||
@@ -89,7 +133,7 @@ class SocialAuthService
|
||||
$socialId = $socialUser->getId();
|
||||
|
||||
// Get any attached social accounts or users
|
||||
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
|
||||
$socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
|
||||
$isLoggedIn = auth()->check();
|
||||
$currentUser = user();
|
||||
$titleCaseDriver = Str::title($socialDriver);
|
||||
@@ -97,28 +141,32 @@ class SocialAuthService
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
if (!$isLoggedIn && $socialAccount !== null) {
|
||||
auth()->login($socialAccount->user);
|
||||
$this->loginService->login($socialAccount->user, $socialAccount);
|
||||
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// When a user is logged in but the social account does not exist,
|
||||
// Create the social account and attach it to the user & redirect to the profile page.
|
||||
if ($isLoggedIn && $socialAccount === null) {
|
||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
||||
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
||||
$currentUser->socialAccounts()->save($account);
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
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', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
return redirect($currentUser->getEditUrl());
|
||||
}
|
||||
|
||||
@@ -127,12 +175,13 @@ class SocialAuthService
|
||||
if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
|
||||
}
|
||||
|
||||
|
||||
throw new SocialSignInAccountNotUsed($message, '/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the social driver is correct and supported.
|
||||
*
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
protected function validateDriver(string $socialDriver): string
|
||||
@@ -158,6 +207,7 @@ class SocialAuthService
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -204,29 +254,27 @@ class SocialAuthService
|
||||
/**
|
||||
* Fill and return a SocialAccount from the given driver name and SocialUser.
|
||||
*/
|
||||
public function fillSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
|
||||
{
|
||||
$this->socialAccount->fill([
|
||||
return new SocialAccount([
|
||||
'driver' => $socialDriver,
|
||||
'driver_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar()
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
return $this->socialAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function detachSocialAccount(string $socialDriver)
|
||||
public function detachSocialAccount(string $socialDriver): void
|
||||
{
|
||||
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide redirect options per service for the Laravel Socialite driver
|
||||
* Provide redirect options per service for the Laravel Socialite driver.
|
||||
*/
|
||||
public function getSocialDriver(string $driverName): Provider
|
||||
protected function getDriverForRedirect(string $driverName): Provider
|
||||
{
|
||||
$driver = $this->socialite->driver($driverName);
|
||||
|
||||
@@ -237,6 +285,33 @@ class SocialAuthService
|
||||
$driver->with(['resource' => 'https://graph.windows.net']);
|
||||
}
|
||||
|
||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom socialite driver to be used.
|
||||
* Driver name should be lower_snake_case.
|
||||
* Config array should mirror the structure of a service
|
||||
* within the `Config/services.php` file.
|
||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||
*/
|
||||
public function addSocialDriver(
|
||||
string $driverName,
|
||||
array $config,
|
||||
string $socialiteHandler,
|
||||
callable $configureForRedirect = null
|
||||
) {
|
||||
$this->validSocialDrivers[] = $driverName;
|
||||
config()->set('services.' . $driverName, $config);
|
||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||
if (!is_null($configureForRedirect)) {
|
||||
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Notifications\UserInvite;
|
||||
@@ -11,6 +13,7 @@ class UserInviteService extends UserTokenService
|
||||
/**
|
||||
* Send an invitation to a user to sign into BookStack
|
||||
* Removes existing invitation tokens.
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
public function sendInvitation(User $user)
|
||||
|
||||
@@ -1,59 +1,56 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Connection as Database;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use stdClass;
|
||||
|
||||
class UserTokenService
|
||||
{
|
||||
|
||||
/**
|
||||
* Name of table where user tokens are stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tokenTable = 'user_tokens';
|
||||
|
||||
/**
|
||||
* Token expiry time in hours.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $expiryTime = 24;
|
||||
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* UserTokenService constructor.
|
||||
* @param Database $db
|
||||
*/
|
||||
public function __construct(Database $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteByUser(User $user)
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
return DB::table($this->tokenTable)
|
||||
->where('user_id', '=', $user->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user id from a token, while check the token exists and has not expired.
|
||||
*
|
||||
* @param string $token
|
||||
* @return int
|
||||
*
|
||||
* @throws UserTokenNotFoundException
|
||||
* @throws UserTokenExpiredException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function checkTokenAndGetUserId(string $token) : int
|
||||
public function checkTokenAndGetUserId(string $token): int
|
||||
{
|
||||
$entry = $this->getEntryByToken($token);
|
||||
|
||||
@@ -70,63 +67,74 @@ class UserTokenService
|
||||
|
||||
/**
|
||||
* Creates a unique token within the email confirmation database.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateToken() : string
|
||||
protected function generateToken(): string
|
||||
{
|
||||
$token = Str::random(24);
|
||||
while ($this->tokenExists($token)) {
|
||||
$token = Str::random(25);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and store a token for the given user.
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function createTokenForUser(User $user) : string
|
||||
protected function createTokenForUser(User $user): string
|
||||
{
|
||||
$token = $this->generateToken();
|
||||
$this->db->table($this->tokenTable)->insert([
|
||||
'user_id' => $user->id,
|
||||
'token' => $token,
|
||||
DB::table($this->tokenTable)->insert([
|
||||
'user_id' => $user->id,
|
||||
'token' => $token,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()
|
||||
'updated_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given token exists.
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function tokenExists(string $token) : bool
|
||||
protected function tokenExists(string $token): bool
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
return DB::table($this->tokenTable)
|
||||
->where('token', '=', $token)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a token entry for the given token.
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
protected function getEntryByToken(string $token)
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
return DB::table($this->tokenTable)
|
||||
->where('token', '=', $token)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given token entry has expired.
|
||||
*
|
||||
* @param stdClass $tokenEntry
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function entryExpired(stdClass $tokenEntry) : bool
|
||||
protected function entryExpired(stdClass $tokenEntry): bool
|
||||
{
|
||||
return Carbon::now()->subHours($this->expiryTime)
|
||||
->gt(new Carbon($tokenEntry->created_at));
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class PermissionService
|
||||
{
|
||||
/**
|
||||
* @var ?array
|
||||
*/
|
||||
protected $userRoles = null;
|
||||
|
||||
protected $currentAction;
|
||||
protected $isAdminUser;
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
/**
|
||||
* @var ?User
|
||||
*/
|
||||
protected $currentUserModel = null;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
@@ -28,52 +37,20 @@ class PermissionService
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var JointPermission
|
||||
* @var array
|
||||
*/
|
||||
protected $jointPermission;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var EntityPermission
|
||||
*/
|
||||
protected $entityPermission;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Role $role
|
||||
* @param Connection $db
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(
|
||||
JointPermission $jointPermission,
|
||||
Permissions\EntityPermission $entityPermission,
|
||||
Role $role,
|
||||
Connection $db,
|
||||
EntityProvider $entityProvider
|
||||
) {
|
||||
public function __construct(Connection $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
* Set the database connection.
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
@@ -81,82 +58,65 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
* Prepare the local entity cache and ensure it's empty.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache($entities = [])
|
||||
protected function readyEntityCache(array $entities = [])
|
||||
{
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$type = $entity->getType();
|
||||
if (!isset($this->entityCache[$type])) {
|
||||
$this->entityCache[$type] = collect();
|
||||
$class = get_class($entity);
|
||||
if (!isset($this->entityCache[$class])) {
|
||||
$this->entityCache[$class] = collect();
|
||||
}
|
||||
$this->entityCache[$type]->put($entity->id, $entity);
|
||||
$this->entityCache[$class]->put($entity->id, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a book via ID, Checks local cache
|
||||
* @param $bookId
|
||||
* @return Book
|
||||
* Get a book via ID, Checks local cache.
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
protected function getBook(int $bookId): ?Book
|
||||
{
|
||||
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
|
||||
return $this->entityCache['book']->get($bookId);
|
||||
if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
|
||||
return $this->entityCache[Book::class]->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->entityProvider->book->find($bookId);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
|
||||
return $book;
|
||||
return Book::query()->withTrashed()->find($bookId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return \BookStack\Entities\Book
|
||||
* Get a chapter via ID, Checks local cache.
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
protected function getChapter(int $chapterId): ?Chapter
|
||||
{
|
||||
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
|
||||
return $this->entityCache['chapter']->get($chapterId);
|
||||
if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
|
||||
return $this->entityCache[Chapter::class]->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->entityProvider->chapter->find($chapterId);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
return Chapter::query()
|
||||
->withTrashed()
|
||||
->find($chapterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for the current user;
|
||||
* @return array|bool
|
||||
* Get the roles for the current logged in user.
|
||||
*/
|
||||
protected function getRoles()
|
||||
protected function getCurrentUserRoles(): array
|
||||
{
|
||||
if ($this->userRoles !== false) {
|
||||
if (!is_null($this->userRoles)) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
if (auth()->guest()) {
|
||||
$roles[] = $this->role->getSystemRole('public')->id;
|
||||
return $roles;
|
||||
$this->userRoles = [Role::getSystemRole('public')->id];
|
||||
} else {
|
||||
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
|
||||
}
|
||||
|
||||
|
||||
foreach ($this->currentUser()->roles as $role) {
|
||||
$roles[] = $role->id;
|
||||
}
|
||||
return $roles;
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,59 +124,59 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissions()
|
||||
{
|
||||
$this->jointPermission->truncate();
|
||||
JointPermission::query()->truncate();
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
$roles = Role::query()->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
protected function bookFetchQuery(): Builder
|
||||
{
|
||||
return $this->entityProvider->book->newQuery()
|
||||
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
return Book::query()->withTrashed()
|
||||
->select(['id', 'restricted', 'owned_by'])->with([
|
||||
'chapters' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
|
||||
},
|
||||
'pages' => function ($query) {
|
||||
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
* Build joint permissions for the given shelf and role combinations.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
$this->createManyJointPermissions($shelves->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* Build joint permissions for the given book and role combinations.
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
|
||||
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
@@ -233,55 +193,56 @@ class PermissionService
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
$this->createManyJointPermissions($entities->all(), $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @throws \Throwable
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var BookChild $entity */
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
if ($entity instanceof Page && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
if ($entity instanceof Chapter) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
$this->buildJointPermissionsForEntities($entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
public function buildJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$roles = Role::query()->get()->values()->all();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the entity jointPermissions for a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
@@ -294,7 +255,7 @@ class PermissionService
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
@@ -302,7 +263,6 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions attached to a particular role.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function deleteJointPermissionsForRole(Role $role)
|
||||
{
|
||||
@@ -311,6 +271,7 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
*
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
@@ -318,13 +279,15 @@ class PermissionService
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
JointPermission::query()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
*
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -333,17 +296,18 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
* @throws \Throwable
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
protected function deleteManyJointPermissionsForEntities(array $entities)
|
||||
{
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->transaction(function () use ($entities) {
|
||||
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
@@ -358,19 +322,21 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
* @throws \Throwable
|
||||
* Create & Save entity jointPermissions for many entities and roles.
|
||||
*
|
||||
* @param Entity[] $entities
|
||||
* @param Role[] $roles
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
protected function createManyJointPermissions(array $entities, array $roles)
|
||||
{
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
$permissionFetch = EntityPermission::query();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
@@ -411,35 +377,27 @@ class PermissionService
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
protected function getActions(Entity $entity): array
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
if ($entity instanceof Chapter || $entity instanceof Book) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity instanceof Book) {
|
||||
$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 string $action
|
||||
* @param array $permissionMap
|
||||
* @param array $rolePermissionMap
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
@@ -453,10 +411,11 @@ class PermissionService
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
if ($entity instanceof Book || $entity instanceof Bookshelf) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
@@ -466,7 +425,7 @@ class PermissionService
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
|
||||
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
@@ -485,29 +444,19 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
* @param $entityMap
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
|
||||
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
|
||||
return $entityMap[$key] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
* @param $permissionOwn
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
|
||||
{
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
@@ -516,119 +465,91 @@ class PermissionService
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
'created_by' => $entity->getRawAttribute('created_by')
|
||||
'owned_by' => $entity->getRawAttribute('owned_by'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity has a restriction set upon it.
|
||||
* @param Ownable $ownable
|
||||
* @param $permission
|
||||
* @return bool
|
||||
*
|
||||
* @param HasCreatorAndUpdater|HasOwner $ownable
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
|
||||
{
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
$user = $this->currentUser();
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// 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));
|
||||
$allPermission = $user && $user->can($permission . '-all');
|
||||
$ownPermission = $user && $user->can($permission . '-own');
|
||||
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
|
||||
$isOwner = $user && $user->id === $ownable->$ownerField;
|
||||
|
||||
return $allPermission || ($isOwner && $ownPermission);
|
||||
}
|
||||
|
||||
// Handle abnormal create jointPermissions
|
||||
if ($action === 'create') {
|
||||
$this->currentAction = $permission;
|
||||
$action = $permission;
|
||||
}
|
||||
|
||||
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
|
||||
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
|
||||
$this->clean();
|
||||
return $q;
|
||||
|
||||
return $hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
* @param string $permission
|
||||
* @param string $entityClass
|
||||
* @return bool
|
||||
*/
|
||||
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
|
||||
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
|
||||
{
|
||||
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
||||
$userId = $this->currentUser()->id;
|
||||
|
||||
$permissionQuery = $this->db->table('joint_permissions')
|
||||
$permissionQuery = JointPermission::query()
|
||||
->where('action', '=', $permission)
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where(function ($query) use ($userId) {
|
||||
$query->where('has_permission', '=', 1)
|
||||
->orWhere(function ($query2) use ($userId) {
|
||||
$query2->where('has_permission_own', '=', 1)
|
||||
->where('created_by', '=', $userId);
|
||||
});
|
||||
->where(function (Builder $query) use ($userId) {
|
||||
$this->addJointHasPermissionCheck($query, $userId);
|
||||
});
|
||||
|
||||
if (!is_null($entityClass)) {
|
||||
$entityInstance = app()->make($entityClass);
|
||||
$entityInstance = app($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
$hasPermission = $permissionQuery->count() > 0;
|
||||
$this->clean();
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param \BookStack\Entities\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;
|
||||
}
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
protected function entityRestrictionQuery(Builder $query, string $action): Builder
|
||||
{
|
||||
$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);
|
||||
});
|
||||
$q = $query->where(function ($parentQuery) use ($action) {
|
||||
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $action)
|
||||
->where(function (Builder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
@@ -639,16 +560,13 @@ class PermissionService
|
||||
public function restrictEntityQuery(Builder $query, string $ability = 'view'): Builder
|
||||
{
|
||||
$this->clean();
|
||||
|
||||
return $query->where(function (Builder $parentQuery) use ($ability) {
|
||||
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
|
||||
$permissionQuery->whereIn('role_id', $this->getRoles())
|
||||
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where('action', '=', $ability)
|
||||
->where(function (Builder $query) {
|
||||
$query->where('has_permission', '=', true)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -658,104 +576,76 @@ class PermissionService
|
||||
* Extend the given page query to ensure draft items are not visible
|
||||
* unless created by the given user.
|
||||
*/
|
||||
public function enforceDraftVisiblityOnQuery(Builder $query): Builder
|
||||
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function (Builder $query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
->where('owned_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|\BookStack\Entities\Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
* Add restrictions for a generic entity.
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
|
||||
{
|
||||
if (strtolower($entityType) === 'page') {
|
||||
if ($entity instanceof Page) {
|
||||
// Prevent drafts being visible to others.
|
||||
$query = $query->where(function ($query) {
|
||||
$query->where('draft', '=', false)
|
||||
->orWhere(function ($query) {
|
||||
$query->where('draft', '=', true)
|
||||
->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
$this->enforceDraftVisibilityOnQuery($query);
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
return $this->entityRestrictionQuery($query, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @param string $action
|
||||
* @return QueryBuilder
|
||||
*
|
||||
* @param Builder|\Illuminate\Database\Query\Builder $query
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||
{
|
||||
|
||||
$this->currentAction = $action;
|
||||
$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')
|
||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||
$permissionQuery->select(['role_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);
|
||||
});
|
||||
->where('action', '=', $action)
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where permissions are granted.
|
||||
* @param $entityType
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
* where view permissions are granted.
|
||||
*/
|
||||
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
|
||||
{
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
$morphClass = app($entityClass)->getMorphClass();
|
||||
|
||||
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
$q = $query->where(function ($query) use ($tableDetails, $morphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $morphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', $pageMorphClass)
|
||||
->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);
|
||||
});
|
||||
->where('entity_type', '=', $morphClass)
|
||||
->where('action', '=', 'view')
|
||||
->whereIn('role_id', $this->getCurrentUserRoles())
|
||||
->where(function (QueryBuilder $query) {
|
||||
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
@@ -767,12 +657,25 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return \BookStack\Auth\User
|
||||
* Add the query for checking the given user id has permission
|
||||
* within the join_permissions table.
|
||||
*
|
||||
* @param QueryBuilder|Builder $query
|
||||
*/
|
||||
private function currentUser()
|
||||
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
|
||||
{
|
||||
if ($this->currentUserModel === false) {
|
||||
$query->where('has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
|
||||
$query->where('has_permission_own', '=', true)
|
||||
->where('owned_by', '=', $userIdToCheck);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user.
|
||||
*/
|
||||
private function currentUser(): User
|
||||
{
|
||||
if (is_null($this->currentUserModel)) {
|
||||
$this->currentUserModel = user();
|
||||
}
|
||||
|
||||
@@ -782,10 +685,9 @@ class PermissionService
|
||||
/**
|
||||
* Clean the cached user elements.
|
||||
*/
|
||||
private function clean()
|
||||
private function clean(): void
|
||||
{
|
||||
$this->currentUserModel = false;
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
$this->currentUserModel = null;
|
||||
$this->userRoles = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
|
||||
protected $permission;
|
||||
protected $role;
|
||||
protected $permissionService;
|
||||
@@ -55,11 +57,14 @@ class PermissionsRepo
|
||||
public function saveNewRole(array $roleData): Role
|
||||
{
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
Activity::add(ActivityType::ROLE_CREATE, $role);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
@@ -86,14 +91,16 @@ class PermissionsRepo
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
Activity::add(ActivityType::ROLE_UPDATE, $role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an list of permission names to an role.
|
||||
*/
|
||||
public function assignRolePermissions(Role $role, array $permissionNameArray = [])
|
||||
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
|
||||
{
|
||||
$permissions = [];
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
@@ -113,6 +120,7 @@ class PermissionsRepo
|
||||
* 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.
|
||||
*
|
||||
* @throws PermissionsException
|
||||
* @throws Exception
|
||||
*/
|
||||
@@ -124,7 +132,7 @@ class PermissionsRepo
|
||||
// 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 === intval(setting('registration-role'))) {
|
||||
} elseif ($role->id === intval(setting('registration-role'))) {
|
||||
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
|
||||
}
|
||||
|
||||
@@ -137,6 +145,7 @@ class PermissionsRepo
|
||||
}
|
||||
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
Activity::add(ActivityType::ROLE_DELETE, $role);
|
||||
$role->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Model;
|
||||
@@ -18,7 +20,9 @@ class RolePermission extends Model
|
||||
|
||||
/**
|
||||
* Get the permission object by name.
|
||||
*
|
||||
* @param $name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getByName($name)
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\RolePermission;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Class Role
|
||||
* @property int $id
|
||||
* Class Role.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $display_name
|
||||
* @property string $description
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
* @property bool $mfa_enforced
|
||||
*/
|
||||
class Role extends Model
|
||||
class Role extends Model implements Loggable
|
||||
{
|
||||
|
||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
*/
|
||||
public function users()
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
|
||||
}
|
||||
@@ -38,7 +43,7 @@ class Role extends Model
|
||||
/**
|
||||
* The RolePermissions that belong to the role.
|
||||
*/
|
||||
public function permissions()
|
||||
public function permissions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
@@ -54,6 +59,7 @@ class Role extends Model
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -90,7 +96,7 @@ class Role extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* Get all visible roles.
|
||||
*/
|
||||
public static function visible(): Collection
|
||||
{
|
||||
@@ -102,6 +108,17 @@ class Role extends Model
|
||||
*/
|
||||
public static function restrictable(): Collection
|
||||
{
|
||||
return static::query()->where('system_name', '!=', 'admin')->get();
|
||||
return static::query()
|
||||
->where('system_name', '!=', 'admin')
|
||||
->orderBy('display_name', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "({$this->id}) {$this->display_name}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
/**
|
||||
* Class SocialAccount.
|
||||
*
|
||||
* @property string $driver
|
||||
* @property User $user
|
||||
*/
|
||||
class SocialAccount extends Model implements Loggable
|
||||
{
|
||||
|
||||
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "{$this->driver}; {$this->user->logDescriptor()}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +1,71 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Auth\Access\Mfa\MfaValue;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use BookStack\Uploads\Image;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class User
|
||||
* @package BookStack\Auth
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property bool $email_confirmed
|
||||
* @property int $image_id
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
* Class User.
|
||||
*
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property bool $email_confirmed
|
||||
* @property int $image_id
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
* @property Collection $roles
|
||||
* @property Collection $mfaValues
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
|
||||
{
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
use Authenticatable;
|
||||
use CanResetPassword;
|
||||
use 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'];
|
||||
|
||||
protected $casts = ['last_activity_at' => 'datetime'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
@@ -54,48 +75,51 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* This holds the user's permissions when loaded.
|
||||
* @var array
|
||||
*
|
||||
* @var ?Collection
|
||||
*/
|
||||
protected $permissions;
|
||||
|
||||
/**
|
||||
* This holds the default user when loaded.
|
||||
*
|
||||
* @var null|User
|
||||
*/
|
||||
protected static $defaultUser = null;
|
||||
|
||||
/**
|
||||
* Returns the default public user.
|
||||
* @return User
|
||||
*/
|
||||
public static function getDefault()
|
||||
public static function getDefault(): User
|
||||
{
|
||||
if (!is_null(static::$defaultUser)) {
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
static::$defaultUser = static::where('system_name', '=', 'public')->first();
|
||||
|
||||
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
|
||||
|
||||
return static::$defaultUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is the default public user.
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault()
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->system_name === 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* The roles that belong to the user.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
if ($this->id === 0) {
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
@@ -109,12 +133,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
public function hasSystemRole(string $roleSystemName): bool
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains($role);
|
||||
return $this->roles->pluck('system_name')->contains($roleSystemName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,35 +150,44 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->map(function ($role) {
|
||||
return $role->permissions;
|
||||
})->flatten()->unique();
|
||||
$this->permissions = $permissions;
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a particular permission.
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function can($permissionName)
|
||||
public function can(string $permissionName): bool
|
||||
{
|
||||
if ($this->email === 'guest') {
|
||||
return false;
|
||||
}
|
||||
return $this->permissions()->pluck('name')->contains($permissionName);
|
||||
|
||||
return $this->permissions()->contains($permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions belonging to a the current user.
|
||||
*/
|
||||
protected function permissions(): Collection
|
||||
{
|
||||
if (isset($this->permissions)) {
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
$this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
|
||||
->select('role_permissions.name as name')->distinct()
|
||||
->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
|
||||
->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
|
||||
->where('ru.user_id', '=', $this->id)
|
||||
->get()
|
||||
->pluck('name');
|
||||
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any cached permissions on this instance.
|
||||
*/
|
||||
public function clearPermissionCache()
|
||||
{
|
||||
$this->permissions = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,9 +200,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Get the social account associated with this user.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function socialAccounts()
|
||||
public function socialAccounts(): HasMany
|
||||
{
|
||||
return $this->hasMany(SocialAccount::class);
|
||||
}
|
||||
@@ -179,7 +209,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
/**
|
||||
* Check if the user has a social account,
|
||||
* If a driver is passed it checks for that single account type.
|
||||
*
|
||||
* @param bool|string $socialDriver
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSocialAccount($socialDriver = false)
|
||||
@@ -192,11 +224,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's avatar,
|
||||
* @param int $size
|
||||
* @return string
|
||||
* Returns a URL to the user's avatar.
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
public function getAvatar(int $size = 50): string
|
||||
{
|
||||
$default = url('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
@@ -206,17 +236,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
try {
|
||||
$avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
} catch (Exception $err) {
|
||||
$avatar = $default;
|
||||
}
|
||||
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar for the user.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function avatar()
|
||||
public function avatar(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
@@ -229,12 +259,42 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->hasMany(ApiToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the favourite instances for this user.
|
||||
*/
|
||||
public function favourites(): HasMany
|
||||
{
|
||||
return $this->hasMany(Favourite::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MFA values belonging to this use.
|
||||
*/
|
||||
public function mfaValues(): HasMany
|
||||
{
|
||||
return $this->hasMany(MfaValue::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last activity time for this user.
|
||||
*/
|
||||
public function scopeWithLastActivityAt(Builder $query)
|
||||
{
|
||||
$query->addSelect(['activities.created_at as last_activity_at'])
|
||||
->leftJoinSub(function (\Illuminate\Database\Query\Builder $query) {
|
||||
$query->from('activities')->select('user_id')
|
||||
->selectRaw('max(created_at) as created_at')
|
||||
->groupBy('user_id');
|
||||
}, 'activities', 'users.id', '=', 'activities.user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for editing this user.
|
||||
*/
|
||||
public function getEditUrl(string $path = ''): string
|
||||
{
|
||||
$uri = '/settings/users/' . $this->id . '/' . trim($path, '/');
|
||||
|
||||
return url(rtrim($uri, '/'));
|
||||
}
|
||||
|
||||
@@ -243,15 +303,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getProfileUrl(): string
|
||||
{
|
||||
return url('/user/' . $this->id);
|
||||
return url('/user/' . $this->slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shortened version of the user's name.
|
||||
* @param int $chars
|
||||
* @return string
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
public function getShortName(int $chars = 8): string
|
||||
{
|
||||
if (mb_strlen($this->name) <= $chars) {
|
||||
return $this->name;
|
||||
@@ -267,11 +325,31 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
* @param string $token
|
||||
*
|
||||
* @param string $token
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
return "({$this->id}) {$this->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
|
||||
return $this->slug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Auth;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Images;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Log;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
protected $user;
|
||||
protected $role;
|
||||
protected $userAvatar;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
*/
|
||||
public function __construct(User $user, Role $role)
|
||||
public function __construct(UserAvatars $userAvatar)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->role = $role;
|
||||
$this->userAvatar = $userAvatar;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,36 +34,45 @@ class UserRepo
|
||||
*/
|
||||
public function getByEmail(string $email): ?User
|
||||
{
|
||||
return $this->user->where('email', '=', $email)->first();
|
||||
return User::query()->where('email', '=', $email)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return User
|
||||
* Get a user by their ID.
|
||||
*/
|
||||
public function getById($id)
|
||||
public function getById(int $id): User
|
||||
{
|
||||
return $this->user->newQuery()->findOrFail($id);
|
||||
return User::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their slug.
|
||||
*/
|
||||
public function getBySlug(string $slug): User
|
||||
{
|
||||
return User::query()->where('slug', '=', $slug)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* @return Builder|static
|
||||
*/
|
||||
public function getAllUsers()
|
||||
public function getAllUsers(): Collection
|
||||
{
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
||||
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
$sort = $sortData['sort'];
|
||||
|
||||
$query = User::query()->select(['*'])
|
||||
->withLastActivityAt()
|
||||
->with(['roles', 'avatar'])
|
||||
->withCount('mfaValues')
|
||||
->orderBy($sort, $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
@@ -75,7 +85,7 @@ class UserRepo
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
*/
|
||||
public function registerNew(array $data, bool $emailConfirmed = false): User
|
||||
@@ -89,14 +99,13 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Assign a user to a system-level role.
|
||||
* @param User $user
|
||||
* @param $systemRoleName
|
||||
*
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function attachSystemRole(User $user, $systemRoleName)
|
||||
public function attachSystemRole(User $user, string $systemRoleName)
|
||||
{
|
||||
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
|
||||
if ($role === null) {
|
||||
$role = Role::getSystemRole($systemRoleName);
|
||||
if (is_null($role)) {
|
||||
throw new NotFoundException("Role '{$systemRoleName}' not found");
|
||||
}
|
||||
$user->attachRole($role);
|
||||
@@ -104,26 +113,24 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Checks if the give user is the only admin.
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
public function isOnlyAdmin(User $user): bool
|
||||
{
|
||||
if (!$user->hasSystemRole('admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
if ($adminRole->users->count() > 1) {
|
||||
$adminRole = Role::getSystemRole('admin');
|
||||
if ($adminRole->users()->count() > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the assigned user roles via an array of role IDs.
|
||||
* @param User $user
|
||||
* @param array $roles
|
||||
*
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function setUserRoles(User $user, array $roles)
|
||||
@@ -138,14 +145,11 @@ class UserRepo
|
||||
/**
|
||||
* Check if the given user is the last admin and their new roles no longer
|
||||
* contains the admin role.
|
||||
* @param User $user
|
||||
* @param array $newRoles
|
||||
* @return bool
|
||||
*/
|
||||
protected function demotingLastAdmin(User $user, array $newRoles) : bool
|
||||
protected function demotingLastAdmin(User $user, array $newRoles): bool
|
||||
{
|
||||
if ($this->isOnlyAdmin($user)) {
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
$adminRole = Role::getSystemRole('admin');
|
||||
if (!in_array(strval($adminRole->id), $newRoles)) {
|
||||
return true;
|
||||
}
|
||||
@@ -159,41 +163,62 @@ class UserRepo
|
||||
*/
|
||||
public function create(array $data, bool $emailConfirmed = false): User
|
||||
{
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
$details = [
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => $emailConfirmed,
|
||||
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||
]);
|
||||
];
|
||||
|
||||
$user = new User();
|
||||
$user->forceFill($details);
|
||||
$user->refreshSlug();
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
* @param User $user
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
public function destroy(User $user, ?int $newOwnerId = null)
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->favourites()->delete();
|
||||
$user->mfaValues()->delete();
|
||||
$user->delete();
|
||||
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroy($image);
|
||||
$this->userAvatar->destroyAllForUser($user);
|
||||
|
||||
if (!empty($newOwnerId)) {
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
if (!is_null($newOwner)) {
|
||||
$this->migrateOwnership($user, $newOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate ownership of items in the system from one user to another.
|
||||
*/
|
||||
protected function migrateOwnership(User $fromUser, User $toUser)
|
||||
{
|
||||
$entities = (new EntityProvider())->all();
|
||||
foreach ($entities as $instance) {
|
||||
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
|
||||
->update(['owned_by' => $toUser->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
public function getActivity(User $user, int $count = 20, int $page = 0): array
|
||||
{
|
||||
return Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
@@ -224,43 +249,33 @@ class UserRepo
|
||||
public function getAssetCounts(User $user): array
|
||||
{
|
||||
$createdBy = ['created_by' => $user->id];
|
||||
|
||||
return [
|
||||
'pages' => Page::visible()->where($createdBy)->count(),
|
||||
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
||||
'books' => Book::visible()->where($createdBy)->count(),
|
||||
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
||||
'pages' => Page::visible()->where($createdBy)->count(),
|
||||
'chapters' => Chapter::visible()->where($createdBy)->count(),
|
||||
'books' => Book::visible()->where($createdBy)->count(),
|
||||
'shelves' => Bookshelf::visible()->where($createdBy)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRoles()
|
||||
public function getAllRoles(): Collection
|
||||
{
|
||||
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
|
||||
return Role::query()->orderBy('display_name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an avatar image for a user and set it as their avatar.
|
||||
* Returns early if avatars disabled or not set in config.
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function downloadAndAssignUserAvatar(User $user)
|
||||
public function downloadAndAssignUserAvatar(User $user): void
|
||||
{
|
||||
if (!Images::avatarFetchEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = Images::saveUserAvatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
return true;
|
||||
$this->userAvatar->fetchAndAssignToUser($user);
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ return [
|
||||
'max_item_count' => env('API_MAX_ITEM_COUNT', 500),
|
||||
|
||||
// The number of API requests that can be made per minute by a single user.
|
||||
'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180)
|
||||
'requests_per_minute' => env('API_REQUESTS_PER_MIN', 180),
|
||||
|
||||
];
|
||||
|
||||
@@ -19,23 +19,28 @@ return [
|
||||
// private configuration variables so should remain disabled in public.
|
||||
'debug' => env('APP_DEBUG', false),
|
||||
|
||||
// Set the default view type for various lists. Can be overridden by user preferences.
|
||||
// These will be used for public viewers and users that have not set a preference.
|
||||
'views' => [
|
||||
'books' => env('APP_VIEWS_BOOKS', 'list'),
|
||||
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
],
|
||||
|
||||
// The number of revisions to keep in the database.
|
||||
// Once this limit is reached older revisions will be deleted.
|
||||
// If set to false then a limit will not be enforced.
|
||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
||||
|
||||
// The number of days that content will remain in the recycle bin before
|
||||
// being considered for auto-removal. It is not a guarantee that content will
|
||||
// be removed after this time.
|
||||
// Set to 0 for no recycle bin functionality.
|
||||
// Set to -1 for unlimited recycle bin lifetime.
|
||||
'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
|
||||
|
||||
// Allow <script> tags to entered within page content.
|
||||
// <script> tags are escaped by default.
|
||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
||||
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
|
||||
|
||||
// Allow server-side fetches to be performed to potentially unknown
|
||||
// and user-provided locations. Primarily used in exports when loading
|
||||
// in externally referenced assets.
|
||||
'allow_untrusted_server_fetching' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
|
||||
|
||||
// Override the default behaviour for allowing crawlers to crawl the instance.
|
||||
// May be ignored if view has be overridden or modified.
|
||||
// Defaults to null since, if not set, 'app-public' status used instead.
|
||||
@@ -45,6 +50,10 @@ return [
|
||||
// and used by BookStack in URL generation.
|
||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
|
||||
// A list of hosts that BookStack can be iframed within.
|
||||
// Space separated if multiple. BookStack host domain is auto-inferred.
|
||||
'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
|
||||
|
||||
// Application timezone for back-end date functions.
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
@@ -52,7 +61,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW'],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
@@ -111,12 +120,14 @@ return [
|
||||
BookStack\Providers\TranslationServiceProvider::class,
|
||||
|
||||
// BookStack custom service providers
|
||||
BookStack\Providers\ThemeServiceProvider::class,
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\BroadcastServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
BookStack\Providers\CustomFacadeProvider::class,
|
||||
BookStack\Providers\CustomValidationServiceProvider::class,
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -134,55 +145,52 @@ return [
|
||||
'aliases' => [
|
||||
|
||||
// Laravel
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Arr' => Illuminate\Support\Arr::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||
'Config' => Illuminate\Support\Facades\Config::class,
|
||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||
'DB' => Illuminate\Support\Facades\DB::class,
|
||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||
'Event' => Illuminate\Support\Facades\Event::class,
|
||||
'File' => Illuminate\Support\Facades\File::class,
|
||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||
'Input' => Illuminate\Support\Facades\Input::class,
|
||||
'Inspiring' => Illuminate\Foundation\Inspiring::class,
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Arr' => Illuminate\Support\Arr::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
'Blade' => Illuminate\Support\Facades\Blade::class,
|
||||
'Bus' => Illuminate\Support\Facades\Bus::class,
|
||||
'Cache' => Illuminate\Support\Facades\Cache::class,
|
||||
'Config' => Illuminate\Support\Facades\Config::class,
|
||||
'Cookie' => Illuminate\Support\Facades\Cookie::class,
|
||||
'Crypt' => Illuminate\Support\Facades\Crypt::class,
|
||||
'DB' => Illuminate\Support\Facades\DB::class,
|
||||
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
|
||||
'Event' => Illuminate\Support\Facades\Event::class,
|
||||
'File' => Illuminate\Support\Facades\File::class,
|
||||
'Hash' => Illuminate\Support\Facades\Hash::class,
|
||||
'Input' => Illuminate\Support\Facades\Input::class,
|
||||
'Inspiring' => Illuminate\Foundation\Inspiring::class,
|
||||
'Lang' => Illuminate\Support\Facades\Lang::class,
|
||||
'Log' => Illuminate\Support\Facades\Log::class,
|
||||
'Mail' => Illuminate\Support\Facades\Mail::class,
|
||||
'Notification' => Illuminate\Support\Facades\Notification::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||
'Session' => Illuminate\Support\Facades\Session::class,
|
||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||
'Str' => Illuminate\Support\Str::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
|
||||
'Password' => Illuminate\Support\Facades\Password::class,
|
||||
'Queue' => Illuminate\Support\Facades\Queue::class,
|
||||
'Redirect' => Illuminate\Support\Facades\Redirect::class,
|
||||
'Redis' => Illuminate\Support\Facades\Redis::class,
|
||||
'Request' => Illuminate\Support\Facades\Request::class,
|
||||
'Response' => Illuminate\Support\Facades\Response::class,
|
||||
'Route' => Illuminate\Support\Facades\Route::class,
|
||||
'Schema' => Illuminate\Support\Facades\Schema::class,
|
||||
'Session' => Illuminate\Support\Facades\Session::class,
|
||||
'Storage' => Illuminate\Support\Facades\Storage::class,
|
||||
'Str' => Illuminate\Support\Str::class,
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
|
||||
|
||||
// Third Party
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Setting' => BookStack\Facades\Setting::class,
|
||||
'Views' => BookStack\Facades\Views::class,
|
||||
'Images' => BookStack\Facades\Images::class,
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Permissions' => BookStack\Facades\Permissions::class,
|
||||
|
||||
'Theme' => BookStack\Facades\Theme::class,
|
||||
],
|
||||
|
||||
// Proxy configuration
|
||||
|
||||
@@ -18,7 +18,7 @@ return [
|
||||
// This option controls the default authentication "guard" and password
|
||||
// reset options for your application.
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_METHOD', 'standard'),
|
||||
'guard' => env('AUTH_METHOD', 'standard'),
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
@@ -29,15 +29,15 @@ return [
|
||||
// Supported drivers: "session", "api-token", "ldap-session"
|
||||
'guards' => [
|
||||
'standard' => [
|
||||
'driver' => 'session',
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'ldap' => [
|
||||
'driver' => 'ldap-session',
|
||||
'driver' => 'ldap-session',
|
||||
'provider' => 'external',
|
||||
],
|
||||
'saml2' => [
|
||||
'driver' => 'saml2-session',
|
||||
'driver' => 'saml2-session',
|
||||
'provider' => 'external',
|
||||
],
|
||||
'api' => [
|
||||
@@ -52,11 +52,11 @@ return [
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
],
|
||||
'external' => [
|
||||
'driver' => 'external-users',
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -67,9 +67,9 @@ return [
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -23,18 +23,18 @@ return [
|
||||
'connections' => [
|
||||
|
||||
'pusher' => [
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
'options' => [
|
||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||
'useTLS' => true,
|
||||
'useTLS' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
],
|
||||
|
||||
@@ -46,7 +46,6 @@ return [
|
||||
'driver' => 'null',
|
||||
],
|
||||
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -42,8 +42,8 @@ return [
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'cache',
|
||||
'driver' => 'database',
|
||||
'table' => 'cache',
|
||||
'connection' => null,
|
||||
],
|
||||
|
||||
@@ -58,7 +58,7 @@ return [
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
],
|
||||
|
||||
|
||||
@@ -59,37 +59,38 @@ return [
|
||||
'connections' => [
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => $mysql_host,
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'port' => $mysql_port,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DATABASE_URL'),
|
||||
'host' => $mysql_host,
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'port' => $mysql_port,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mysql_testing' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('TEST_DATABASE_URL'),
|
||||
'host' => '127.0.0.1',
|
||||
'database' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'driver' => 'mysql',
|
||||
'url' => env('TEST_DATABASE_URL'),
|
||||
'host' => '127.0.0.1',
|
||||
'database' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'port' => $mysql_port,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => false,
|
||||
'strict' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Debugbar Configuration Options
|
||||
* Debugbar Configuration Options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
@@ -10,53 +10,52 @@
|
||||
|
||||
return [
|
||||
|
||||
// Debugbar is enabled by default, when debug is set to true in app.php.
|
||||
// You can override the value by setting enable to true or false instead of null.
|
||||
//
|
||||
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
||||
// Debugbar is enabled by default, when debug is set to true in app.php.
|
||||
// You can override the value by setting enable to true or false instead of null.
|
||||
//
|
||||
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
||||
'except' => [
|
||||
'telescope*'
|
||||
'except' => [
|
||||
'telescope*',
|
||||
],
|
||||
|
||||
|
||||
// DebugBar stores data for session/ajax requests.
|
||||
// You can disable this, so the debugbar stores data in headers/session,
|
||||
// but this can cause problems with large data collectors.
|
||||
// By default, file storage (in the storage folder) is used. Redis and PDO
|
||||
// can also be used. For PDO, run the package migrations first.
|
||||
// DebugBar stores data for session/ajax requests.
|
||||
// You can disable this, so the debugbar stores data in headers/session,
|
||||
// but this can cause problems with large data collectors.
|
||||
// By default, file storage (in the storage folder) is used. Redis and PDO
|
||||
// can also be used. For PDO, run the package migrations first.
|
||||
'storage' => [
|
||||
'enabled' => true,
|
||||
'driver' => 'file', // redis, file, pdo, custom
|
||||
'path' => storage_path('debugbar'), // For file driver
|
||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
||||
'provider' => '' // Instance of StorageInterface for custom driver
|
||||
'provider' => '', // Instance of StorageInterface for custom driver
|
||||
],
|
||||
|
||||
// Vendor files are included by default, but can be set to false.
|
||||
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
||||
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
||||
// and for js: jquery and and highlight.js
|
||||
// So if you want syntax highlighting, set it to true.
|
||||
// jQuery is set to not conflict with existing jQuery scripts.
|
||||
// Vendor files are included by default, but can be set to false.
|
||||
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
||||
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
||||
// and for js: jquery and and highlight.js
|
||||
// So if you want syntax highlighting, set it to true.
|
||||
// jQuery is set to not conflict with existing jQuery scripts.
|
||||
'include_vendors' => true,
|
||||
|
||||
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
||||
// you can use this option to disable sending the data through the headers.
|
||||
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
||||
// you can use this option to disable sending the data through the headers.
|
||||
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||
|
||||
'capture_ajax' => true,
|
||||
'capture_ajax' => true,
|
||||
'add_ajax_timing' => false,
|
||||
|
||||
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
||||
// in the Messages tab.
|
||||
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
||||
// in the Messages tab.
|
||||
'error_handler' => false,
|
||||
|
||||
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
||||
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
||||
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
||||
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
||||
'clockwork' => false,
|
||||
|
||||
// Enable/disable DataCollectors
|
||||
// Enable/disable DataCollectors
|
||||
'collectors' => [
|
||||
'phpinfo' => true, // Php version
|
||||
'messages' => true, // Messages
|
||||
@@ -82,7 +81,7 @@ return [
|
||||
'models' => true, // Display models
|
||||
],
|
||||
|
||||
// Configure some DataCollectors
|
||||
// Configure some DataCollectors
|
||||
'options' => [
|
||||
'auth' => [
|
||||
'show_name' => true, // Also show the users name/email in the debugbar
|
||||
@@ -91,43 +90,43 @@ return [
|
||||
'with_params' => true, // Render SQL with the parameters substituted
|
||||
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
||||
'timeline' => false, // Add the queries to the timeline
|
||||
'explain' => [ // Show EXPLAIN output on queries
|
||||
'explain' => [ // Show EXPLAIN output on queries
|
||||
'enabled' => false,
|
||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||
],
|
||||
'hints' => true, // Show hints for common mistakes
|
||||
],
|
||||
'mail' => [
|
||||
'full_log' => false
|
||||
'full_log' => false,
|
||||
],
|
||||
'views' => [
|
||||
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
||||
],
|
||||
'route' => [
|
||||
'label' => true // show complete route on bar
|
||||
'label' => true, // show complete route on bar
|
||||
],
|
||||
'logs' => [
|
||||
'file' => null
|
||||
'file' => null,
|
||||
],
|
||||
'cache' => [
|
||||
'values' => true // collect cache values
|
||||
'values' => true, // collect cache values
|
||||
],
|
||||
],
|
||||
|
||||
// Inject Debugbar into the response
|
||||
// Usually, the debugbar is added just before </body>, by listening to the
|
||||
// Response after the App is done. If you disable this, you have to add them
|
||||
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
||||
// Inject Debugbar into the response
|
||||
// Usually, the debugbar is added just before </body>, by listening to the
|
||||
// Response after the App is done. If you disable this, you have to add them
|
||||
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
||||
'inject' => true,
|
||||
|
||||
// DebugBar route prefix
|
||||
// Sometimes you want to set route prefix to be used by DebugBar to load
|
||||
// its resources from. Usually the need comes from misconfigured web server or
|
||||
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
||||
// DebugBar route prefix
|
||||
// Sometimes you want to set route prefix to be used by DebugBar to load
|
||||
// its resources from. Usually the need comes from misconfigured web server or
|
||||
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
||||
'route_prefix' => '_debugbar',
|
||||
|
||||
// DebugBar route domain
|
||||
// By default DebugBar route served from the same domain that request served.
|
||||
// To override default domain, specify it as a non-empty value.
|
||||
// DebugBar route domain
|
||||
// By default DebugBar route served from the same domain that request served.
|
||||
// To override default domain, specify it as a non-empty value.
|
||||
'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
];
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
'orientation' => 'portrait',
|
||||
'defines' => [
|
||||
'orientation' => 'portrait',
|
||||
'defines' => [
|
||||
/**
|
||||
* The location of the DOMPDF font directory
|
||||
* The location of the DOMPDF font directory.
|
||||
*
|
||||
* The location of the directory where DOMPDF will store fonts and font metrics
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
@@ -38,17 +37,17 @@ return [
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
* The location of the DOMPDF font cache directory.
|
||||
*
|
||||
* This directory contains the cached font metrics for the fonts used by DOMPDF.
|
||||
* This directory can be the same as DOMPDF_FONT_DIR
|
||||
*
|
||||
* Note: This directory must exist and be writable by the webserver process.
|
||||
*/
|
||||
"DOMPDF_FONT_CACHE" => storage_path('fonts/'),
|
||||
'font_cache' => storage_path('fonts/'),
|
||||
|
||||
/**
|
||||
* The location of a temporary directory.
|
||||
@@ -57,10 +56,10 @@ return [
|
||||
* The temporary directory is required to download remote images and when
|
||||
* using the PFDLib back end.
|
||||
*/
|
||||
"DOMPDF_TEMP_DIR" => sys_get_temp_dir(),
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
|
||||
/**
|
||||
* ==== IMPORTANT ====
|
||||
* ==== IMPORTANT ====.
|
||||
*
|
||||
* dompdf's "chroot": Prevents dompdf from accessing system files or other
|
||||
* files on the webserver. All local files opened by dompdf must be in a
|
||||
@@ -71,7 +70,7 @@ return [
|
||||
* direct class use like:
|
||||
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
|
||||
*/
|
||||
"DOMPDF_CHROOT" => realpath(base_path()),
|
||||
'chroot' => realpath(base_path()),
|
||||
|
||||
/**
|
||||
* Whether to use Unicode fonts or not.
|
||||
@@ -82,20 +81,19 @@ return [
|
||||
* When enabled, dompdf can support all Unicode glyphs. Any glyphs used in a
|
||||
* document must be present in your fonts, however.
|
||||
*/
|
||||
"DOMPDF_UNICODE_ENABLED" => true,
|
||||
'unicode_enabled' => true,
|
||||
|
||||
/**
|
||||
* Whether to enable font subsetting or not.
|
||||
*/
|
||||
"DOMPDF_ENABLE_FONTSUBSETTING" => false,
|
||||
'enable_fontsubsetting' => false,
|
||||
|
||||
/**
|
||||
* The PDF rendering backend to use
|
||||
* The PDF rendering backend to use.
|
||||
*
|
||||
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
|
||||
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
|
||||
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link
|
||||
* Canvas_Factory} ultimately determines which rendering class to instantiate
|
||||
* fall back on CPDF. 'GD' renders PDFs to graphic files. {@link * Canvas_Factory} ultimately determines which rendering class to instantiate
|
||||
* based on this setting.
|
||||
*
|
||||
* Both PDFLib & CPDF rendering backends provide sufficient rendering
|
||||
@@ -117,10 +115,10 @@ return [
|
||||
* @link http://www.ros.co.nz/pdf
|
||||
* @link http://www.php.net/image
|
||||
*/
|
||||
"DOMPDF_PDF_BACKEND" => "CPDF",
|
||||
'pdf_backend' => 'CPDF',
|
||||
|
||||
/**
|
||||
* PDFlib license key
|
||||
* PDFlib license key.
|
||||
*
|
||||
* If you are using a licensed, commercial version of PDFlib, specify
|
||||
* your license key here. If you are using PDFlib-Lite or are evaluating
|
||||
@@ -143,7 +141,7 @@ return [
|
||||
* the desired content might be different (e.g. screen or projection view of html file).
|
||||
* Therefore allow specification of content here.
|
||||
*/
|
||||
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
|
||||
'default_media_type' => 'print',
|
||||
|
||||
/**
|
||||
* The default paper size.
|
||||
@@ -152,18 +150,19 @@ return [
|
||||
*
|
||||
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
|
||||
*/
|
||||
"DOMPDF_DEFAULT_PAPER_SIZE" => "a4",
|
||||
'default_paper_size' => 'a4',
|
||||
|
||||
/**
|
||||
* The default font family
|
||||
* The default font family.
|
||||
*
|
||||
* Used if no suitable fonts can be found. This must exist in the font folder.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
"DOMPDF_DEFAULT_FONT" => "dejavu sans",
|
||||
'default_font' => 'dejavu sans',
|
||||
|
||||
/**
|
||||
* Image DPI setting
|
||||
* Image DPI setting.
|
||||
*
|
||||
* This setting determines the default DPI setting for images and fonts. The
|
||||
* DPI may be overridden for inline images by explictly setting the
|
||||
@@ -195,10 +194,10 @@ return [
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
"DOMPDF_DPI" => 96,
|
||||
'dpi' => 96,
|
||||
|
||||
/**
|
||||
* Enable inline PHP
|
||||
* Enable inline PHP.
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically evaluate
|
||||
* inline PHP contained within <script type="text/php"> ... </script> tags.
|
||||
@@ -209,20 +208,20 @@ return [
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_PHP" => false,
|
||||
'enable_php' => false,
|
||||
|
||||
/**
|
||||
* Enable inline Javascript
|
||||
* Enable inline Javascript.
|
||||
*
|
||||
* If this setting is set to true then DOMPDF will automatically insert
|
||||
* JavaScript code contained within <script type="text/javascript"> ... </script> tags.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => true,
|
||||
'enable_javascript' => false,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
* Enable remote file access.
|
||||
*
|
||||
* If this setting is set to true, DOMPDF will access remote sites for
|
||||
* images and CSS files as required.
|
||||
@@ -238,29 +237,27 @@ return [
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_REMOTE" => true,
|
||||
'enable_remote' => env('ALLOW_UNTRUSTED_SERVER_FETCHING', false),
|
||||
|
||||
/**
|
||||
* A ratio applied to the fonts height to be more like browsers' line height
|
||||
* A ratio applied to the fonts height to be more like browsers' line height.
|
||||
*/
|
||||
"DOMPDF_FONT_HEIGHT_RATIO" => 1.1,
|
||||
'font_height_ratio' => 1.1,
|
||||
|
||||
/**
|
||||
* Enable CSS float
|
||||
* Enable CSS float.
|
||||
*
|
||||
* Allows people to disabled CSS float support
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_CSS_FLOAT" => true,
|
||||
|
||||
'enable_css_float' => true,
|
||||
|
||||
/**
|
||||
* Use the more-than-experimental HTML5 Lib parser
|
||||
* Use the more-than-experimental HTML5 Lib parser.
|
||||
*/
|
||||
"DOMPDF_ENABLE_HTML5PARSER" => true,
|
||||
|
||||
'enable_html5parser' => true,
|
||||
|
||||
],
|
||||
|
||||
|
||||
];
|
||||
|
||||
@@ -34,7 +34,7 @@ return [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => public_path(),
|
||||
'root' => public_path(),
|
||||
],
|
||||
|
||||
'local_secure' => [
|
||||
@@ -42,33 +42,16 @@ return [
|
||||
'root' => storage_path(),
|
||||
],
|
||||
|
||||
'ftp' => [
|
||||
'driver' => 'ftp',
|
||||
'host' => 'ftp.example.com',
|
||||
'username' => 'your-username',
|
||||
'password' => 'your-password',
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
||||
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
||||
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
||||
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
||||
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
||||
'driver' => 's3',
|
||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
||||
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
||||
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
||||
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
||||
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
||||
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
||||
],
|
||||
|
||||
'rackspace' => [
|
||||
'driver' => 'rackspace',
|
||||
'username' => 'your-username',
|
||||
'key' => 'your-key',
|
||||
'container' => 'your-container',
|
||||
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
|
||||
'region' => 'IAD',
|
||||
'url_type' => 'publicURL',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -29,9 +29,9 @@ return [
|
||||
// passwords are hashed using the Argon algorithm. These will allow you
|
||||
// to control the amount of time it takes to hash the given password.
|
||||
'argon' => [
|
||||
'memory' => 1024,
|
||||
'memory' => 1024,
|
||||
'threads' => 2,
|
||||
'time' => 2,
|
||||
'time' => 2,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -30,66 +30,66 @@ return [
|
||||
// "custom", "stack"
|
||||
'channels' => [
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => ['daily'],
|
||||
'driver' => 'stack',
|
||||
'channels' => ['daily'],
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 7,
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 7,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => 'Laravel Log',
|
||||
'emoji' => ':boom:',
|
||||
'level' => 'critical',
|
||||
'emoji' => ':boom:',
|
||||
'level' => 'critical',
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'driver' => 'monolog',
|
||||
'handler' => StreamHandler::class,
|
||||
'with' => [
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => 'debug',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => 'debug',
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
// Custom errorlog implementation that logs out a plain,
|
||||
// non-formatted message intended for the webserver log.
|
||||
'errorlog_plain_webserver' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => 'debug',
|
||||
'handler' => ErrorLogHandler::class,
|
||||
'handler_with' => [4],
|
||||
'formatter' => LineFormatter::class,
|
||||
'driver' => 'monolog',
|
||||
'level' => 'debug',
|
||||
'handler' => ErrorLogHandler::class,
|
||||
'handler_with' => [4],
|
||||
'formatter' => LineFormatter::class,
|
||||
'formatter_with' => [
|
||||
'format' => "%message%",
|
||||
'format' => '%message%',
|
||||
],
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
@@ -101,7 +101,6 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
// Failed Login Message
|
||||
// Allows a configurable message to be logged when a login request fails.
|
||||
'failed_login' => [
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
return [
|
||||
|
||||
// Mail driver to use.
|
||||
// Options: smtp, mail, sendmail, log
|
||||
// Options: smtp, sendmail, log, array
|
||||
'driver' => env('MAIL_DRIVER', 'smtp'),
|
||||
|
||||
// SMTP host address
|
||||
@@ -23,7 +23,7 @@ return [
|
||||
// Global "From" address & name
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'BookStack')
|
||||
'name' => env('MAIL_FROM_NAME', 'BookStack'),
|
||||
],
|
||||
|
||||
// Email encryption protocol
|
||||
|
||||
@@ -17,24 +17,23 @@ return [
|
||||
// Queue connection configuration
|
||||
'connections' => [
|
||||
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => 90,
|
||||
'block_for' => null,
|
||||
'block_for' => null,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
|
||||
|
||||
return [
|
||||
|
||||
// Display name, shown to users, for SAML2 option
|
||||
@@ -29,7 +31,6 @@ return [
|
||||
// Overrides, in JSON format, to the configuration passed to underlying onelogin library.
|
||||
'onelogin_overrides' => env('SAML2_ONELOGIN_OVERRIDES', null),
|
||||
|
||||
|
||||
'onelogin' => [
|
||||
// If 'strict' is True, then the PHP Toolkit will reject unsigned
|
||||
// or unencrypted messages if it expects them signed or encrypted
|
||||
@@ -79,7 +80,7 @@ return [
|
||||
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
// Usually x509cert and privateKey of the SP are provided by files placed at
|
||||
// the certs folder. But we can also provide them with the following parameters
|
||||
'x509cert' => '',
|
||||
'x509cert' => '',
|
||||
'privateKey' => '',
|
||||
],
|
||||
// Identity Provider Data that we want connect with our SP
|
||||
@@ -139,6 +140,14 @@ return [
|
||||
// )
|
||||
// ),
|
||||
],
|
||||
'security' => [
|
||||
// SAML2 Authn context
|
||||
// When set to false no AuthContext will be sent in the AuthNRequest,
|
||||
// When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.
|
||||
// Multiple forced values can be passed via a space separated array, For example:
|
||||
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
||||
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -28,16 +28,16 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/github/callback',
|
||||
'name' => 'GitHub',
|
||||
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_APP_ID', false),
|
||||
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||
'name' => 'Google',
|
||||
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
||||
'client_id' => env('GOOGLE_APP_ID', false),
|
||||
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||
'name' => 'Google',
|
||||
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
||||
'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),
|
||||
],
|
||||
|
||||
@@ -47,7 +47,7 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/slack/callback',
|
||||
'name' => 'Slack',
|
||||
'auto_register' => env('SLACK_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'facebook' => [
|
||||
@@ -56,7 +56,7 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
|
||||
'name' => 'Facebook',
|
||||
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'twitter' => [
|
||||
@@ -65,27 +65,27 @@ return [
|
||||
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
|
||||
'name' => 'Twitter',
|
||||
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'azure' => [
|
||||
'client_id' => env('AZURE_APP_ID', false),
|
||||
'client_secret' => env('AZURE_APP_SECRET', false),
|
||||
'tenant' => env('AZURE_TENANT', false),
|
||||
'tenant' => env('AZURE_TENANT', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/azure/callback',
|
||||
'name' => 'Microsoft Azure',
|
||||
'auto_register' => env('AZURE_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'okta' => [
|
||||
'client_id' => env('OKTA_APP_ID'),
|
||||
'client_id' => env('OKTA_APP_ID'),
|
||||
'client_secret' => env('OKTA_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
||||
'base_url' => env('OKTA_BASE_URL'),
|
||||
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
||||
'base_url' => env('OKTA_BASE_URL'),
|
||||
'name' => 'Okta',
|
||||
'auto_register' => env('OKTA_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'gitlab' => [
|
||||
@@ -95,43 +95,45 @@ return [
|
||||
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
|
||||
'name' => 'GitLab',
|
||||
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'twitch' => [
|
||||
'client_id' => env('TWITCH_APP_ID'),
|
||||
'client_id' => env('TWITCH_APP_ID'),
|
||||
'client_secret' => env('TWITCH_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
||||
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
||||
'name' => 'Twitch',
|
||||
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'discord' => [
|
||||
'client_id' => env('DISCORD_APP_ID'),
|
||||
'client_id' => env('DISCORD_APP_ID'),
|
||||
'client_secret' => env('DISCORD_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
||||
'name' => 'Discord',
|
||||
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
||||
'name' => 'Discord',
|
||||
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
||||
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'ldap' => [
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||
'version' => env('LDAP_VERSION', false),
|
||||
'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'),
|
||||
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dump_user_details' => env('LDAP_DUMP_USER_DETAILS', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||
'version' => env('LDAP_VERSION', false),
|
||||
'id_attribute' => env('LDAP_ID_ATTRIBUTE', 'uid'),
|
||||
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
||||
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
|
||||
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
||||
'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
|
||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
||||
'user_to_groups' => env('LDAP_USER_TO_GROUPS', false),
|
||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
'start_tls' => env('LDAP_START_TLS', false),
|
||||
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Session configuration options.
|
||||
*
|
||||
@@ -57,7 +59,7 @@ return [
|
||||
// The session cookie path determines the path for which the cookie will
|
||||
// be regarded as available. Typically, this will be the root path of
|
||||
// your application but you are free to change this when necessary.
|
||||
'path' => '/',
|
||||
'path' => '/' . (explode('/', env('APP_URL', ''), 4)[3] ?? ''),
|
||||
|
||||
// Session Cookie Domain
|
||||
// Here you may change the domain of the cookie used to identify a session
|
||||
@@ -69,7 +71,8 @@ return [
|
||||
// By setting this option to true, session cookies will only be sent back
|
||||
// to the server if the browser has a HTTPS connection. This will keep
|
||||
// the cookie from being sent to you if it can not be done securely.
|
||||
'secure' => env('SESSION_SECURE_COOKIE', false),
|
||||
'secure' => env('SESSION_SECURE_COOKIE', null)
|
||||
?? Str::startsWith(env('APP_URL'), 'https:'),
|
||||
|
||||
// HTTP Access Only
|
||||
// Setting this value to true will prevent JavaScript from accessing the
|
||||
@@ -80,6 +83,6 @@ return [
|
||||
// This option determines how your cookies behave when cross-site requests
|
||||
// take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
// do not enable this as other CSRF protection services are in place.
|
||||
// Options: lax, strict
|
||||
'same_site' => null,
|
||||
// Options: lax, strict, none
|
||||
'same_site' => 'lax',
|
||||
];
|
||||
|
||||
@@ -24,4 +24,12 @@ return [
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
// User-level default settings
|
||||
'user' => [
|
||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -14,7 +14,7 @@ return [
|
||||
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
|
||||
'timeout' => false,
|
||||
'options' => [
|
||||
'outline' => true
|
||||
'outline' => true,
|
||||
],
|
||||
'env' => [],
|
||||
],
|
||||
|
||||
@@ -14,8 +14,8 @@ class CleanupImages extends Command
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:cleanup-images
|
||||
{--a|all : Include images that are used in page revisions}
|
||||
{--f|force : Actually run the deletions}
|
||||
{--a|all : Also delete images that are only used in old revisions}
|
||||
{--f|force : Actually run the deletions, Defaults to a dry-run}
|
||||
';
|
||||
|
||||
/**
|
||||
@@ -25,11 +25,11 @@ class CleanupImages extends Command
|
||||
*/
|
||||
protected $description = 'Cleanup images and drawings';
|
||||
|
||||
|
||||
protected $imageService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param \BookStack\Uploads\ImageService $imageService
|
||||
*/
|
||||
public function __construct(ImageService $imageService)
|
||||
@@ -63,6 +63,7 @@ class CleanupImages extends Command
|
||||
$this->comment($deleteCount . ' images found that would have been deleted');
|
||||
$this->showDeletedImages($deleted);
|
||||
$this->comment('Run with -f or --force to perform deletions');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Entities\PageRevision;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearRevisions extends Command
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearViews extends Command
|
||||
@@ -22,7 +23,6 @@ class ClearViews extends Command
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
@@ -36,7 +36,7 @@ class ClearViews extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
\Views::resetAll();
|
||||
View::clearAll();
|
||||
$this->comment('Views cleared');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -54,13 +54,14 @@ class CopyShelfPermissions extends Command
|
||||
|
||||
if (!$cascadeAll && !$shelfSlug) {
|
||||
$this->error('Either a --slug or --all option must be provided.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($cascadeAll) {
|
||||
$continue = $this->confirm(
|
||||
'Permission settings for all shelves will be cascaded. '.
|
||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. '.
|
||||
'Permission settings for all shelves will be cascaded. ' .
|
||||
'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' .
|
||||
'Are you sure you want to proceed?'
|
||||
);
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ class CreateAdmin extends Command
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo)
|
||||
{
|
||||
@@ -40,8 +38,9 @@ class CreateAdmin extends Command
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
@@ -73,7 +72,6 @@ class CreateAdmin extends Command
|
||||
return $this->error('Invalid password provided, Must be at least 5 characters');
|
||||
}
|
||||
|
||||
|
||||
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
||||
$this->userRepo->attachSystemRole($user, 'admin');
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Console\Command;
|
||||
|
||||
class DeleteUsers extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
@@ -47,7 +46,7 @@ class DeleteUsers extends Command
|
||||
continue;
|
||||
}
|
||||
$this->userRepo->destroy($user);
|
||||
++$numDeleted;
|
||||
$numDeleted++;
|
||||
}
|
||||
$this->info("Deleted $numDeleted of $totalUsers total users.");
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Entities\SearchService;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use DB;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -22,17 +22,15 @@ class RegenerateSearch extends Command
|
||||
*/
|
||||
protected $description = 'Re-index all content for searching';
|
||||
|
||||
protected $searchService;
|
||||
protected $searchIndex;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
public function __construct(SearchIndex $searchIndex)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->searchService = $searchService;
|
||||
$this->searchIndex = $searchIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,10 +43,9 @@ class RegenerateSearch extends Command
|
||||
$connection = DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
$this->searchService->setConnection(DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->searchService->indexAllEntities();
|
||||
$this->searchIndex->indexAllEntities();
|
||||
DB::setDefaultConnection($connection);
|
||||
$this->comment('Search index regenerated');
|
||||
}
|
||||
|
||||
77
app/Console/Commands/ResetMfa.php
Normal file
77
app/Console/Commands/ResetMfa.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ResetMfa extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:reset-mfa
|
||||
{--id= : Numeric ID of the user to reset MFA for}
|
||||
{--email= : Email address of the user to reset MFA for}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Reset & Clear any configured MFA methods for the given user';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->option('id');
|
||||
$email = $this->option('email');
|
||||
if (!$id && !$email) {
|
||||
$this->error('Either a --id=<number> or --email=<email> option must be provided.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$field = $id ? 'id' : 'email';
|
||||
$value = $id ?: $email;
|
||||
$user = User::query()
|
||||
->where($field, '=', $value)
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
$this->error("A user where {$field}={$value} could not be found.");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("This will delete any configure multi-factor authentication methods for user: \n- ID: {$user->id}\n- Name: {$user->name}\n- Email: {$user->email}\n");
|
||||
$this->info('If multi-factor authentication is required for this user they will be asked to reconfigure their methods on next login.');
|
||||
$confirm = $this->confirm('Are you sure you want to proceed?');
|
||||
if ($confirm) {
|
||||
$user->mfaValues()->delete();
|
||||
$this->info('User MFA methods have been reset.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,8 @@ class UpdateUrl extends Command
|
||||
|
||||
$urlPattern = '/https?:\/\/(.+)/';
|
||||
if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) {
|
||||
$this->error("The given urls are expected to be full urls starting with http:// or https://");
|
||||
$this->error('The given urls are expected to be full urls starting with http:// or https://');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -57,25 +58,55 @@ class UpdateUrl extends Command
|
||||
}
|
||||
|
||||
$columnsToUpdateByTable = [
|
||||
"attachments" => ["path"],
|
||||
"pages" => ["html", "text", "markdown"],
|
||||
"images" => ["url"],
|
||||
"comments" => ["html", "text"],
|
||||
'attachments' => ['path'],
|
||||
'pages' => ['html', 'text', 'markdown'],
|
||||
'images' => ['url'],
|
||||
'settings' => ['value'],
|
||||
'comments' => ['html', 'text'],
|
||||
];
|
||||
|
||||
foreach ($columnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$changeCount = $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
|
||||
]);
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldUrl, $newUrl);
|
||||
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("URL update procedure complete.");
|
||||
$jsonColumnsToUpdateByTable = [
|
||||
'settings' => ['value'],
|
||||
];
|
||||
|
||||
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
$oldJson = trim(json_encode($oldUrl), '"');
|
||||
$newJson = trim(json_encode($newUrl), '"');
|
||||
$changeCount = $this->replaceValueInTable($table, $column, $oldJson, $newJson);
|
||||
$this->info("Updated {$changeCount} JSON encoded rows in {$table}->{$column}");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('URL update procedure complete.');
|
||||
$this->info('============================================================================');
|
||||
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
|
||||
$this->info('============================================================================');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a find+replace operations in the provided table and column.
|
||||
* Returns the count of rows changed.
|
||||
*/
|
||||
protected function replaceValueInTable(string $table, string $column, string $oldUrl, string $newUrl): int
|
||||
{
|
||||
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
|
||||
$newQuoted = $this->db->getPdo()->quote($newUrl);
|
||||
|
||||
return $this->db->table($table)->update([
|
||||
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn the user of the dangers of this operation.
|
||||
* Returns a boolean indicating if they've accepted the warnings.
|
||||
@@ -83,8 +114,8 @@ class UpdateUrl extends Command
|
||||
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
|
||||
{
|
||||
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
|
||||
$dangerWarning .= "Are you sure you want to proceed?";
|
||||
$backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?";
|
||||
$dangerWarning .= 'Are you sure you want to proceed?';
|
||||
$backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?';
|
||||
|
||||
return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class UpgradeDatabaseEncoding extends Command
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
@@ -44,12 +43,12 @@ class UpgradeDatabaseEncoding extends Command
|
||||
|
||||
$database = DB::getDatabaseName();
|
||||
$tables = DB::select('SHOW TABLES');
|
||||
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
$this->line('USE `'.$database.'`;');
|
||||
$this->line('ALTER DATABASE `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
$this->line('USE `' . $database . '`;');
|
||||
$key = 'Tables_in_' . $database;
|
||||
foreach ($tables as $table) {
|
||||
$tableName = $table->$key;
|
||||
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
$this->line('ALTER TABLE `' . $tableName . '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
}
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Console;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@@ -17,7 +19,8 @@ class Kernel extends ConsoleKernel
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
@@ -32,6 +35,6 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
use BookStack\Entities\Managers\EntityContext;
|
||||
namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BreadcrumbsViewComposer
|
||||
{
|
||||
|
||||
protected $entityContextManager;
|
||||
|
||||
/**
|
||||
* BreadcrumbsViewComposer constructor.
|
||||
* @param EntityContext $entityContextManager
|
||||
*
|
||||
* @param ShelfContext $entityContextManager
|
||||
*/
|
||||
public function __construct(EntityContext $entityContextManager)
|
||||
public function __construct(ShelfContext $entityContextManager)
|
||||
{
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify data when the view is composed.
|
||||
*
|
||||
* @param View $view
|
||||
*/
|
||||
public function compose(View $view)
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class Chapter
|
||||
* @property Collection<Page> $pages
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class Chapter extends BookChild
|
||||
{
|
||||
public $searchFactor = 1.3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
protected $hidden = ['restricted', 'pivot'];
|
||||
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
* @param string $dir
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages($dir = 'ASC')
|
||||
{
|
||||
return $this->hasMany(Page::class)->orderBy('priority', $dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
$fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
|
||||
|
||||
if ($path !== false) {
|
||||
$fullPath .= '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
return url($fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this chapter's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->text ?? $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this chapter has any child pages.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren()
|
||||
{
|
||||
return count($this->pages) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible pages in this chapter.
|
||||
*/
|
||||
public function getVisiblePages(): Collection
|
||||
{
|
||||
return $this->pages()->visible()
|
||||
->orderBy('draft', 'desc')
|
||||
->orderBy('priority', 'asc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Models\PageRevision;
|
||||
|
||||
/**
|
||||
* Class EntityProvider
|
||||
* Class EntityProvider.
|
||||
*
|
||||
* Provides access to the core entity models.
|
||||
* Wrapped up in this provider since they are often used together
|
||||
* so this is a neater alternative to injecting all in individually.
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class EntityProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Bookshelf
|
||||
*/
|
||||
@@ -37,34 +43,28 @@ class EntityProvider
|
||||
*/
|
||||
public $pageRevision;
|
||||
|
||||
/**
|
||||
* EntityProvider constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
Bookshelf $bookshelf,
|
||||
Book $book,
|
||||
Chapter $chapter,
|
||||
Page $page,
|
||||
PageRevision $pageRevision
|
||||
) {
|
||||
$this->bookshelf = $bookshelf;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
public function __construct()
|
||||
{
|
||||
$this->bookshelf = new Bookshelf();
|
||||
$this->book = new Book();
|
||||
$this->chapter = new Chapter();
|
||||
$this->page = new Page();
|
||||
$this->pageRevision = new PageRevision();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all core entity types as an associated array
|
||||
* with their basic names as the keys.
|
||||
*
|
||||
* @return array<Entity>
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return [
|
||||
'bookshelf' => $this->bookshelf,
|
||||
'book' => $this->book,
|
||||
'chapter' => $this->chapter,
|
||||
'page' => $this->page,
|
||||
'book' => $this->book,
|
||||
'chapter' => $this->chapter,
|
||||
'page' => $this->page,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class EntityProvider
|
||||
public function get(string $type): Entity
|
||||
{
|
||||
$type = strtolower($type);
|
||||
|
||||
return $this->all()[$type];
|
||||
}
|
||||
|
||||
@@ -87,6 +88,7 @@ class EntityProvider
|
||||
$model = $this->get($type);
|
||||
$morphClasses[] = $model->getMorphClass();
|
||||
}
|
||||
|
||||
return $morphClasses;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<?php namespace BookStack\Entities\Managers;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\HasCoverImage;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Facades\Activity;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
|
||||
class TrashCan
|
||||
{
|
||||
|
||||
/**
|
||||
* Remove a bookshelf from the system.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyShelf(Bookshelf $shelf)
|
||||
{
|
||||
$this->destroyCommonRelations($shelf);
|
||||
$shelf->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a book from the system.
|
||||
* @throws NotifyException
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function destroyBook(Book $book)
|
||||
{
|
||||
foreach ($book->pages as $page) {
|
||||
$this->destroyPage($page);
|
||||
}
|
||||
|
||||
foreach ($book->chapters as $chapter) {
|
||||
$this->destroyChapter($chapter);
|
||||
}
|
||||
|
||||
$this->destroyCommonRelations($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a page from the system.
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
// Check if set as custom homepage & remove setting if not used or throw error if active
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
if (setting('app-homepage-type') === 'page') {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
}
|
||||
setting()->remove('app-homepage');
|
||||
}
|
||||
|
||||
$this->destroyCommonRelations($page);
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
$attachmentService->deleteFile($attachment);
|
||||
}
|
||||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a chapter from the system.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyChapter(Chapter $chapter)
|
||||
{
|
||||
if (count($chapter->pages) > 0) {
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->destroyCommonRelations($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity relations to remove or update outstanding connections.
|
||||
*/
|
||||
protected function destroyCommonRelations(Entity $entity)
|
||||
{
|
||||
Activity::removeEntity($entity);
|
||||
$entity->views()->delete();
|
||||
$entity->permissions()->delete();
|
||||
$entity->tags()->delete();
|
||||
$entity->comments()->delete();
|
||||
$entity->jointPermissions()->delete();
|
||||
$entity->searchTerms()->delete();
|
||||
|
||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
||||
$imageService = app()->make(ImageService::class);
|
||||
$imageService->destroy($entity->cover);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
use Exception;
|
||||
@@ -8,36 +10,33 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class Book
|
||||
* @property string $description
|
||||
* @property int $image_id
|
||||
* Class Book.
|
||||
*
|
||||
* @property string $description
|
||||
* @property int $image_id
|
||||
* @property Image|null $cover
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class Book extends Entity implements HasCoverImage
|
||||
{
|
||||
public $searchFactor = 2;
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
protected $hidden = ['restricted', 'pivot', 'image_id'];
|
||||
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
public function getUrl(string $path = ''): string
|
||||
{
|
||||
if ($path !== false) {
|
||||
return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return url('/books/' . urlencode($this->slug));
|
||||
return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns book cover image, if book cover not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
*
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
@@ -52,11 +51,12 @@ class Book extends Entity implements HasCoverImage
|
||||
} catch (Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the book
|
||||
* Get the cover image of the book.
|
||||
*/
|
||||
public function cover(): BelongsTo
|
||||
{
|
||||
@@ -73,6 +73,7 @@ class Book extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function pages()
|
||||
@@ -82,6 +83,7 @@ class Book extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get the direct child pages of this book.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function directPages()
|
||||
@@ -91,6 +93,7 @@ class Book extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
@@ -100,6 +103,7 @@ class Book extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get the shelves this book is contained within.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function shelves()
|
||||
@@ -109,23 +113,14 @@ class Book extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get the direct child items within this book.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getDirectChildren(): Collection
|
||||
{
|
||||
$pages = $this->directPages()->visible()->get();
|
||||
$chapters = $this->chapters()->visible()->get();
|
||||
|
||||
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,38 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Class BookChild
|
||||
* @property int $book_id
|
||||
* @property int $priority
|
||||
* @property Book $book
|
||||
* Class BookChild.
|
||||
*
|
||||
* @property int $book_id
|
||||
* @property int $priority
|
||||
* @property string $book_slug
|
||||
* @property Book $book
|
||||
*
|
||||
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
|
||||
*/
|
||||
class BookChild extends Entity
|
||||
abstract class BookChild extends Entity
|
||||
{
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Load book slugs onto these models by default during query-time
|
||||
static::addGlobalScope('book_slug', function (Builder $builder) {
|
||||
$builder->addSelect(['book_slug' => function ($builder) {
|
||||
$builder->select('slug')
|
||||
->from('books')
|
||||
->whereColumn('books.id', '=', 'book_id');
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to find items where the the child has the given childSlug
|
||||
* Scope a query to find items where the child has the given childSlug
|
||||
* where its parent has the bookSlug.
|
||||
*/
|
||||
public function scopeWhereSlugs(Builder $query, string $bookSlug, string $childSlug)
|
||||
@@ -28,11 +46,10 @@ class BookChild extends Entity
|
||||
|
||||
/**
|
||||
* Get the book this page sits in.
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function book(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Book::class);
|
||||
return $this->belongsTo(Book::class)->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,12 +62,9 @@ class BookChild extends Entity
|
||||
$this->save();
|
||||
$this->refresh();
|
||||
|
||||
// Update related activity
|
||||
$this->activity()->update(['book_id' => $newBookId]);
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if ($this instanceof Chapter) {
|
||||
foreach ($this->pages as $page) {
|
||||
foreach ($this->pages()->withTrashed()->get() as $page) {
|
||||
$page->changeBook($newBookId);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -12,11 +14,12 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
protected $hidden = ['restricted', 'image_id'];
|
||||
protected $hidden = ['restricted', 'image_id', 'deleted_at'];
|
||||
|
||||
/**
|
||||
* Get the books in this shelf.
|
||||
* Should not be used directly since does not take into account permissions.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function books()
|
||||
@@ -36,21 +39,18 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Get the url for this bookshelf.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
public function getUrl(string $path = ''): string
|
||||
{
|
||||
if ($path !== false) {
|
||||
return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return url('/shelves/' . urlencode($this->slug));
|
||||
return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
*
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
@@ -66,11 +66,12 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
} catch (\Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the shelf
|
||||
* Get the cover image of the shelf.
|
||||
*/
|
||||
public function cover(): BelongsTo
|
||||
{
|
||||
@@ -85,20 +86,11 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
return 'cover_shelf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this shelf contains the given book.
|
||||
*
|
||||
* @param Book $book
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function contains(Book $book): bool
|
||||
@@ -108,6 +100,7 @@ class Bookshelf extends Entity implements HasCoverImage
|
||||
|
||||
/**
|
||||
* Add a book to the end of this shelf.
|
||||
*
|
||||
* @param Book $book
|
||||
*/
|
||||
public function appendBook(Book $book)
|
||||
58
app/Entities/Models/Chapter.php
Normal file
58
app/Entities/Models/Chapter.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class Chapter.
|
||||
*
|
||||
* @property Collection<Page> $pages
|
||||
* @property mixed description
|
||||
*/
|
||||
class Chapter extends BookChild
|
||||
{
|
||||
public $searchFactor = 1.3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
|
||||
|
||||
/**
|
||||
* Get the pages that this chapter contains.
|
||||
*
|
||||
* @param string $dir
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function pages($dir = 'ASC')
|
||||
{
|
||||
return $this->hasMany(Page::class)->orderBy('priority', $dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this chapter.
|
||||
*/
|
||||
public function getUrl($path = ''): string
|
||||
{
|
||||
$parts = [
|
||||
'books',
|
||||
urlencode($this->book_slug ?? $this->book->slug),
|
||||
'chapter',
|
||||
urlencode($this->slug),
|
||||
trim($path, '/'),
|
||||
];
|
||||
|
||||
return url('/' . implode('/', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible pages in this chapter.
|
||||
*/
|
||||
public function getVisiblePages(): Collection
|
||||
{
|
||||
return $this->pages()->visible()
|
||||
->orderBy('draft', 'desc')
|
||||
->orderBy('priority', 'asc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
61
app/Entities/Models/Deletion.php
Normal file
61
app/Entities/Models/Deletion.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* @property Model deletable
|
||||
*/
|
||||
class Deletion extends Model implements Loggable
|
||||
{
|
||||
/**
|
||||
* Get the related deletable record.
|
||||
*/
|
||||
public function deletable(): MorphTo
|
||||
{
|
||||
return $this->morphTo('deletable')->withTrashed();
|
||||
}
|
||||
|
||||
/**
|
||||
* The the user that performed the deletion.
|
||||
*/
|
||||
public function deleter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'deleted_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new deletion record for the provided entity.
|
||||
*/
|
||||
public static function createForEntity(Entity $entity): Deletion
|
||||
{
|
||||
$record = (new self())->forceFill([
|
||||
'deleted_by' => user()->id,
|
||||
'deletable_type' => $entity->getMorphClass(),
|
||||
'deletable_id' => $entity->id,
|
||||
]);
|
||||
$record->save();
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
public function logDescriptor(): string
|
||||
{
|
||||
$deletable = $this->deletable()->first();
|
||||
|
||||
return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a URL for this specific deletion.
|
||||
*/
|
||||
public function getUrl($path): string
|
||||
{
|
||||
return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/'));
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,54 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Actions\Activity;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Class Entity
|
||||
* The base class for book-like items such as pages, chapters & books.
|
||||
* This is not a database model in itself but extended.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property boolean $restricted
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property bool $restricted
|
||||
* @property Collection $tags
|
||||
*
|
||||
* @method static Entity|Builder visible()
|
||||
* @method static Entity|Builder hasPermission(string $permission)
|
||||
* @method static Builder withLastView()
|
||||
* @method static Builder withViewCount()
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class Entity extends Ownable
|
||||
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasCreatorAndUpdater;
|
||||
use HasOwner;
|
||||
|
||||
/**
|
||||
* @var string - Name of property where the main text content is found
|
||||
@@ -50,7 +63,7 @@ class Entity extends Ownable
|
||||
/**
|
||||
* Get the entities that are visible to the current user.
|
||||
*/
|
||||
public function scopeVisible(Builder $query)
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
return $this->scopeHasPermission($query, 'view');
|
||||
}
|
||||
@@ -92,24 +105,18 @@ class Entity extends Ownable
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
* @param $entity
|
||||
* @return bool
|
||||
*/
|
||||
public function matches($entity)
|
||||
public function matches(Entity $entity): bool
|
||||
{
|
||||
return [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an entity matches or contains another given entity.
|
||||
* @param Entity $entity
|
||||
* @return bool
|
||||
* Checks if the current entity matches or contains the given.
|
||||
*/
|
||||
public function matchesOrContains(Entity $entity)
|
||||
public function matchesOrContains(Entity $entity): bool
|
||||
{
|
||||
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||
|
||||
if ($matches) {
|
||||
if ($this->matches($entity)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,9 +133,8 @@ class Entity extends Ownable
|
||||
|
||||
/**
|
||||
* Gets the activity objects for this entity.
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function activity()
|
||||
public function activity(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Activity::class, 'entity')
|
||||
->orderBy('created_at', 'desc');
|
||||
@@ -137,36 +143,33 @@ class Entity extends Ownable
|
||||
/**
|
||||
* Get View objects for this entity.
|
||||
*/
|
||||
public function views()
|
||||
public function views(): MorphMany
|
||||
{
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function tags()
|
||||
public function tags(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for an entity
|
||||
* @param bool $orderByCreated
|
||||
* @return MorphMany
|
||||
* Get the comments for an entity.
|
||||
*/
|
||||
public function comments($orderByCreated = true)
|
||||
public function comments(bool $orderByCreated = true): MorphMany
|
||||
{
|
||||
$query = $this->morphMany(Comment::class, 'entity');
|
||||
|
||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function searchTerms()
|
||||
public function searchTerms(): MorphMany
|
||||
{
|
||||
return $this->morphMany(SearchTerm::class, 'entity');
|
||||
}
|
||||
@@ -174,18 +177,15 @@ class Entity extends Ownable
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
public function permissions()
|
||||
public function permissions(): MorphMany
|
||||
{
|
||||
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)
|
||||
public function hasRestriction(int $role_id, string $action): bool
|
||||
{
|
||||
return $this->permissions()->where('role_id', '=', $role_id)
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
@@ -193,16 +193,23 @@ class Entity extends Ownable
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
public function jointPermissions(): MorphMany
|
||||
{
|
||||
return $this->morphMany(JointPermission::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related delete records for this entity.
|
||||
*/
|
||||
public function deletions(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Deletion::class, 'deletable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this instance or class is a certain type of entity.
|
||||
* Examples of $type are 'page', 'book', 'chapter'
|
||||
* Examples of $type are 'page', 'book', 'chapter'.
|
||||
*/
|
||||
public static function isA(string $type): bool
|
||||
{
|
||||
@@ -210,28 +217,13 @@ class Entity extends Ownable
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity type.
|
||||
* @return mixed
|
||||
* Get the entity type as a simple lowercase word.
|
||||
*/
|
||||
public static function getType()
|
||||
public static function getType(): string
|
||||
{
|
||||
return strtolower(static::getClassName());
|
||||
}
|
||||
$className = array_slice(explode('\\', static::class), -1, 1)[0];
|
||||
|
||||
/**
|
||||
* Get an instance of an entity of the given type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\Entities\\' . $className);
|
||||
return strtolower($className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,40 +234,52 @@ class Entity extends Ownable
|
||||
if (mb_strlen($this->name) <= $length) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
return mb_substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body text of this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getText()
|
||||
public function getText(): string
|
||||
{
|
||||
return $this->{$this->textField};
|
||||
return $this->{$this->textField} ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this entity's descriptive content to the specified length.
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
public function getExcerpt(int $length = 100): string
|
||||
{
|
||||
$text = $this->getText();
|
||||
|
||||
if (mb_strlen($text) > $length) {
|
||||
$text = mb_substr($text, 0, $length-3) . '...';
|
||||
$text = mb_substr($text, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this entity
|
||||
* @param $path
|
||||
* @return string
|
||||
* Get the url of this entity.
|
||||
*/
|
||||
public function getUrl($path = '/')
|
||||
abstract public function getUrl(string $path = '/'): string;
|
||||
|
||||
/**
|
||||
* Get the parent entity if existing.
|
||||
* This is the "static" parent and does not include dynamic
|
||||
* relations such as shelves to books.
|
||||
*/
|
||||
public function getParent(): ?Entity
|
||||
{
|
||||
return $path;
|
||||
if ($this instanceof Page) {
|
||||
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
|
||||
}
|
||||
if ($this instanceof Chapter) {
|
||||
return $this->book()->withTrashed()->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,21 +292,38 @@ class Entity extends Ownable
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the current entity for search
|
||||
* Index the current entity for search.
|
||||
*/
|
||||
public function indexForSearch()
|
||||
{
|
||||
$searchService = app()->make(SearchService::class);
|
||||
$searchService->indexEntity(clone $this);
|
||||
app(SearchIndex::class)->indexEntity(clone $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and set a new URL slug for this model.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function refreshSlug(): string
|
||||
{
|
||||
$generator = new SlugGenerator($this);
|
||||
$this->slug = $generator->generate();
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function favourites(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Favourite::class, 'favouritable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the entity is a favourite of the current user.
|
||||
*/
|
||||
public function isFavourite(): bool
|
||||
{
|
||||
return $this->favourites()
|
||||
->where('user_id', '=', user()->id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace BookStack\Entities;
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
interface HasCoverImage
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the cover image for this item.
|
||||
*/
|
||||
140
app/Entities/Models/Page.php
Normal file
140
app/Entities/Models/Page.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Permissions;
|
||||
|
||||
/**
|
||||
* Class Page.
|
||||
*
|
||||
* @property int $chapter_id
|
||||
* @property string $html
|
||||
* @property string $markdown
|
||||
* @property string $text
|
||||
* @property bool $template
|
||||
* @property bool $draft
|
||||
* @property int $revision_count
|
||||
* @property Chapter $chapter
|
||||
* @property Collection $attachments
|
||||
*/
|
||||
class Page extends BookChild
|
||||
{
|
||||
public static $listAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'text', 'created_at', 'updated_at'];
|
||||
public static $contentAttributes = ['name', 'id', 'slug', 'book_id', 'chapter_id', 'draft', 'template', 'html', 'text', 'created_at', 'updated_at'];
|
||||
|
||||
protected $fillable = ['name', 'priority', 'markdown'];
|
||||
|
||||
public $textField = 'text';
|
||||
|
||||
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
|
||||
|
||||
protected $casts = [
|
||||
'draft' => 'boolean',
|
||||
'template' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the entities that are visible to the current user.
|
||||
*/
|
||||
public function scopeVisible(Builder $query): Builder
|
||||
{
|
||||
$query = Permissions::enforceDraftVisibilityOnQuery($query);
|
||||
|
||||
return parent::scopeVisible($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function 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.
|
||||
* Only provides actual saved page revision instances, Not drafts.
|
||||
*/
|
||||
public function revisions(): HasMany
|
||||
{
|
||||
return $this->allRevisions()
|
||||
->where('type', '=', 'version')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all revision instances assigned to this page.
|
||||
* Includes all types of revisions.
|
||||
*/
|
||||
public function allRevisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageRevision::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments assigned to this page.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this page.
|
||||
*/
|
||||
public function getUrl($path = ''): string
|
||||
{
|
||||
$parts = [
|
||||
'books',
|
||||
urlencode($this->book_slug ?? $this->book->slug),
|
||||
$this->draft ? 'draft' : 'page',
|
||||
$this->draft ? $this->id : urlencode($this->slug),
|
||||
trim($path, '/'),
|
||||
];
|
||||
|
||||
return url('/' . implode('/', $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing.
|
||||
*
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function getCurrentRevision()
|
||||
{
|
||||
return $this->revisions()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this page for JSON display.
|
||||
*/
|
||||
public function forJsonDisplay(): Page
|
||||
{
|
||||
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
|
||||
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
|
||||
$refreshed->html = (new PageContent($refreshed))->render();
|
||||
|
||||
return $refreshed;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,32 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Class PageRevision
|
||||
* @property int $page_id
|
||||
* Class PageRevision.
|
||||
*
|
||||
* @property int $page_id
|
||||
* @property string $slug
|
||||
* @property string $book_slug
|
||||
* @property int $created_by
|
||||
* @property int $created_by
|
||||
* @property Carbon $created_at
|
||||
* @property string $type
|
||||
* @property string $summary
|
||||
* @property string $markdown
|
||||
* @property string $html
|
||||
* @property int $revision_number
|
||||
* @property int $revision_number
|
||||
*/
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
|
||||
/**
|
||||
* Get the user that created the page revision
|
||||
* Get the user that created the page revision.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function createdBy()
|
||||
@@ -32,6 +36,7 @@ class PageRevision extends Model
|
||||
|
||||
/**
|
||||
* Get the page this revision originates from.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
@@ -41,7 +46,9 @@ class PageRevision extends Model
|
||||
|
||||
/**
|
||||
* Get the url for this revision.
|
||||
*
|
||||
* @param null|string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = null)
|
||||
@@ -50,11 +57,13 @@ class PageRevision extends Model
|
||||
if ($path) {
|
||||
return $url . '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the previous revision for the same page if existing
|
||||
* Get the previous revision for the same page if existing.
|
||||
*
|
||||
* @return \BookStack\Entities\PageRevision|null
|
||||
*/
|
||||
public function getPrevious()
|
||||
@@ -73,8 +82,10 @@ class PageRevision extends Model
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Included here to align with entities in similar use cases.
|
||||
* (Yup, Bit of an awkward hack)
|
||||
* (Yup, Bit of an awkward hack).
|
||||
*
|
||||
* @param $type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isA($type)
|
||||
@@ -1,15 +1,17 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Models;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the entity that this term belongs to
|
||||
* Get the entity that this term belongs to.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
@@ -1,123 +0,0 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Attachment;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Permissions;
|
||||
|
||||
/**
|
||||
* Class Page
|
||||
* @property int $chapter_id
|
||||
* @property string $html
|
||||
* @property string $markdown
|
||||
* @property string $text
|
||||
* @property bool $template
|
||||
* @property bool $draft
|
||||
* @property int $revision_count
|
||||
* @property Chapter $chapter
|
||||
* @property Collection $attachments
|
||||
*/
|
||||
class Page extends BookChild
|
||||
{
|
||||
protected $fillable = ['name', 'priority', 'markdown'];
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
public $textField = 'text';
|
||||
|
||||
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
|
||||
|
||||
/**
|
||||
* Get the entities that are visible to the current user.
|
||||
*/
|
||||
public function scopeVisible(Builder $query)
|
||||
{
|
||||
$query = Permissions::enforceDraftVisiblityOnQuery($query);
|
||||
return parent::scopeVisible($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
*/
|
||||
public function toSimpleArray()
|
||||
{
|
||||
$array = array_intersect_key($this->toArray(), array_flip($this->simpleAttributes));
|
||||
$array['url'] = $this->getUrl();
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent item
|
||||
*/
|
||||
public function parent(): Entity
|
||||
{
|
||||
return $this->chapter_id ? $this->chapter : $this->book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function 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(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments assigned to this page.
|
||||
* @return HasMany
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
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);
|
||||
|
||||
$url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
|
||||
if ($path !== false) {
|
||||
$url .= '/' . trim($path, '/');
|
||||
}
|
||||
|
||||
return url($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function getCurrentRevision()
|
||||
{
|
||||
return $this->revisions()->first();
|
||||
}
|
||||
}
|
||||
19
app/Entities/Queries/EntityQuery.php
Normal file
19
app/Entities/Queries/EntityQuery.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
|
||||
abstract class EntityQuery
|
||||
{
|
||||
protected function permissionService(): PermissionService
|
||||
{
|
||||
return app()->make(PermissionService::class);
|
||||
}
|
||||
|
||||
protected function entityProvider(): EntityProvider
|
||||
{
|
||||
return app()->make(EntityProvider::class);
|
||||
}
|
||||
}
|
||||
29
app/Entities/Queries/Popular.php
Normal file
29
app/Entities/Queries/Popular.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Popular extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
34
app/Entities/Queries/RecentlyViewed.php
Normal file
34
app/Entities/Queries/RecentlyViewed.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RecentlyViewed extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page): Collection
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()->filterRestrictedEntityRelations(
|
||||
View::query(),
|
||||
'views',
|
||||
'viewable_id',
|
||||
'viewable_type',
|
||||
'view'
|
||||
)
|
||||
->orderBy('views.updated_at', 'desc')
|
||||
->where('user_id', '=', user()->id);
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip(($page - 1) * $count)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
35
app/Entities/Queries/TopFavourites.php
Normal file
35
app/Entities/Queries/TopFavourites.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class TopFavourites extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $skip = 0)
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
|
||||
->select('favourites.*')
|
||||
->leftJoin('views', function (JoinClause $join) {
|
||||
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
|
||||
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
|
||||
$join->where('views.user_id', '=', user()->id);
|
||||
})
|
||||
->orderBy('views.views', 'desc')
|
||||
->where('favourites.user_id', '=', user()->id);
|
||||
|
||||
return $query->with('favouritable')
|
||||
->skip($skip)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('favouritable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,17 @@
|
||||
namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Actions\TagRepo;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\HasCoverImage;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\HasCoverImage;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BaseRepo
|
||||
{
|
||||
|
||||
protected $tagRepo;
|
||||
protected $imageRepo;
|
||||
|
||||
|
||||
/**
|
||||
* BaseRepo constructor.
|
||||
* @param $tagRepo
|
||||
*/
|
||||
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
|
||||
{
|
||||
$this->tagRepo = $tagRepo;
|
||||
@@ -29,7 +21,7 @@ class BaseRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity in the system
|
||||
* Create a new entity in the system.
|
||||
*/
|
||||
public function create(Entity $entity, array $input)
|
||||
{
|
||||
@@ -37,6 +29,7 @@ class BaseRepo
|
||||
$entity->forceFill([
|
||||
'created_by' => user()->id,
|
||||
'updated_by' => user()->id,
|
||||
'owned_by' => user()->id,
|
||||
]);
|
||||
$entity->refreshSlug();
|
||||
$entity->save();
|
||||
@@ -73,6 +66,7 @@ class BaseRepo
|
||||
|
||||
/**
|
||||
* Update the given items' cover image, or clear it.
|
||||
*
|
||||
* @throws ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
@@ -91,29 +85,4 @@ class BaseRepo
|
||||
$entity->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the permissions of an entity.
|
||||
*/
|
||||
public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
|
||||
{
|
||||
$entity->restricted = $restricted;
|
||||
$entity->permissions()->delete();
|
||||
|
||||
if (!is_null($permissions)) {
|
||||
$entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
|
||||
return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
|
||||
return [
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action),
|
||||
] ;
|
||||
});
|
||||
});
|
||||
|
||||
$entity->permissions()->createMany($entityPermissionData);
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$entity->rebuildPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user