mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-08 11:19:36 +03:00
Compare commits
572 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0381f76bf | ||
|
|
3cd08382e9 | ||
|
|
6102f66daa | ||
|
|
c6134d162d | ||
|
|
4f788384f0 | ||
|
|
23f90ed6b4 | ||
|
|
f1586be516 | ||
|
|
1a9f676416 | ||
|
|
df1a3a0715 | ||
|
|
1e015af3c9 | ||
|
|
f101c1a622 | ||
|
|
3df7d828eb | ||
|
|
5ad9c5d319 | ||
|
|
9fead9890b | ||
|
|
746684ec8c | ||
|
|
2ede273ef3 | ||
|
|
6882bd3c62 | ||
|
|
1061946858 | ||
|
|
696ef3ff33 | ||
|
|
2d1567ea30 | ||
|
|
2cfcbe0a3c | ||
|
|
bf8dddd99c | ||
|
|
0335f58478 | ||
|
|
3a5c20c17e | ||
|
|
380e2ff668 | ||
|
|
c6844324d0 | ||
|
|
ecdeb545e0 | ||
|
|
2c8d7da885 | ||
|
|
35c7e00203 | ||
|
|
83d830fd7d | ||
|
|
accd936781 | ||
|
|
880987f15c | ||
|
|
018084a951 | ||
|
|
098b594104 | ||
|
|
bb7fab1dc0 | ||
|
|
d859be3a12 | ||
|
|
8b27ce3296 | ||
|
|
8828adfc9c | ||
|
|
d44e0b7964 | ||
|
|
0372efa89a | ||
|
|
d2eec4fbce | ||
|
|
b42b07179f | ||
|
|
1ad6fe1cbd | ||
|
|
0a1546daea | ||
|
|
b64940be82 | ||
|
|
2ff2c0b257 | ||
|
|
ced4e58137 | ||
|
|
f42d355fd7 | ||
|
|
28a5bd24b0 | ||
|
|
85605ab570 | ||
|
|
5e41933773 | ||
|
|
f6f44c9de8 | ||
|
|
e52bfc0c24 | ||
|
|
c44c42103c | ||
|
|
1e98759722 | ||
|
|
bf4b95f929 | ||
|
|
a4072365e3 | ||
|
|
2bd977b7d7 | ||
|
|
496289ad94 | ||
|
|
98a3c815cf | ||
|
|
01e03f5a0f | ||
|
|
1c8a8acb3d | ||
|
|
70cfb6624d | ||
|
|
fb48b025f3 | ||
|
|
9a88b2cd0c | ||
|
|
395d02ef81 | ||
|
|
67332a2f1b | ||
|
|
81fa021083 | ||
|
|
5ab39bfd5a | ||
|
|
dc1a16be4c | ||
|
|
2046f9b9de | ||
|
|
ac3ba594a4 | ||
|
|
981d215155 | ||
|
|
2d43ab8a1b | ||
|
|
548dcd4db1 | ||
|
|
2d41a4f064 | ||
|
|
110f32a16d | ||
|
|
bed7ba78d3 | ||
|
|
2533db260d | ||
|
|
87a45edde9 | ||
|
|
9becc8055b | ||
|
|
d84f75c257 | ||
|
|
1d49b65c2e | ||
|
|
7c44f5462c | ||
|
|
b7e5cc6763 | ||
|
|
ab3231b550 | ||
|
|
d65cd53c99 | ||
|
|
46ea90c36e | ||
|
|
a45922616f | ||
|
|
ecf68b6365 | ||
|
|
194bb0f042 | ||
|
|
addfb96002 | ||
|
|
6f7cfe7206 | ||
|
|
f51e0e9eb9 | ||
|
|
3cf2c6a027 | ||
|
|
8b125be8f6 | ||
|
|
44d8f39037 | ||
|
|
1651c807cb | ||
|
|
5e2bf7c3e4 | ||
|
|
1d5440493c | ||
|
|
59e809be16 | ||
|
|
ec050a5eef | ||
|
|
62342433f4 | ||
|
|
30b4f81fc6 | ||
|
|
bd711d69e4 | ||
|
|
98d4bf4486 | ||
|
|
ead4b14d94 | ||
|
|
35e00ddb95 | ||
|
|
4eb5205070 | ||
|
|
1d1cc19596 | ||
|
|
faf7c55fdd | ||
|
|
ba6eb6727a | ||
|
|
88d09a2a3b | ||
|
|
daa11c3f13 | ||
|
|
682bc9f896 | ||
|
|
9bbef3a3dd | ||
|
|
1411ee86b3 | ||
|
|
56264551e7 | ||
|
|
0c383eee5b | ||
|
|
f4bfbf91db | ||
|
|
34782fbc91 | ||
|
|
1bfd77e7a1 | ||
|
|
5b075aa9bd | ||
|
|
281da59bae | ||
|
|
0afa417b0a | ||
|
|
f2c62765ca | ||
|
|
a77756a2da | ||
|
|
6988a6ff88 | ||
|
|
e269cc7ea7 | ||
|
|
e13e71cbe0 | ||
|
|
4a24d1c31b | ||
|
|
96b8c403a8 | ||
|
|
359b1b40a2 | ||
|
|
920964a561 | ||
|
|
7bb336d1a8 | ||
|
|
141bf22725 | ||
|
|
1aa4d0dc59 | ||
|
|
0c1b1cd435 | ||
|
|
3eb2246291 | ||
|
|
937d2cd55c | ||
|
|
afe781bc39 | ||
|
|
d5a2529775 | ||
|
|
0d4db603a4 | ||
|
|
7da8804753 | ||
|
|
8b160b9fb4 | ||
|
|
0dc1f0b07f | ||
|
|
22df25a480 | ||
|
|
8b30c7f02e | ||
|
|
03eb63ec77 | ||
|
|
3ed5426315 | ||
|
|
90bf13c1ab | ||
|
|
d17eb0f54c | ||
|
|
ac7e3977de | ||
|
|
d7edc389a6 | ||
|
|
56d5af1336 | ||
|
|
06cf175b08 | ||
|
|
b65abd25e0 | ||
|
|
a5e49f642b | ||
|
|
91444e83fd | ||
|
|
6063ac4a11 | ||
|
|
02fd1c48ed | ||
|
|
6ee35f55cc | ||
|
|
261e57fc4e | ||
|
|
bc1302a8d8 | ||
|
|
eeb2b8cbe5 | ||
|
|
b167ae795e | ||
|
|
6ebe8bf619 | ||
|
|
009af9736e | ||
|
|
7668a999a2 | ||
|
|
ed88c623d6 | ||
|
|
873b1099f8 | ||
|
|
6a54733f2b | ||
|
|
7a5bd23909 | ||
|
|
6bb7b5465f | ||
|
|
0b967d84ad | ||
|
|
2261308415 | ||
|
|
7b5edb4d62 | ||
|
|
8378f06889 | ||
|
|
10dc851697 | ||
|
|
757cdddc7c | ||
|
|
65579214e2 | ||
|
|
d89440d198 | ||
|
|
08e58bab79 | ||
|
|
d29b177c84 | ||
|
|
151d72e42c | ||
|
|
711ba258f1 | ||
|
|
df4d4f30f1 | ||
|
|
f094837709 | ||
|
|
e27cbb9dce | ||
|
|
bdba25b6f2 | ||
|
|
6b2581de63 | ||
|
|
1031c61d0c | ||
|
|
46fc0e5026 | ||
|
|
332f678ed0 | ||
|
|
df95e99680 | ||
|
|
5a6d544db7 | ||
|
|
0d5d77d8ab | ||
|
|
db51cee2d8 | ||
|
|
a988438946 | ||
|
|
3bf7cac030 | ||
|
|
79c3a07e9a | ||
|
|
16117d329c | ||
|
|
9758872baf | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
b711bc6816 | ||
|
|
247e6dba85 | ||
|
|
2b3d6e4e4a | ||
|
|
6b1980c4f3 | ||
|
|
9ba29770e1 | ||
|
|
3d375fae55 | ||
|
|
c99a50de2c | ||
|
|
1a32b25b5e | ||
|
|
481aa5b5b0 | ||
|
|
c943eb4d0d | ||
|
|
aca6de49b0 | ||
|
|
5fd04fa470 | ||
|
|
87339e4cd0 | ||
|
|
a9eb058dad | ||
|
|
61fad6a665 | ||
|
|
fa4bee2d98 | ||
|
|
ce63260fa6 | ||
|
|
a3557d5bb2 | ||
|
|
9ca22976c3 | ||
|
|
9e2934fe17 | ||
|
|
2259263214 | ||
|
|
762cf5f183 | ||
|
|
07175f2b3e | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
0c4ddf16a5 | ||
|
|
74a5e3113e | ||
|
|
df0a982433 | ||
|
|
212f924ffa | ||
|
|
09936566dd | ||
|
|
9469c04eab | ||
|
|
941bb73a68 | ||
|
|
8e652f5d8f | ||
|
|
eec1b21928 | ||
|
|
1baeb7bec9 | ||
|
|
61475ca0a3 | ||
|
|
244c5a3ebb | ||
|
|
39e7ac1c15 | ||
|
|
ab7f5def04 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
cd7e727f8c | ||
|
|
2329a5cedf | ||
|
|
d1a4ff9308 | ||
|
|
9a3bc27ef4 | ||
|
|
f8315fb9c4 | ||
|
|
bb62dee5a2 | ||
|
|
8acc188e16 | ||
|
|
874386ceab | ||
|
|
9dfbea8bf9 | ||
|
|
c1627a1468 | ||
|
|
576a59a693 | ||
|
|
fec6c65b78 | ||
|
|
f8c046d182 | ||
|
|
9ca2184f09 | ||
|
|
fd449582bd | ||
|
|
621142a46e | ||
|
|
0275d2ad58 | ||
|
|
41f56e659d | ||
|
|
5034f21394 | ||
|
|
e02fcbe983 | ||
|
|
1c88d21abf | ||
|
|
c1a1bc0135 | ||
|
|
fea5630ea4 | ||
|
|
e3f2bde26d | ||
|
|
756ee0b172 | ||
|
|
c81b63b56f | ||
|
|
70ee28ee13 | ||
|
|
1c9ecc3edd | ||
|
|
9bd5d6a422 | ||
|
|
1e41ccbc7a | ||
|
|
6200948eec | ||
|
|
0a402e3c63 | ||
|
|
55759bd22a | ||
|
|
d8e1f52ddd | ||
|
|
baea92b206 | ||
|
|
cfc05c1b22 | ||
|
|
ebf78d49a8 | ||
|
|
4cb4c9e568 | ||
|
|
36f524a354 | ||
|
|
b60d2190ac | ||
|
|
2f8b8c580d | ||
|
|
5187d3fa78 | ||
|
|
9c07741972 | ||
|
|
8fcbe44d3e | ||
|
|
7f902e41c7 | ||
|
|
3966fb1df6 | ||
|
|
830614fb19 | ||
|
|
76ae5c7398 | ||
|
|
769935f99e | ||
|
|
6920d6eef1 | ||
|
|
b5cd3bff3c | ||
|
|
ac07cb41b6 | ||
|
|
e8fa58f201 | ||
|
|
1f6994b62c | ||
|
|
47d82a1ac2 | ||
|
|
703d579561 | ||
|
|
ed375bfaf7 | ||
|
|
3da8c01c1f | ||
|
|
295f520f21 | ||
|
|
fba7ae923d | ||
|
|
28a9bd514f | ||
|
|
666c86b108 | ||
|
|
194293664f | ||
|
|
039ee5d06c | ||
|
|
afc66b3c3d | ||
|
|
a04b31866d | ||
|
|
db3dde98ef | ||
|
|
0d6a6b5b63 | ||
|
|
4df3267521 | ||
|
|
d3e4a1a6f9 | ||
|
|
b023699f1b | ||
|
|
f338dbe3f8 | ||
|
|
ab07f7df6c | ||
|
|
a59d73de7b | ||
|
|
1ac7618bb1 | ||
|
|
2a069880cd | ||
|
|
5e5928a8a6 | ||
|
|
d6e87420c3 | ||
|
|
79cfd39fde | ||
|
|
e9831a7507 | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
9126da6299 | ||
|
|
fea139d8e7 | ||
|
|
ac7a8a8e1e | ||
|
|
bbaa2f4cda | ||
|
|
9d61eecd81 | ||
|
|
21247e10d0 | ||
|
|
c1fc06ae34 | ||
|
|
a0eb3d1079 | ||
|
|
164aea3a3a | ||
|
|
ec83f83017 | ||
|
|
5cd08ab2f5 | ||
|
|
072f6b103e | ||
|
|
b4dcde252b | ||
|
|
0b2c3c1aa7 | ||
|
|
0dc9d0bed7 | ||
|
|
a2a2e37797 | ||
|
|
3d9819f97c | ||
|
|
b2b4a24d7c | ||
|
|
7557d6d619 | ||
|
|
57eeb9b0a3 | ||
|
|
813c7d5902 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
f200b4183d | ||
|
|
33642c20ec | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
2704962277 | ||
|
|
6bcd89acf7 | ||
|
|
433cb9b3b2 | ||
|
|
7f43372dd4 | ||
|
|
b12e2ceada | ||
|
|
bc067e9ad4 | ||
|
|
245294fbc5 | ||
|
|
f38bc75ab4 | ||
|
|
3407900abb | ||
|
|
684c20c4ea | ||
|
|
6ef522df7e | ||
|
|
3b771f2976 | ||
|
|
afc56c12fe | ||
|
|
5eeed03dcd | ||
|
|
0d98b4ce5e | ||
|
|
ae2ec43a82 | ||
|
|
265ed34ffd | ||
|
|
711dcb4a48 | ||
|
|
3079a9f4de | ||
|
|
a7d2cfdee2 | ||
|
|
a149e87ca7 | ||
|
|
854fd52a27 | ||
|
|
3d808ac75f | ||
|
|
39b924f158 | ||
|
|
a488ef6b00 | ||
|
|
6d66c38c12 | ||
|
|
922964ecf2 | ||
|
|
0c70416b5c | ||
|
|
770f30c3a8 | ||
|
|
b4044e6c3a | ||
|
|
9872767f20 | ||
|
|
dd4d2f4696 | ||
|
|
e5dc0e6bb8 | ||
|
|
85fbe820c4 | ||
|
|
832f8eaa94 | ||
|
|
3435dcc91e | ||
|
|
1ed74b8598 | ||
|
|
fd36978c13 | ||
|
|
1278a0b818 | ||
|
|
6a6516ddd5 | ||
|
|
67bc7007aa | ||
|
|
1fe8f13503 | ||
|
|
8f3adcda5d | ||
|
|
9e0c931573 | ||
|
|
21a8df78ee | ||
|
|
7f8351e044 | ||
|
|
afc1ecafe9 | ||
|
|
ab6ff5fda2 | ||
|
|
b0ba1a43a9 | ||
|
|
e919cab3d1 | ||
|
|
f37509062e | ||
|
|
24ee78ccd8 | ||
|
|
d37b398e79 | ||
|
|
7a724f9134 | ||
|
|
f3b2e0fb91 | ||
|
|
844976c85b | ||
|
|
f0d914abbf | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
2ea7e10923 | ||
|
|
e7f8188ee5 | ||
|
|
0ed3023b42 | ||
|
|
3fd61a3600 | ||
|
|
a663fc8aa8 | ||
|
|
d84315fff8 | ||
|
|
144a6e469d | ||
|
|
c5f11e4516 | ||
|
|
16a09e8ff6 | ||
|
|
f51db4b9f6 | ||
|
|
6ad24a6bee | ||
|
|
5b736c3b36 | ||
|
|
cc553cc93d | ||
|
|
e88a06291e | ||
|
|
6eccb3d5b9 | ||
|
|
026de8c5ca | ||
|
|
e10d4b91cf | ||
|
|
d089eaf754 | ||
|
|
bb2d85965f | ||
|
|
d99fd1fd65 | ||
|
|
947c58f227 | ||
|
|
bce5fdd5cd | ||
|
|
fdf139edb2 | ||
|
|
67d9e24d8f | ||
|
|
314d98abc3 | ||
|
|
b2dfc069c5 | ||
|
|
69c50b9b48 | ||
|
|
f101e4f010 | ||
|
|
005f0eb4fc | ||
|
|
7293ad7b24 | ||
|
|
c5b58d8fd2 | ||
|
|
4db2c274e2 | ||
|
|
cbff801aec | ||
|
|
d5d83da766 | ||
|
|
de6d8a811c | ||
|
|
a94844b6b7 | ||
|
|
968e7b8b72 | ||
|
|
5f461c9796 | ||
|
|
b2b8706d05 | ||
|
|
2d345fe454 | ||
|
|
7bec70d429 | ||
|
|
60710e7fb4 | ||
|
|
f10d3a8564 | ||
|
|
b020520366 | ||
|
|
52010e5820 | ||
|
|
af72f0d490 | ||
|
|
8924618d12 | ||
|
|
6557fbb666 | ||
|
|
75eca03b05 | ||
|
|
b8ebefd803 | ||
|
|
168c66815e | ||
|
|
26e703a1cd | ||
|
|
c8be214ee0 | ||
|
|
2060cdb931 | ||
|
|
574ee820a9 | ||
|
|
7d02f77e67 | ||
|
|
fd50efb503 | ||
|
|
9dbd7fa618 | ||
|
|
8dab31b87a | ||
|
|
e155c52256 | ||
|
|
c76e7c706c | ||
|
|
552943c033 | ||
|
|
4efe3b41da | ||
|
|
218376a41c | ||
|
|
e647ec22b1 | ||
|
|
38fe756725 | ||
|
|
652a67ad65 | ||
|
|
5bd9da6054 | ||
|
|
7c6fe8c4e2 | ||
|
|
689d1eb082 | ||
|
|
06d75e1804 | ||
|
|
0528e98d9c | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
f99c8ff99a | ||
|
|
9558f84b97 | ||
|
|
2fd421b115 | ||
|
|
6ff440e677 | ||
|
|
0bda5554dd | ||
|
|
860d4d4be5 | ||
|
|
88f93f76dd | ||
|
|
e5fc6bf5fa | ||
|
|
8990a9817f | ||
|
|
9a97995f18 | ||
|
|
1a1e71cd60 | ||
|
|
34802ff8a6 | ||
|
|
0ff5aad9c0 | ||
|
|
03e5d61798 | ||
|
|
4f231d1bf0 | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
75981c2412 | ||
|
|
8b82753218 | ||
|
|
3368fe42d8 | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f8ae4c335e | ||
|
|
5570e858e5 | ||
|
|
1859a4d356 | ||
|
|
ad4642c2c4 | ||
|
|
92108d710d | ||
|
|
c3ea0d333e | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
44347ee353 | ||
|
|
9e704fcae4 | ||
|
|
fdd816b17d | ||
|
|
82e2c523e6 | ||
|
|
a323b0d49c | ||
|
|
4c985aac7e | ||
|
|
87e18b8068 | ||
|
|
607a2c91fc | ||
|
|
d447355a61 | ||
|
|
8e2437498f | ||
|
|
9de85283cd | ||
|
|
b3d4c199ae | ||
|
|
fde970ba59 | ||
|
|
ec7be1b08b | ||
|
|
746a760a23 | ||
|
|
1a09d88891 | ||
|
|
46c01ecba2 | ||
|
|
544ece03a5 | ||
|
|
5fee7c4db1 | ||
|
|
8ed9f75d57 | ||
|
|
a15b179676 | ||
|
|
73844b9eeb | ||
|
|
dcde599709 | ||
|
|
0e0945ef84 | ||
|
|
ad125327c0 | ||
|
|
dfaf20dd83 | ||
|
|
786262db3b | ||
|
|
29a4110d8f | ||
|
|
9b639f715f | ||
|
|
46f3d78c8a | ||
|
|
1338ae2fc3 | ||
|
|
37813a223a | ||
|
|
b488e969bb | ||
|
|
1377296ef4 | ||
|
|
01cb22af37 | ||
|
|
331305333d | ||
|
|
0651eae7ec | ||
|
|
1552417598 | ||
|
|
4e71a5a47b | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
cc0ce7c630 | ||
|
|
070d4aeb6c | ||
|
|
668ce26269 | ||
|
|
410e967eb1 | ||
|
|
388f2f40dc | ||
|
|
148350009c | ||
|
|
70991fc1e5 | ||
|
|
e5c4e0ac86 | ||
|
|
397db04428 | ||
|
|
cd6572b61a | ||
|
|
581881d0ca | ||
|
|
d2efc2f47f | ||
|
|
4cc73657a1 |
16
.env.example
16
.env.example
@@ -20,6 +20,8 @@ SESSION_DRIVER=file
|
||||
#CACHE_DRIVER=memcached
|
||||
#SESSION_DRIVER=memcached
|
||||
QUEUE_DRIVER=sync
|
||||
# A different prefix is useful when multiple BookStack instances use the same caching server
|
||||
CACHE_PREFIX=bookstack
|
||||
|
||||
# Memcached settings
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
@@ -46,8 +48,16 @@ GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
OKTA_BASE_URL=false
|
||||
OKTA_APP_ID=false
|
||||
OKTA_APP_SECRET=false
|
||||
TWITCH_APP_ID=false
|
||||
TWITCH_APP_SECRET=false
|
||||
GITLAB_APP_ID=false
|
||||
GITLAB_APP_SECRET=false
|
||||
GITLAB_BASE_URI=false
|
||||
|
||||
# External services such as Gravatar
|
||||
# External services such as Gravatar and Draw.IO
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# LDAP Settings
|
||||
@@ -64,4 +74,6 @@ MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM=null
|
||||
MAIL_FROM_NAME=null
|
||||
|
||||
12
.github/ISSUE_TEMPLATE.md
vendored
12
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,10 +4,18 @@ Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
|
||||
* BookStack Version:
|
||||
* BookStack Version *(Found in settings, Please don't put 'latest')*:
|
||||
* PHP Version:
|
||||
* MySQL Version:
|
||||
|
||||
##### Expected Behavior
|
||||
|
||||
##### Actual Behavior
|
||||
|
||||
|
||||
##### Current Behavior
|
||||
|
||||
|
||||
|
||||
##### Steps to Reproduce
|
||||
|
||||
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -2,22 +2,22 @@
|
||||
/node_modules
|
||||
Homestead.yaml
|
||||
.env
|
||||
/public/dist
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist/*.map
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
nbproject
|
||||
.buildpath
|
||||
|
||||
.project
|
||||
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
.settings/
|
||||
|
||||
@@ -2,7 +2,8 @@ dist: trusty
|
||||
sudo: false
|
||||
language: php
|
||||
php:
|
||||
- 7.0
|
||||
- 7.0.20
|
||||
- 7.1.9
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -14,7 +15,6 @@ before_script:
|
||||
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||
- mysql -u root -e "FLUSH PRIVILEGES;"
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer dump-autoload --no-interaction
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- php artisan clear-compiled -n
|
||||
- php artisan optimize -n
|
||||
@@ -25,4 +25,4 @@ after_failure:
|
||||
- cat storage/logs/laravel.log
|
||||
|
||||
script:
|
||||
- phpunit
|
||||
- phpunit
|
||||
|
||||
@@ -16,7 +16,9 @@ class Activity extends Model
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
if ($this->entity_type === '') $this->entity_type = null;
|
||||
if ($this->entity_type === '') {
|
||||
$this->entity_type = null;
|
||||
}
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
@@ -43,8 +45,8 @@ class Activity extends Model
|
||||
* @param $activityB
|
||||
* @return bool
|
||||
*/
|
||||
public function isSimilarTo($activityB) {
|
||||
public function isSimilarTo($activityB)
|
||||
{
|
||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Attachment extends Ownable
|
||||
{
|
||||
protected $fillable = ['name', 'order'];
|
||||
@@ -11,7 +10,9 @@ class Attachment extends Ownable
|
||||
*/
|
||||
public function getFileName()
|
||||
{
|
||||
if (str_contains($this->name, '.')) return $this->name;
|
||||
if (str_contains($this->name, '.')) {
|
||||
return $this->name;
|
||||
}
|
||||
return $this->name . '.' . $this->extension;
|
||||
}
|
||||
|
||||
@@ -32,5 +33,4 @@ class Attachment extends Ownable
|
||||
{
|
||||
return baseUrl('/attachments/' . $this->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
app/Book.php
40
app/Book.php
@@ -2,8 +2,9 @@
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
public $searchFactor = 2;
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
@@ -18,6 +19,35 @@ class Book extends Entity
|
||||
return baseUrl('/books/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns book cover image, if book cover not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
{
|
||||
$default = baseUrl('/book_default_cover.png');
|
||||
if (!$this->image_id) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the book
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function cover()
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
@@ -56,4 +86,12 @@ class Book extends Entity
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Chapter extends Entity
|
||||
{
|
||||
public $searchFactor = 1.3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
protected $with = ['book'];
|
||||
@@ -51,4 +52,12 @@ class Chapter extends Entity
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Comment.php
Normal file
43
app/Comment.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
protected $fillable = ['text', 'html', 'parent_id'];
|
||||
protected $appends = ['created', 'updated'];
|
||||
|
||||
/**
|
||||
* Get the entity that this comment belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment has been updated since creation.
|
||||
* @return bool
|
||||
*/
|
||||
public function isUpdated()
|
||||
{
|
||||
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCreatedAttribute()
|
||||
{
|
||||
return $this->created_at->diffForHumans();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated date as a relative diff.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUpdatedAttribute()
|
||||
{
|
||||
return $this->updated_at->diffForHumans();
|
||||
}
|
||||
}
|
||||
85
app/Console/Commands/CreateAdmin.php
Normal file
85
app/Console/Commands/CreateAdmin.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CreateAdmin extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:create-admin
|
||||
{--email= : The email address for the new admin user}
|
||||
{--name= : The name of the new admin user}
|
||||
{--password= : The password to assign to the new admin user}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Add a new admin user to the system';
|
||||
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(UserRepo $userRepo)
|
||||
{
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$email = trim($this->option('email'));
|
||||
if (empty($email)) {
|
||||
$email = $this->ask('Please specify an email address for the new admin user');
|
||||
}
|
||||
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $this->error('Invalid email address provided');
|
||||
}
|
||||
|
||||
if ($this->userRepo->getByEmail($email) !== null) {
|
||||
return $this->error('A user with the provided email already exists!');
|
||||
}
|
||||
|
||||
$name = trim($this->option('name'));
|
||||
if (empty($name)) {
|
||||
$name = $this->ask('Please specify an name for the new admin user');
|
||||
}
|
||||
if (strlen($name) < 2) {
|
||||
return $this->error('Invalid name provided');
|
||||
}
|
||||
|
||||
$password = trim($this->option('password'));
|
||||
if (empty($password)) {
|
||||
$password = $this->secret('Please specify a password for the new admin user');
|
||||
}
|
||||
if (strlen($password) < 5) {
|
||||
return $this->error('Invalid password provided, Must be at least 5 characters');
|
||||
}
|
||||
|
||||
|
||||
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
||||
$this->userRepo->attachSystemRole($user, 'admin');
|
||||
$this->userRepo->downloadGravatarToUserAvatar($user);
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
$this->info("Admin account with email \"{$user->email}\" successfully created!");
|
||||
}
|
||||
}
|
||||
57
app/Console/Commands/DeleteUsers.php
Normal file
57
app/Console/Commands/DeleteUsers.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\User;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DeleteUsers extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:delete-users';
|
||||
|
||||
protected $user;
|
||||
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete users that are not "admin" or system users.';
|
||||
|
||||
public function __construct(User $user, UserRepo $userRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
|
||||
$numDeleted = 0;
|
||||
if (strtolower(trim($confirm)) === 'yes') {
|
||||
$totalUsers = $this->user->count();
|
||||
$users = $this->user->where('system_name', '=', null)->with('roles')->get();
|
||||
foreach ($users as $user) {
|
||||
if ($user->hasSystemRole('admin')) {
|
||||
// don't delete users with "admin" role
|
||||
continue;
|
||||
}
|
||||
$this->userRepo->destroy($user);
|
||||
++$numDeleted;
|
||||
}
|
||||
$this->info("Deleted $numDeleted of $totalUsers total users.");
|
||||
} else {
|
||||
$this->info('Exiting...');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-permissions';
|
||||
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -46,7 +46,15 @@ class RegeneratePermissions extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->permissionService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissions();
|
||||
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Permissions regenerated');
|
||||
}
|
||||
}
|
||||
|
||||
54
app/Console/Commands/RegenerateSearch.php
Normal file
54
app/Console/Commands/RegenerateSearch.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Re-index all content for searching';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = \DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
\DB::setDefaultConnection($this->option('database'));
|
||||
$this->searchService->setConnection(\DB::connection($this->option('database')));
|
||||
}
|
||||
|
||||
$this->searchService->indexAllEntities();
|
||||
\DB::setDefaultConnection($connection);
|
||||
$this->comment('Search index regenerated');
|
||||
}
|
||||
}
|
||||
57
app/Console/Commands/UpgradeDatabaseEncoding.php
Normal file
57
app/Console/Commands/UpgradeDatabaseEncoding.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpgradeDatabaseEncoding extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:db-utf8mb4 {--database= : The database connection to use.}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$connection = DB::getDefaultConnection();
|
||||
if ($this->option('database') !== null) {
|
||||
DB::setDefaultConnection($this->option('database'));
|
||||
}
|
||||
|
||||
$database = DB::getDatabaseName();
|
||||
$tables = DB::select('SHOW TABLES');
|
||||
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
$this->line('USE `'.$database.'`;');
|
||||
$key = 'Tables_in_' . $database;
|
||||
foreach ($tables as $table) {
|
||||
$tableName = $table->$key;
|
||||
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
|
||||
}
|
||||
|
||||
DB::setDefaultConnection($connection);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console;
|
||||
<?php namespace BookStack\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
@@ -13,10 +11,7 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
\BookStack\Console\Commands\ClearViews::class,
|
||||
\BookStack\Console\Commands\ClearActivity::class,
|
||||
\BookStack\Console\Commands\ClearRevisions::class,
|
||||
\BookStack\Console\Commands\RegeneratePermissions::class,
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -29,4 +24,14 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
}
|
||||
}
|
||||
|
||||
126
app/Entity.php
126
app/Entity.php
@@ -1,10 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
protected $fieldsToSearch = ['name', 'description'];
|
||||
/**
|
||||
* @var string - Name of property where the main text content is found
|
||||
*/
|
||||
public $textField = 'description';
|
||||
|
||||
/**
|
||||
* @var float - Multiplier for search indexing.
|
||||
*/
|
||||
public $searchFactor = 1.0;
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
@@ -26,7 +35,9 @@ class Entity extends Ownable
|
||||
{
|
||||
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||
|
||||
if ($matches) return true;
|
||||
if ($matches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
||||
return $entity->book_id === $this->id;
|
||||
@@ -65,6 +76,26 @@ class Entity extends Ownable
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for an entity
|
||||
* @param bool $orderByCreated
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function comments($orderByCreated = true)
|
||||
{
|
||||
$query = $this->morphMany(Comment::class, 'entity');
|
||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function searchTerms()
|
||||
{
|
||||
return $this->morphMany(SearchTerm::class, 'entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this entities restrictions.
|
||||
*/
|
||||
@@ -85,17 +116,6 @@ class Entity extends Ownable
|
||||
->where('action', '=', $action)->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this entity has live (active) restrictions in place.
|
||||
* @param $role_id
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
public function hasActiveRestriction($role_id, $action)
|
||||
{
|
||||
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity jointPermissions this is connected to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
@@ -148,72 +168,38 @@ class Entity extends Ownable
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if (strlen($this->name) <= $length) return $this->name;
|
||||
if (strlen($this->name) <= $length) {
|
||||
return $this->name;
|
||||
}
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a full-text search on this entity.
|
||||
* @param string[] $fieldsToSearch
|
||||
* @param string[] $terms
|
||||
* @param string[] array $wheres
|
||||
* Get the body text of this entity.
|
||||
* @return mixed
|
||||
*/
|
||||
public function fullTextSearchQuery($terms, $wheres = [])
|
||||
public function getText()
|
||||
{
|
||||
$exactTerms = [];
|
||||
$fuzzyTerms = [];
|
||||
$search = static::newQuery();
|
||||
return $this->{$this->textField};
|
||||
}
|
||||
|
||||
foreach ($terms as $key => $term) {
|
||||
$term = htmlentities($term, ENT_QUOTES);
|
||||
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
|
||||
if (preg_match('/".*?"/', $term) || is_numeric($term)) {
|
||||
$term = str_replace('"', '', $term);
|
||||
$exactTerms[] = '%' . $term . '%';
|
||||
} else {
|
||||
$term = '' . $term . '*';
|
||||
if ($term !== '*') $fuzzyTerms[] = $term;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
|
||||
|
||||
|
||||
// Perform fulltext search if relevant terms exist.
|
||||
if ($isFuzzy) {
|
||||
$termString = implode(' ', $fuzzyTerms);
|
||||
$fields = implode(',', $this->fieldsToSearch);
|
||||
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
|
||||
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
|
||||
}
|
||||
|
||||
// Ensure at least one exact term matches if in search
|
||||
if (count($exactTerms) > 0) {
|
||||
$search = $search->where(function ($query) use ($exactTerms) {
|
||||
foreach ($exactTerms as $exactTerm) {
|
||||
foreach ($this->fieldsToSearch as $field) {
|
||||
$query->orWhere($field, 'like', $exactTerm);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
|
||||
|
||||
// Add additional where terms
|
||||
foreach ($wheres as $whereTerm) {
|
||||
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
|
||||
}
|
||||
|
||||
// Load in relations
|
||||
if ($this->isA('page')) {
|
||||
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
|
||||
} else if ($this->isA('chapter')) {
|
||||
$search = $search->with('book');
|
||||
}
|
||||
|
||||
return $search->orderBy($orderBy, 'desc');
|
||||
/**
|
||||
* Get the url of this entity
|
||||
* @param $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path)
|
||||
{
|
||||
return '/';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class AuthException extends PrettyException
|
||||
{
|
||||
|
||||
class AuthException extends PrettyException {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class ConfirmationEmailException extends NotifyException
|
||||
{
|
||||
|
||||
class ConfirmationEmailException extends NotifyException {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class FileUploadException extends PrettyException
|
||||
{
|
||||
|
||||
class FileUploadException extends PrettyException {}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pipeline\Pipeline;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -26,10 +29,10 @@ class Handler extends ExceptionHandler
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return mixed
|
||||
*/
|
||||
public function report(Exception $e)
|
||||
{
|
||||
@@ -60,18 +63,44 @@ class Handler extends ExceptionHandler
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
// Handle 404 errors with a loaded session to enable showing user-specific information
|
||||
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
|
||||
return $this->loadErrorMiddleware($request, function ($request) use ($e) {
|
||||
$message = $e->getMessage() ?: trans('errors.404_page_not_found');
|
||||
return response()->view('errors/404', ['message' => $message], 404);
|
||||
});
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the middleware required to show state/session-enabled error pages.
|
||||
* @param Request $request
|
||||
* @param $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function loadErrorMiddleware(Request $request, $callback)
|
||||
{
|
||||
$middleware = (\Route::getMiddlewareGroups()['web_errors']);
|
||||
return (new Pipeline($this->container))
|
||||
->send($request)
|
||||
->through($middleware)
|
||||
->then($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
* @param Exception $e
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
protected function isExceptionType(Exception $e, $type)
|
||||
{
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
if (is_a($e, $type)) {
|
||||
return true;
|
||||
}
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
@@ -81,7 +110,8 @@ class Handler extends ExceptionHandler
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
protected function getOriginalMessage(Exception $e)
|
||||
{
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
@@ -103,4 +133,16 @@ class Handler extends ExceptionHandler
|
||||
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a validation exception into a JSON response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Validation\ValidationException $exception
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function invalidJson($request, ValidationException $exception)
|
||||
{
|
||||
return response()->json($exception->errors(), $exception->status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class ImageUploadException extends PrettyException {}
|
||||
class ImageUploadException extends PrettyException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class LdapException extends PrettyException {}
|
||||
class LdapException extends PrettyException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class NotFoundException extends PrettyException {
|
||||
class NotFoundException extends PrettyException
|
||||
{
|
||||
|
||||
/**
|
||||
* NotFoundException constructor.
|
||||
@@ -11,4 +11,4 @@ class NotFoundException extends PrettyException {
|
||||
{
|
||||
parent::__construct($message, 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
class NotifyException extends \Exception
|
||||
{
|
||||
|
||||
@@ -18,4 +17,4 @@ class NotifyException extends \Exception
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class PermissionsException extends Exception {}
|
||||
class PermissionsException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class PrettyException extends \Exception {}
|
||||
class PrettyException extends \Exception
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class SocialDriverNotConfigured extends PrettyException
|
||||
{
|
||||
|
||||
class SocialDriverNotConfigured extends PrettyException {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class SocialSignInException extends NotifyException
|
||||
{
|
||||
|
||||
class SocialSignInException extends NotifyException {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserRegistrationException extends NotifyException
|
||||
{
|
||||
|
||||
class UserRegistrationException extends NotifyException {}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -182,11 +183,16 @@ class AttachmentController extends Controller
|
||||
* Get an attachment from storage.
|
||||
* @param $attachmentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
*/
|
||||
public function get($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
||||
if ($page === null) {
|
||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
if ($attachment->external) {
|
||||
@@ -204,6 +210,7 @@ class AttachmentController extends Controller
|
||||
* Delete a specific attachment in the system.
|
||||
* @param $attachmentId
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete($attachmentId)
|
||||
{
|
||||
|
||||
@@ -64,5 +64,4 @@ class ForgotPasswordController extends Controller
|
||||
['email' => trans($response)]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,20 +70,21 @@ class LoginController extends Controller
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
if (!$user->exists) {
|
||||
auth()->logout($user);
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
if (!$user->exists && $user->email === null && !$request->filled('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
if (!$user->exists && $user->email === null && $request->filled('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
@@ -102,12 +103,21 @@ class LoginController extends Controller
|
||||
|
||||
/**
|
||||
* Show the application login form.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getLogin()
|
||||
public function getLogin(Request $request)
|
||||
{
|
||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||
$authMethod = config('auth.method');
|
||||
|
||||
if ($request->has('email')) {
|
||||
session()->flashInput([
|
||||
'email' => $request->get('email'),
|
||||
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
|
||||
]);
|
||||
}
|
||||
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
@@ -121,4 +131,4 @@ class LoginController extends Controller
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -52,7 +53,7 @@ class RegisterController extends Controller
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
|
||||
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
@@ -90,6 +91,7 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Show the application registration form.
|
||||
* @return Response
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function getRegister()
|
||||
{
|
||||
@@ -101,20 +103,13 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
* @param Request|\Illuminate\Http\Request $request
|
||||
* @return Response
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \Illuminate\Foundation\Validation\ValidationException
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
$this->checkRegistrationAllowed();
|
||||
$validator = $this->validator($request->all());
|
||||
|
||||
if ($validator->fails()) {
|
||||
$this->throwValidationException(
|
||||
$request, $validator
|
||||
);
|
||||
}
|
||||
$this->validator($request->all())->validate();
|
||||
|
||||
$userData = $request->all();
|
||||
return $this->registerUser($userData);
|
||||
@@ -140,7 +135,6 @@ class RegisterController extends Controller
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
{
|
||||
@@ -230,7 +224,6 @@ class RegisterController extends Controller
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
@@ -239,6 +232,8 @@ class RegisterController extends Controller
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
* @return mixed
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
*/
|
||||
public function socialRegister($socialDriver)
|
||||
{
|
||||
@@ -250,21 +245,34 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* The callback for social login services.
|
||||
* @param $socialDriver
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws SocialSignInException
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
public function socialCallback($socialDriver)
|
||||
public function socialCallback($socialDriver, Request $request)
|
||||
{
|
||||
if (session()->has('social-callback')) {
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
} elseif ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
}
|
||||
} else {
|
||||
if (!session()->has('social-callback')) {
|
||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||
}
|
||||
|
||||
// Check request for error information
|
||||
if ($request->has('error') && $request->has('error_description')) {
|
||||
throw new SocialSignInException(trans('errors.social_login_bad_response', [
|
||||
'socialAccount' => $socialDriver,
|
||||
'error' => $request->get('error_description'),
|
||||
]), '/login');
|
||||
}
|
||||
|
||||
$action = session()->pull('social-callback');
|
||||
if ($action == 'login') {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
}
|
||||
if ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
}
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
@@ -283,6 +291,7 @@ class RegisterController extends Controller
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
{
|
||||
@@ -297,5 +306,4 @@ class RegisterController extends Controller
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +46,4 @@ class ResetPasswordController extends Controller
|
||||
return redirect($this->redirectPath())
|
||||
->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
@@ -35,11 +36,19 @@ class BookController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = $this->entityRepo->getAllPaginated('book', 10);
|
||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||
$this->setPageTitle('Books');
|
||||
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
|
||||
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
||||
$this->setPageTitle(trans('entities.books'));
|
||||
return view('books/index', [
|
||||
'books' => $books,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'booksViewType' => $booksViewType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +92,12 @@ class BookController extends Controller
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
||||
return view('books/show', [
|
||||
'book' => $book,
|
||||
'current' => $book,
|
||||
'bookChildren' => $bookChildren,
|
||||
'activity' => Activity::entityActivity($book, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +109,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -113,9 +127,9 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +155,7 @@ class BookController extends Controller
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||
$books = $this->entityRepo->getAll('book', false);
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
@@ -171,48 +185,61 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->has('sort-tree')) {
|
||||
if (!$request->filled('sort-tree')) {
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
$sortMap = collect(json_decode($request->get('sort-tree')));
|
||||
$bookIdsInvolved = collect([$book->id]);
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
|
||||
// Load models into map
|
||||
$sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
|
||||
$mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
|
||||
$mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
|
||||
// Store source and target books
|
||||
$bookIdsInvolved->push(intval($mapItem->model->book_id));
|
||||
$bookIdsInvolved->push(intval($mapItem->book));
|
||||
});
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
// Get the books involved in the sort
|
||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
||||
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
|
||||
// Throw permission error if invalid ids or inaccessible books given.
|
||||
if (count($bookIdsInvolved) !== count($booksInvolved)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
// Check permissions of involved books
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
});
|
||||
|
||||
// Perform the sort
|
||||
$sortMap->each(function ($mapItem) {
|
||||
$model = $mapItem->model;
|
||||
|
||||
$priorityChanged = intval($model->priority) !== intval($mapItem->sort);
|
||||
$bookChanged = intval($model->book_id) !== intval($mapItem->book);
|
||||
$chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
|
||||
|
||||
if ($bookChanged) {
|
||||
$this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
|
||||
}
|
||||
if ($chapterChanged) {
|
||||
$model->chapter_id = intval($mapItem->parentChapter);
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
if ($priorityChanged) {
|
||||
$model->priority = intval($mapItem->sort);
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add activity for books
|
||||
foreach ($sortedBooks as $bookId) {
|
||||
$updatedBook = $this->entityRepo->getById('book', $bookId);
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
|
||||
// Update permissions on changed models
|
||||
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
|
||||
// Rebuild permissions and add activity for involved books.
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
$this->entityRepo->buildJointPermissionsForBook($book);
|
||||
Activity::add($book, 'book_sort', $book->id);
|
||||
});
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -159,7 +159,8 @@ class ChapterController extends Controller
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
public function showMove($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
@@ -177,7 +178,8 @@ class ChapterController extends Controller
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
public function move($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
|
||||
93
app/Http/Controllers/CommentController.php
Normal file
93
app/Http/Controllers/CommentController.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\CommentRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
protected $commentRepo;
|
||||
|
||||
/**
|
||||
* CommentController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param CommentRepo $commentRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->commentRepo = $commentRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new comment for a Page
|
||||
* @param Request $request
|
||||
* @param integer $pageId
|
||||
* @param null|integer $commentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function savePageComment(Request $request, $pageId, $commentId = null)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
try {
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return response('Not found', 404);
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
// Prevent adding comments to draft pages
|
||||
if ($page->draft) {
|
||||
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
|
||||
}
|
||||
|
||||
// Create a new comment.
|
||||
$this->checkPermission('comment-create-all');
|
||||
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
||||
Activity::add($page, 'commented_on', $page->book->id);
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Request $request
|
||||
* @param integer $commentId
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function update(Request $request, $commentId)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'text' => 'required|string',
|
||||
'html' => 'required|string',
|
||||
]);
|
||||
|
||||
$comment = $this->commentRepo->getById($commentId);
|
||||
$this->checkOwnablePermission('page-view', $comment->entity);
|
||||
$this->checkOwnablePermission('comment-update', $comment);
|
||||
|
||||
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
||||
return view('comments/comment', ['comment' => $comment]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param integer $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$comment = $this->commentRepo->getById($id);
|
||||
$this->checkOwnablePermission('comment-delete', $comment);
|
||||
$this->commentRepo->delete($comment);
|
||||
return response()->json(['message' => trans('entities.comment_deleted')]);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function preventAccessForDemoUsers()
|
||||
{
|
||||
if (config('app.env') === 'demo') $this->showPermissionError();
|
||||
if (config('app.env') === 'demo') {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +102,9 @@ abstract class Controller extends BaseController
|
||||
*/
|
||||
protected function checkOwnablePermission($permission, Ownable $ownable)
|
||||
{
|
||||
if (userCan($permission, $ownable)) return true;
|
||||
if (userCan($permission, $ownable)) {
|
||||
return true;
|
||||
}
|
||||
return $this->showPermissionError();
|
||||
}
|
||||
|
||||
@@ -113,7 +117,9 @@ abstract class Controller extends BaseController
|
||||
protected function checkPermissionOr($permissionName, $callback)
|
||||
{
|
||||
$callbackResult = $callback();
|
||||
if ($callbackResult === false) $this->checkPermission($permissionName);
|
||||
if ($callbackResult === false) {
|
||||
$this->checkPermission($permissionName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -145,5 +151,4 @@ abstract class Controller extends BaseController
|
||||
->withInput($request->input())
|
||||
->withErrors($errors, $this->errorBag());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
@@ -29,24 +30,36 @@ class HomeController extends Controller
|
||||
$activity = Activity::latest(10);
|
||||
$draftPages = $this->signedIn ? $this->entityRepo->getUserDraftPages(6) : [];
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 10*$recentFactor);
|
||||
$recentlyCreatedPages = $this->entityRepo->getRecentlyCreated('page', 5);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 5);
|
||||
return view('home', [
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||
|
||||
// Custom homepage
|
||||
$customHomepage = false;
|
||||
$homepageSetting = setting('app-homepage');
|
||||
if ($homepageSetting) {
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
}
|
||||
|
||||
$view = $customHomepage ? 'home-custom' : 'home';
|
||||
return view($view, [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyCreatedPages' => $recentlyCreatedPages,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages
|
||||
'draftPages' => $draftPages,
|
||||
'customHomepage' => $customHomepage
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a js representation of the current translations
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getTranslations() {
|
||||
$locale = trans()->getLocale();
|
||||
public function getTranslations()
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||
$resp = cache($cacheKey);
|
||||
@@ -76,4 +89,33 @@ class HomeController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an icon via image request.
|
||||
* Can provide a 'color' parameter with hex value to color the icon.
|
||||
* @param $iconName
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getIcon($iconName, Request $request)
|
||||
{
|
||||
$attrs = [];
|
||||
if ($request->filled('color')) {
|
||||
$attrs['fill'] = '#' . $request->get('color');
|
||||
}
|
||||
|
||||
$icon = icon($iconName, $attrs);
|
||||
return response($icon, 200, [
|
||||
'Content-Type' => 'image/svg+xml',
|
||||
'Cache-Control' => 'max-age=3600',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom head HTML, Used in ajax calls to show in editor.
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function customHeadContent()
|
||||
{
|
||||
return view('partials/custom-head-content');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
@@ -28,6 +29,21 @@ class ImageController extends Controller
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an image file from storage.
|
||||
* @param string $path
|
||||
* @return mixed
|
||||
*/
|
||||
public function showImage(string $path)
|
||||
{
|
||||
$path = storage_path('uploads/images/' . $path);
|
||||
if (!file_exists($path)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return response()->file($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all images for a specific type, Paginated
|
||||
* @param string $type
|
||||
@@ -47,14 +63,14 @@ class ImageController extends Controller
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchByType($type, $page = 0, Request $request)
|
||||
public function searchByType(Request $request, $type, $page = 0)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'term' => 'required|string'
|
||||
]);
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
|
||||
return response()->json($imgData);
|
||||
}
|
||||
|
||||
@@ -76,17 +92,19 @@ class ImageController extends Controller
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||
public function getGalleryFiltered(Request $request, $filter, $page = 0)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page_id' => 'required|integer'
|
||||
]);
|
||||
|
||||
$validFilters = collect(['page', 'book']);
|
||||
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||
if (!$validFilters->contains($filter)) {
|
||||
return response('Invalid filter', 500);
|
||||
}
|
||||
|
||||
$pageId = $request->get('page_id');
|
||||
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
|
||||
|
||||
return response()->json($imgData);
|
||||
}
|
||||
@@ -96,6 +114,7 @@ class ImageController extends Controller
|
||||
* @param string $type
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function uploadByType($type, Request $request)
|
||||
{
|
||||
@@ -104,18 +123,90 @@ class ImageController extends Controller
|
||||
'file' => 'is_image'
|
||||
]);
|
||||
|
||||
if (!$this->imageRepo->isValidType($type)) {
|
||||
return $this->jsonError(trans('errors.image_upload_type_error'));
|
||||
}
|
||||
|
||||
$imageUpload = $request->file('file');
|
||||
|
||||
try {
|
||||
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
|
||||
$uploadedTo = $request->get('uploaded_to', 0);
|
||||
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a drawing to the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function uploadDrawing(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'image' => 'required|string',
|
||||
'uploaded_to' => 'required|integer'
|
||||
]);
|
||||
$this->checkPermission('image-create-all');
|
||||
$imageBase64Data = $request->get('image');
|
||||
|
||||
try {
|
||||
$uploadedTo = $request->get('uploaded_to', 0);
|
||||
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the data content of a drawing.
|
||||
* @param string $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function replaceDrawing(string $id, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'image' => 'required|string'
|
||||
]);
|
||||
$this->checkPermission('image-create-all');
|
||||
|
||||
$imageBase64Data = $request->get('image');
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$this->checkOwnablePermission('image-update', $image);
|
||||
|
||||
try {
|
||||
$image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data);
|
||||
} catch (ImageUploadException $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return response()->json($image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of an image based64 encoded.
|
||||
* @param $id
|
||||
* @return \Illuminate\Http\JsonResponse|mixed
|
||||
*/
|
||||
public function getBase64Image($id)
|
||||
{
|
||||
$image = $this->imageRepo->getById($id);
|
||||
$imageData = $this->imageRepo->getImageData($image);
|
||||
if ($imageData === null) {
|
||||
return $this->jsonError("Image data could not be found");
|
||||
}
|
||||
return response()->json([
|
||||
'content' => base64_encode($imageData)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sized thumbnail for an image.
|
||||
* @param $id
|
||||
@@ -123,6 +214,8 @@ class ImageController extends Controller
|
||||
* @param $height
|
||||
* @param $crop
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getThumbnail($id, $width, $height, $crop)
|
||||
{
|
||||
@@ -137,6 +230,8 @@ class ImageController extends Controller
|
||||
* @param integer $imageId
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update($imageId, Request $request)
|
||||
{
|
||||
@@ -162,7 +257,7 @@ class ImageController extends Controller
|
||||
$this->checkOwnablePermission('image-delete', $image);
|
||||
|
||||
// Check if this image is used on any pages
|
||||
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||
$isForced = in_array($request->get('force', ''), [true, 'true']);
|
||||
if (!$isForced) {
|
||||
$pageSearch = $entityRepo->searchForImage($image->url);
|
||||
if ($pageSearch !== false) {
|
||||
@@ -173,6 +268,4 @@ class ImageController extends Controller
|
||||
$this->imageRepo->destroyImage($image);
|
||||
return response()->json(trans('components.images_deleted'));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ class PageController extends Controller
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
@@ -152,22 +153,33 @@ class PageController extends Controller
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) abort(404);
|
||||
if ($page === null) {
|
||||
throw $e;
|
||||
}
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$pageContent = $this->entityRepo->renderPage($page);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($pageContent);
|
||||
|
||||
$pageNav = $this->entityRepo->getPageNav($page->html);
|
||||
|
||||
// check if the comment's are enabled
|
||||
$commentsEnabled = !setting('app-disable-comments');
|
||||
if ($commentsEnabled) {
|
||||
$page->load(['comments.createdBy']);
|
||||
}
|
||||
|
||||
Views::add($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages/show', [
|
||||
'page' => $page,'book' => $page->book,
|
||||
'current' => $page, 'sidebarTree' => $sidebarTree,
|
||||
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
|
||||
'current' => $page,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'commentsEnabled' => $commentsEnabled,
|
||||
'pageNav' => $pageNav
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +222,9 @@ class PageController extends Controller
|
||||
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||
if (count($warnings) > 0) {
|
||||
session()->flash('warning', implode("\n", $warnings));
|
||||
}
|
||||
|
||||
$draftsEnabled = $this->signedIn;
|
||||
return view('pages/edit', [
|
||||
@@ -323,9 +337,10 @@ class PageController extends Controller
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->entityRepo->destroyPage($page);
|
||||
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
session()->flash('success', trans('entities.pages_delete_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -376,10 +391,11 @@ class PageController extends Controller
|
||||
|
||||
$page->fill($revision->toArray());
|
||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||
|
||||
|
||||
return view('pages/revision', [
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -409,6 +425,7 @@ class PageController extends Controller
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -438,6 +455,7 @@ class PageController extends Controller
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
@@ -454,6 +472,7 @@ class PageController extends Controller
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
@@ -589,5 +608,4 @@ class PageController extends Controller
|
||||
session()->flash('success', trans('entities.pages_permissions_success'));
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ class PermissionController extends Controller
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
if ($role->hidden) {
|
||||
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
}
|
||||
return view('settings/roles/edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -8,16 +9,19 @@ class SearchController extends Controller
|
||||
{
|
||||
protected $entityRepo;
|
||||
protected $viewService;
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
|
||||
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->viewService = $viewService;
|
||||
$this->searchService = $searchService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -27,84 +31,25 @@ class SearchController extends Controller
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchAll(Request $request)
|
||||
public function search(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
|
||||
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||
|
||||
$page = intval($request->get('page', '0')) ?: 1;
|
||||
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||
|
||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||
|
||||
return view('search/all', [
|
||||
'pages' => $pages,
|
||||
'books' => $books,
|
||||
'chapters' => $chapters,
|
||||
'searchTerm' => $searchTerm
|
||||
'entities' => $results['results'],
|
||||
'totalResults' => $results['total'],
|
||||
'searchTerm' => $searchTerm,
|
||||
'hasNextPage' => $results['has_more'],
|
||||
'nextPageLink' => $nextPageLink
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the pages in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchPages(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $pages,
|
||||
'title' => trans('entities.search_results_page'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the chapters in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchChapters(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $chapters,
|
||||
'title' => trans('entities.search_results_chapter'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search only the books in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||
*/
|
||||
public function searchBooks(Request $request)
|
||||
{
|
||||
if (!$request->has('term')) return redirect()->back();
|
||||
|
||||
$searchTerm = $request->get('term');
|
||||
$paginationAppends = $request->only('term');
|
||||
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
|
||||
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
|
||||
return view('search/entity-search-list', [
|
||||
'entities' => $books,
|
||||
'title' => trans('entities.search_results_book'),
|
||||
'searchTerm' => $searchTerm
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a book.
|
||||
@@ -115,16 +60,24 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchBook(Request $request, $bookId)
|
||||
{
|
||||
if (!$request->has('term')) {
|
||||
return redirect()->back();
|
||||
}
|
||||
$searchTerm = $request->get('term');
|
||||
$searchWhereTerms = [['book_id', '=', $bookId]];
|
||||
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
|
||||
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
|
||||
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchBook($bookId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches all entities within a chapter.
|
||||
* @param Request $request
|
||||
* @param integer $chapterId
|
||||
* @return \Illuminate\View\View
|
||||
* @internal param string $searchTerm
|
||||
*/
|
||||
public function searchChapter(Request $request, $chapterId)
|
||||
{
|
||||
$term = $request->get('term', '');
|
||||
$results = $this->searchService->searchChapter($chapterId, $term);
|
||||
return view('partials/entity-list', ['entities' => $results]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a list of entities and return a partial HTML response of matching entities.
|
||||
@@ -134,18 +87,13 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entities = collect();
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = $request->get('term', false);
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
foreach (['page', 'chapter', 'book'] as $entityType) {
|
||||
if ($entityTypes->contains($entityType)) {
|
||||
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
|
||||
}
|
||||
}
|
||||
$entities = $entities->sortByDesc('title_relevance');
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
@@ -155,7 +103,4 @@ class SearchController extends Controller
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ class SettingController extends Controller
|
||||
|
||||
// Cycles through posted settings and update them
|
||||
foreach ($request->all() as $name => $value) {
|
||||
if (strpos($name, 'setting-') !== 0) continue;
|
||||
if (strpos($name, 'setting-') !== 0) {
|
||||
continue;
|
||||
}
|
||||
$key = str_replace('setting-', '', trim($name));
|
||||
Setting::put($key, $value);
|
||||
}
|
||||
@@ -41,5 +43,4 @@ class SettingController extends Controller
|
||||
session()->flash('success', trans('settings.settings_save_success'));
|
||||
return redirect('/settings');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class TagController extends Controller
|
||||
*/
|
||||
public function getNameSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$searchTerm = $request->get('search', false);
|
||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
@@ -49,10 +49,9 @@ class TagController extends Controller
|
||||
*/
|
||||
public function getValueSuggestions(Request $request)
|
||||
{
|
||||
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||
$searchTerm = $request->get('search', false);
|
||||
$tagName = $request->get('name', false);
|
||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||
return response()->json($suggestions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -34,9 +34,9 @@ class UserController extends Controller
|
||||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$listDetails = [
|
||||
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||
'search' => $request->has('search') ? $request->get('search') : '',
|
||||
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||
'order' => $request->get('order', 'asc'),
|
||||
'search' => $request->get('search', ''),
|
||||
'sort' => $request->get('sort', 'name'),
|
||||
];
|
||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||
$this->setPageTitle(trans('settings.users'));
|
||||
@@ -88,22 +88,12 @@ class UserController extends Controller
|
||||
|
||||
$user->save();
|
||||
|
||||
if ($request->has('roles')) {
|
||||
if ($request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
|
||||
}
|
||||
$this->userRepo->downloadGravatarToUserAvatar($user);
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
@@ -155,24 +145,24 @@ class UserController extends Controller
|
||||
$user->fill($request->all());
|
||||
|
||||
// Role updates
|
||||
if (userCan('users-manage') && $request->has('roles')) {
|
||||
if (userCan('users-manage') && $request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
if ($request->has('password') && $request->get('password') != '') {
|
||||
if ($request->filled('password')) {
|
||||
$password = $request->get('password');
|
||||
$user->password = bcrypt($password);
|
||||
}
|
||||
|
||||
// External auth id updates
|
||||
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
|
||||
if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
|
||||
$user->external_auth_id = $request->get('external_auth_id');
|
||||
}
|
||||
|
||||
// Save an user-specific settings
|
||||
if ($request->has('setting')) {
|
||||
if ($request->filled('setting')) {
|
||||
foreach ($request->get('setting') as $key => $value) {
|
||||
setting()->putUser($user, $key, $value);
|
||||
}
|
||||
@@ -249,4 +239,27 @@ class UserController extends Controller
|
||||
'assetCounts' => $assetCounts
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred book-list display setting.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function switchBookView($id, Request $request)
|
||||
{
|
||||
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$viewType = $request->get('book_view_type');
|
||||
if (!in_array($viewType, ['grid', 'list'])) {
|
||||
$viewType = 'list';
|
||||
}
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
setting()->putUser($user, 'books_view_type', $viewType);
|
||||
|
||||
return redirect()->back(302, [], "/settings/users/$id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ class Kernel extends HttpKernel
|
||||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\BookStack\Http\Middleware\TrimStrings::class,
|
||||
\BookStack\Http\Middleware\TrustProxies::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -26,10 +27,20 @@ class Kernel extends HttpKernel
|
||||
'web' => [
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\BookStack\Http\Middleware\Localization::class
|
||||
],
|
||||
'web_errors' => [
|
||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||
\BookStack\Http\Middleware\Localization::class
|
||||
],
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
@@ -42,10 +53,11 @@ class Kernel extends HttpKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
|
||||
];
|
||||
}
|
||||
|
||||
@@ -30,8 +30,11 @@ class Authenticate
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||
return redirect(baseUrl('/register/confirm/awaiting'));
|
||||
if ($this->auth->check()) {
|
||||
$requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict'));
|
||||
if ($requireConfirmation && !$this->auth->user()->email_confirmed) {
|
||||
return redirect('/register/confirm/awaiting');
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends BaseEncrypter
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
|
||||
@@ -15,7 +15,19 @@ class Localization
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$defaultLang = config('app.locale');
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
if (user()->isDefault()) {
|
||||
$locale = $defaultLang;
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (!in_array($lang, $availableLocales)) {
|
||||
continue;
|
||||
}
|
||||
$locale = $lang;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
}
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
return $next($request);
|
||||
|
||||
19
app/Http/Middleware/TrimStrings.php
Normal file
19
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
'password',
|
||||
'password_confirmation',
|
||||
'password-confirm',
|
||||
];
|
||||
}
|
||||
47
app/Http/Middleware/TrustProxies.php
Normal file
47
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Fideloper\Proxy\TrustProxies as Middleware;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $proxies;
|
||||
|
||||
/**
|
||||
* The current proxy header mappings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [
|
||||
Request::HEADER_FORWARDED => 'FORWARDED',
|
||||
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
||||
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
||||
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
||||
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
||||
];
|
||||
|
||||
/**
|
||||
* Handle the request, Set the correct user-configured proxy information.
|
||||
* @param Request $request
|
||||
* @param Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$setProxies = config('app.proxies');
|
||||
if ($setProxies !== '**' && $setProxies !== '*' && $setProxies !== '') {
|
||||
$setProxies = explode(',', $setProxies);
|
||||
}
|
||||
$this->proxies = $setProxies;
|
||||
|
||||
return parent::handle($request, $next);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
|
||||
@@ -15,5 +15,4 @@ class Model extends EloquentModel
|
||||
{
|
||||
return parent::getAttributeFromArray($key);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ConfirmEmail extends Notification
|
||||
class ConfirmEmail extends Notification implements ShouldQueue
|
||||
{
|
||||
|
||||
use Queueable;
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
@@ -45,5 +49,4 @@ class ConfirmEmail extends Notification
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
/**
|
||||
@@ -29,5 +28,4 @@ abstract class Ownable extends Model
|
||||
{
|
||||
return strtolower(array_slice(explode('\\', static::class), -1, 1)[0]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
14
app/Page.php
14
app/Page.php
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||
@@ -8,8 +7,7 @@ class Page extends Entity
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
|
||||
protected $fieldsToSearch = ['name', 'text'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
@@ -96,4 +94,14 @@ class Page extends Entity
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @param bool $withContent
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{
|
||||
$htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'text', 'markdown', 'summary'];
|
||||
@@ -31,7 +30,9 @@ class PageRevision extends Model
|
||||
public function getUrl($path = null)
|
||||
{
|
||||
$url = $this->page->getUrl() . '/revisions/' . $this->id;
|
||||
if ($path) return $url . '/' . trim($path, '/');
|
||||
if ($path) {
|
||||
return $url . '/' . trim($path, '/');
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
@@ -47,4 +48,15 @@ class PageRevision extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows checking of the exact class, Used to check entity type.
|
||||
* Included here to align with entities in similar use cases.
|
||||
* (Yup, Bit of an awkward hack)
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function isA($type)
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,17 @@ class AppServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
// Custom validation methods
|
||||
Validator::extend('is_image', function($attribute, $value, $parameters, $validator) {
|
||||
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
|
||||
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
\Blade::directive('icon', function($expression) {
|
||||
\Blade::directive('icon', function ($expression) {
|
||||
return "<?php echo icon($expression); ?>";
|
||||
});
|
||||
|
||||
// Allow longer string lengths after upgrade to utf8mb4
|
||||
\Schema::defaultStringLength(191);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +35,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(SettingService::class, function($app) {
|
||||
$this->app->singleton(SettingService::class, function ($app) {
|
||||
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
Auth::provider('ldap', function($app, array $config) {
|
||||
Auth::provider('ldap', function ($app, array $config) {
|
||||
return new LdapUserProvider($config['model'], $app[LdapService::class]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,28 +34,28 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->bind('activity', function() {
|
||||
$this->app->bind('activity', function () {
|
||||
return new ActivityService(
|
||||
$this->app->make(Activity::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('views', function() {
|
||||
$this->app->bind('views', function () {
|
||||
return new ViewService(
|
||||
$this->app->make(View::class),
|
||||
$this->app->make(PermissionService::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('setting', function() {
|
||||
$this->app->bind('setting', function () {
|
||||
return new SettingService(
|
||||
$this->app->make(Setting::class),
|
||||
$this->app->make(Repository::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind('images', function() {
|
||||
$this->app->bind('images', function () {
|
||||
return new ImageService(
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
|
||||
@@ -16,6 +16,10 @@ class EventServiceProvider extends ServiceProvider
|
||||
protected $listen = [
|
||||
SocialiteWasCalled::class => [
|
||||
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
|
||||
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
|
||||
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
|
||||
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
|
||||
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\User;
|
||||
@@ -102,7 +101,9 @@ class LdapUserProvider implements UserProvider
|
||||
{
|
||||
// Get user via LDAP
|
||||
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
|
||||
if ($userDetails === null) return null;
|
||||
if ($userDetails === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Search current user base by looking up a uid
|
||||
$model = $this->createModel();
|
||||
@@ -110,7 +111,9 @@ class LdapUserProvider implements UserProvider
|
||||
->where('external_auth_id', $userDetails['uid'])
|
||||
->first();
|
||||
|
||||
if ($currentUser !== null) return $currentUser;
|
||||
if ($currentUser !== null) {
|
||||
return $currentUser;
|
||||
}
|
||||
|
||||
$model->name = $userDetails['name'];
|
||||
$model->external_auth_id = $userDetails['uid'];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
|
||||
use Illuminate\Pagination\PaginationServiceProvider as IlluminatePaginationServiceProvider;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
|
||||
@@ -32,4 +31,4 @@ class PaginationServiceProvider extends IlluminatePaginationServiceProvider
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
app/Repos/CommentRepo.php
Normal file
90
app/Repos/CommentRepo.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Entity;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class CommentRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* CommentRepo constructor.
|
||||
* @param Comment $comment
|
||||
*/
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comment by ID.
|
||||
* @param $id
|
||||
* @return Comment|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->comment->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new comment on an entity.
|
||||
* @param Entity $entity
|
||||
* @param array $data
|
||||
* @return Comment
|
||||
*/
|
||||
public function create(Entity $entity, $data = [])
|
||||
{
|
||||
$userId = user()->id;
|
||||
$comment = $this->comment->newInstance($data);
|
||||
$comment->created_by = $userId;
|
||||
$comment->updated_by = $userId;
|
||||
$comment->local_id = $this->getNextLocalId($entity);
|
||||
$entity->comments()->save($comment);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Comment $comment
|
||||
* @param array $input
|
||||
* @return mixed
|
||||
*/
|
||||
public function update($comment, $input)
|
||||
{
|
||||
$comment->updated_by = user()->id;
|
||||
$comment->update($input);
|
||||
return $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param Comment $comment
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($comment)
|
||||
{
|
||||
return $comment->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next local ID relative to the linked entity.
|
||||
* @param Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
protected function getNextLocalId(Entity $entity)
|
||||
{
|
||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||
if ($comments === null) {
|
||||
return 1;
|
||||
}
|
||||
return $comments->local_id + 1;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Page;
|
||||
use BookStack\PageRevision;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
@@ -59,13 +61,12 @@ class EntityRepo
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
* @var SearchService
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* EntityService constructor.
|
||||
* EntityRepo constructor.
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
@@ -73,12 +74,18 @@ class EntityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param PermissionService $permissionService
|
||||
* @param TagRepo $tagRepo
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
|
||||
ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo
|
||||
)
|
||||
{
|
||||
Book $book,
|
||||
Chapter $chapter,
|
||||
Page $page,
|
||||
PageRevision $pageRevision,
|
||||
ViewService $viewService,
|
||||
PermissionService $permissionService,
|
||||
TagRepo $tagRepo,
|
||||
SearchService $searchService
|
||||
) {
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
@@ -91,6 +98,7 @@ class EntityRepo
|
||||
$this->viewService = $viewService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->tagRepo = $tagRepo;
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,9 +117,9 @@ class EntityRepo
|
||||
* @param bool $allowDrafts
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
protected function entityQuery($type, $allowDrafts = false)
|
||||
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
|
||||
{
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), 'view');
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type), $permission);
|
||||
if (strtolower($type) === 'page' && !$allowDrafts) {
|
||||
$q = $q->where('draft', '=', false);
|
||||
}
|
||||
@@ -134,10 +142,15 @@ class EntityRepo
|
||||
* @param string $type
|
||||
* @param integer $id
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return Entity
|
||||
*/
|
||||
public function getById($type, $id, $allowDrafts = false)
|
||||
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
if ($ignorePermissions) {
|
||||
$entity = $this->getEntity($type);
|
||||
return $entity->newQuery()->find($id);
|
||||
}
|
||||
return $this->entityQuery($type, $allowDrafts)->find($id);
|
||||
}
|
||||
|
||||
@@ -154,14 +167,16 @@ class EntityRepo
|
||||
$q = $this->entityQuery($type)->where('slug', '=', $slug);
|
||||
|
||||
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
|
||||
$q = $q->where('book_id', '=', function($query) use ($bookSlug) {
|
||||
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
|
||||
$query->select('id')
|
||||
->from($this->book->getTable())
|
||||
->where('slug', '=', $bookSlug)->limit(1);
|
||||
});
|
||||
}
|
||||
$entity = $q->first();
|
||||
if ($entity === null) throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
|
||||
if ($entity === null) {
|
||||
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
|
||||
}
|
||||
return $entity;
|
||||
}
|
||||
|
||||
@@ -187,15 +202,18 @@ class EntityRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities of a type limited by count unless count if false.
|
||||
* Get all entities of a type with the given permission, limited by count unless count is false.
|
||||
* @param string $type
|
||||
* @param integer|bool $count
|
||||
* @param string $permission
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAll($type, $count = 20)
|
||||
public function getAll($type, $count = 20, $permission = 'view')
|
||||
{
|
||||
$q = $this->entityQuery($type)->orderBy('name', 'asc');
|
||||
if ($count !== false) $q = $q->take($count);
|
||||
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
|
||||
if ($count !== false) {
|
||||
$q = $q->take($count);
|
||||
}
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
@@ -216,12 +234,15 @@ class EntityRepo
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
|
||||
->orderBy('created_at', 'desc');
|
||||
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
@@ -234,12 +255,15 @@ class EntityRepo
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
|
||||
->orderBy('updated_at', 'desc');
|
||||
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
@@ -327,7 +351,7 @@ class EntityRepo
|
||||
if ($rawEntity->entity_type === 'BookStack\\Page') {
|
||||
$entities[$index] = $this->page->newFromBuilder($rawEntity);
|
||||
if ($renderPages) {
|
||||
$entities[$index]->html = $rawEntity->description;
|
||||
$entities[$index]->html = $rawEntity->html;
|
||||
$entities[$index]->html = $this->renderPage($entities[$index]);
|
||||
};
|
||||
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
|
||||
@@ -336,13 +360,21 @@ class EntityRepo
|
||||
$parents[$key] = $entities[$index];
|
||||
$parents[$key]->setAttribute('pages', collect());
|
||||
}
|
||||
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
|
||||
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
|
||||
$tree[] = $entities[$index];
|
||||
}
|
||||
$entities[$index]->book = $book;
|
||||
}
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
|
||||
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
|
||||
continue;
|
||||
}
|
||||
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
|
||||
if (!isset($parents[$parentKey])) {
|
||||
$tree[] = $entity;
|
||||
continue;
|
||||
}
|
||||
$chapter = $parents[$parentKey];
|
||||
$chapter->pages->push($entity);
|
||||
}
|
||||
@@ -354,6 +386,7 @@ class EntityRepo
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param Chapter $chapter
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getChapterChildren(Chapter $chapter)
|
||||
{
|
||||
@@ -361,56 +394,6 @@ class EntityRepo
|
||||
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search entities of a type via a given query.
|
||||
* @param string $type
|
||||
* @param string $term
|
||||
* @param array $whereTerms
|
||||
* @param int $count
|
||||
* @param array $paginationAppends
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
|
||||
{
|
||||
$terms = $this->prepareSearchTerms($term);
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
|
||||
$q = $this->addAdvancedSearchQueries($q, $term);
|
||||
$entities = $q->paginate($count)->appends($paginationAppends);
|
||||
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
|
||||
|
||||
// Highlight page content
|
||||
if ($type === 'page') {
|
||||
//lookahead/behind assertions ensures cut between words
|
||||
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
|
||||
|
||||
foreach ($entities as $page) {
|
||||
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
|
||||
//delimiter between occurrences
|
||||
$results = [];
|
||||
foreach ($matches as $line) {
|
||||
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
|
||||
}
|
||||
$matchLimit = 6;
|
||||
if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
|
||||
$result = join('... ', $results);
|
||||
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
|
||||
if (strlen($result) < 5) $result = $page->getExcerpt(80);
|
||||
|
||||
$page->searchSnippet = $result;
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
// Highlight chapter/book content
|
||||
foreach ($entities as $entity) {
|
||||
//highlight
|
||||
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
|
||||
$entity->searchSnippet = $result;
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next sequential priority for a new child element in the given book.
|
||||
@@ -465,7 +448,9 @@ class EntityRepo
|
||||
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
|
||||
$query = $query->where('book_id', '=', $bookId);
|
||||
}
|
||||
if ($currentId) $query = $query->where('id', '!=', $currentId);
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
@@ -476,9 +461,10 @@ class EntityRepo
|
||||
*/
|
||||
public function updateEntityPermissionsFromRequest($request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
|
||||
$entity->restricted = $request->get('restricted', '') === 'true';
|
||||
$entity->permissions()->delete();
|
||||
if ($request->has('restrictions')) {
|
||||
|
||||
if ($request->filled('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->permissions()->create([
|
||||
@@ -488,108 +474,12 @@ class EntityRepo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a string of search terms by turning
|
||||
* it into an array of terms.
|
||||
* Keeps quoted terms together.
|
||||
* @param $termString
|
||||
* @return array
|
||||
*/
|
||||
public function prepareSearchTerms($termString)
|
||||
{
|
||||
$termString = $this->cleanSearchTermString($termString);
|
||||
preg_match_all('/(".*?")/', $termString, $matches);
|
||||
$terms = [];
|
||||
if (count($matches[1]) > 0) {
|
||||
foreach ($matches[1] as $match) {
|
||||
$terms[] = $match;
|
||||
}
|
||||
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
|
||||
}
|
||||
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any special search notation that should not
|
||||
* be used in a full-text search.
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function cleanSearchTermString($termString)
|
||||
{
|
||||
// Strip tag searches
|
||||
$termString = preg_replace('/\[.*?\]/', '', $termString);
|
||||
// Reduced multiple spacing into single spacing
|
||||
$termString = preg_replace("/\s{2,}/", " ", $termString);
|
||||
return $termString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses advanced search notations and adds them to the db query.
|
||||
* @param $query
|
||||
* @param $termString
|
||||
* @return mixed
|
||||
*/
|
||||
protected function addAdvancedSearchQueries($query, $termString)
|
||||
{
|
||||
$escapedOperators = $this->getRegexEscapedOperators();
|
||||
// Look for tag searches
|
||||
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
|
||||
if (count($tags[0]) > 0) {
|
||||
$this->applyTagSearches($query, $tags);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply extracted tag search terms onto a entity query.
|
||||
* @param $query
|
||||
* @param $tags
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearches($query, $tags) {
|
||||
$query->where(function($query) use ($tags) {
|
||||
foreach ($tags[1] as $index => $tagName) {
|
||||
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
|
||||
$tagOperator = $tags[3][$index];
|
||||
$tagValue = $tags[4][$index];
|
||||
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new entity from request input.
|
||||
@@ -608,12 +498,13 @@ class EntityRepo
|
||||
$entity->updated_by = user()->id;
|
||||
$isChapter ? $book->chapters()->save($entity) : $entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
$this->searchService->indexEntity($entity);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity details from request input.
|
||||
* Use for books and chapters
|
||||
* Used for books and chapters
|
||||
* @param string $type
|
||||
* @param Entity $entityModel
|
||||
* @param array $input
|
||||
@@ -628,6 +519,7 @@ class EntityRepo
|
||||
$entityModel->updated_by = user()->id;
|
||||
$entityModel->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
@@ -668,11 +560,11 @@ class EntityRepo
|
||||
|
||||
/**
|
||||
* Alias method to update the book jointPermissions in the PermissionService.
|
||||
* @param Collection $collection collection on entities
|
||||
* @param Book $book
|
||||
*/
|
||||
public function buildJointPermissions(Collection $collection)
|
||||
public function buildJointPermissionsForBook(Book $book)
|
||||
{
|
||||
$this->permissionService->buildJointPermissionsForEntities($collection);
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -682,9 +574,12 @@ class EntityRepo
|
||||
*/
|
||||
protected function nameToSlug($name)
|
||||
{
|
||||
$slug = str_replace(' ', '-', strtolower($name));
|
||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
|
||||
$slug = preg_replace('/\s{2,}/', ' ', $slug);
|
||||
$slug = str_replace(' ', '-', $slug);
|
||||
if ($slug === "") {
|
||||
$slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
@@ -706,12 +601,13 @@ class EntityRepo
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = strip_tags($draftPage->html);
|
||||
$draftPage->text = $this->pageToPlainText($draftPage);
|
||||
$draftPage->draft = false;
|
||||
$draftPage->revision_count = 1;
|
||||
|
||||
$draftPage->save();
|
||||
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
|
||||
|
||||
$this->searchService->indexEntity($draftPage);
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
@@ -724,7 +620,9 @@ class EntityRepo
|
||||
public function savePageRevision(Page $page, $summary = null)
|
||||
{
|
||||
$revision = $this->pageRevision->newInstance($page->toArray());
|
||||
if (setting('app-editor') !== 'markdown') $revision->markdown = '';
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$revision->markdown = '';
|
||||
}
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
@@ -732,6 +630,7 @@ class EntityRepo
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->revision_number = $page->revision_count;
|
||||
$revision->save();
|
||||
|
||||
// Clear old revisions
|
||||
@@ -751,7 +650,9 @@ class EntityRepo
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
if ($htmlText == '') return $htmlText;
|
||||
if ($htmlText == '') {
|
||||
return $htmlText;
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
@@ -765,7 +666,9 @@ class EntityRepo
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
if (get_class($childNode) !== 'DOMElement') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Overwrite id if not a BookStack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
@@ -804,41 +707,56 @@ class EntityRepo
|
||||
/**
|
||||
* Render the page for viewing, Parsing and performing features such as page transclusion.
|
||||
* @param Page $page
|
||||
* @param bool $ignorePermissions
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function renderPage(Page $page)
|
||||
public function renderPage(Page $page, $ignorePermissions = false)
|
||||
{
|
||||
$content = $page->html;
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($content);
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
|
||||
if (count($matches[0]) === 0) return $content;
|
||||
if (count($matches[0]) === 0) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$topLevelTags = ['table', 'ul', 'ol'];
|
||||
foreach ($matches[1] as $index => $includeId) {
|
||||
$splitInclude = explode('#', $includeId, 2);
|
||||
$pageId = intval($splitInclude[0]);
|
||||
if (is_nan($pageId)) continue;
|
||||
if (is_nan($pageId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$page = $this->getById('page', $pageId);
|
||||
if ($page === null) {
|
||||
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
|
||||
if ($matchedPage === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($splitInclude) === 1) {
|
||||
$content = str_replace($matches[0][$index], $page->html, $content);
|
||||
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||
if ($matchingElem === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
continue;
|
||||
}
|
||||
$innerContent = '';
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
|
||||
if ($isTopLevel) {
|
||||
$innerContent .= $doc->saveHTML($matchingElem);
|
||||
} else {
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
}
|
||||
}
|
||||
$content = str_replace($matches[0][$index], trim($innerContent), $content);
|
||||
}
|
||||
@@ -846,6 +764,35 @@ class EntityRepo
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape script tags within HTML content.
|
||||
* @param string $html
|
||||
* @return mixed
|
||||
*/
|
||||
protected function escapeScripts(string $html)
|
||||
{
|
||||
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
|
||||
$matches = [];
|
||||
preg_match_all($scriptSearchRegex, $html, $matches);
|
||||
if (count($matches) === 0) return $html;
|
||||
|
||||
foreach ($matches[0] as $match) {
|
||||
$html = str_replace($match, htmlentities($match), $html);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain text version of a page's content.
|
||||
* @param Page $page
|
||||
* @return string
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$html = $this->renderPage($page);
|
||||
return strip_tags($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
@@ -860,9 +807,12 @@ class EntityRepo
|
||||
$page->updated_by = user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
if ($chapter) {
|
||||
$page->chapter_id = $chapter->id;
|
||||
}
|
||||
|
||||
$book->pages()->save($page);
|
||||
$page = $this->page->find($page->id);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
@@ -890,14 +840,18 @@ class EntityRepo
|
||||
*/
|
||||
public function getPageNav($pageContent)
|
||||
{
|
||||
if ($pageContent == '') return [];
|
||||
if ($pageContent == '') {
|
||||
return [];
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||
|
||||
if (is_null($headers)) return [];
|
||||
if (is_null($headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tree = collect([]);
|
||||
foreach ($headers as $header) {
|
||||
@@ -913,7 +867,7 @@ class EntityRepo
|
||||
// Normalise headers if only smaller headers have been used
|
||||
if (count($tree) > 0) {
|
||||
$minLevel = $tree->pluck('level')->min();
|
||||
$tree = $tree->map(function($header) use ($minLevel) {
|
||||
$tree = $tree->map(function ($header) use ($minLevel) {
|
||||
$header['level'] -= ($minLevel - 2);
|
||||
return $header;
|
||||
});
|
||||
@@ -948,9 +902,12 @@ class EntityRepo
|
||||
$userId = user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
$page->updated_by = $userId;
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
@@ -961,6 +918,8 @@ class EntityRepo
|
||||
$this->savePageRevision($page, $input['summary']);
|
||||
}
|
||||
|
||||
$this->searchService->indexEntity($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -1008,7 +967,9 @@ class EntityRepo
|
||||
public function getUserPageDraftMessage(PageRevision $draft)
|
||||
{
|
||||
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
|
||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
|
||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
|
||||
return $message;
|
||||
}
|
||||
return $message . "\n" . trans('entities.pages_draft_edited_notification');
|
||||
}
|
||||
|
||||
@@ -1057,13 +1018,15 @@ class EntityRepo
|
||||
*/
|
||||
public function restorePageRevision(Page $page, Book $book, $revisionId)
|
||||
{
|
||||
$page->revision_count++;
|
||||
$this->savePageRevision($page);
|
||||
$revision = $this->getById('page_revision', $revisionId);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
$this->searchService->indexEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
@@ -1080,7 +1043,7 @@ class EntityRepo
|
||||
if ($page->draft) {
|
||||
$page->fill($data);
|
||||
if (isset($data['html'])) {
|
||||
$page->text = strip_tags($data['html']);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
}
|
||||
$page->save();
|
||||
return $page;
|
||||
@@ -1102,7 +1065,9 @@ class EntityRepo
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$draft->markdown = '';
|
||||
}
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
@@ -1156,6 +1121,7 @@ class EntityRepo
|
||||
$book->views()->delete();
|
||||
$book->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($book);
|
||||
$this->searchService->deleteEntityTerms($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
@@ -1175,12 +1141,14 @@ class EntityRepo
|
||||
$chapter->views()->delete();
|
||||
$chapter->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($chapter);
|
||||
$this->searchService->deleteEntityTerms($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param Page $page
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
@@ -1190,6 +1158,13 @@ class EntityRepo
|
||||
$page->revisions()->delete();
|
||||
$page->permissions()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($page);
|
||||
$this->searchService->deleteEntityTerms($page);
|
||||
|
||||
// Check if set as custom homepage
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
}
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
@@ -1199,17 +1174,4 @@ class EntityRepo
|
||||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Setting;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageRepo
|
||||
@@ -95,7 +92,7 @@ class ImageRepo
|
||||
* @param string $searchTerm
|
||||
* @return array
|
||||
*/
|
||||
public function searchPaginatedByType($type, $page = 0, $pageSize = 24, $searchTerm)
|
||||
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
|
||||
return $this->returnPaginated($images, $page, $pageSize);
|
||||
@@ -104,13 +101,13 @@ class ImageRepo
|
||||
/**
|
||||
* Get gallery images with a particular filter criteria such as
|
||||
* being within the current book or page.
|
||||
* @param int $pagination
|
||||
* @param int $pageSize
|
||||
* @param $filter
|
||||
* @param $pageId
|
||||
* @param int $pageNum
|
||||
* @param int $pageSize
|
||||
* @return array
|
||||
*/
|
||||
public function getGalleryFiltered($pagination = 0, $pageSize = 24, $filter, $pageId)
|
||||
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
|
||||
{
|
||||
$images = $this->image->where('type', '=', 'gallery');
|
||||
|
||||
@@ -123,7 +120,7 @@ class ImageRepo
|
||||
$images = $images->whereIn('uploaded_to', $validPageIds);
|
||||
}
|
||||
|
||||
return $this->returnPaginated($images, $pagination, $pageSize);
|
||||
return $this->returnPaginated($images, $pageNum, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,6 +129,8 @@ class ImageRepo
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
|
||||
{
|
||||
@@ -140,11 +139,39 @@ class ImageRepo
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a drawing the the database;
|
||||
* @param string $base64Uri
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function saveDrawing(string $base64Uri, int $uploadedTo)
|
||||
{
|
||||
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
|
||||
$image = $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the image content of a drawing.
|
||||
* @param Image $image
|
||||
* @param string $base64Uri
|
||||
* @return Image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
*/
|
||||
public function replaceDrawingContent(Image $image, string $base64Uri)
|
||||
{
|
||||
return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the details of an image via an array of properties.
|
||||
* @param Image $image
|
||||
* @param array $updateDetails
|
||||
* @return Image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updateImageDetails(Image $image, $updateDetails)
|
||||
{
|
||||
@@ -170,6 +197,8 @@ class ImageRepo
|
||||
/**
|
||||
* Load thumbnails onto an image object.
|
||||
* @param Image $image
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function loadThumbs(Image $image)
|
||||
{
|
||||
@@ -183,22 +212,45 @@ class ImageRepo
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool $keepRatio
|
||||
* @return string
|
||||
* @throws \BookStack\Exceptions\ImageUploadException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
try {
|
||||
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
|
||||
} catch (FileNotFoundException $exception) {
|
||||
$image->delete();
|
||||
return [];
|
||||
} catch (\Exception $exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw image data from an Image.
|
||||
* @param Image $image
|
||||
* @return null|string
|
||||
*/
|
||||
public function getImageData(Image $image)
|
||||
{
|
||||
try {
|
||||
return $this->imageService->getImageData($image);
|
||||
} catch (\Exception $exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Check if the provided image type is valid.
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
public function isValidType($type)
|
||||
{
|
||||
$validTypes = ['drawing', 'gallery', 'cover', 'system', 'user'];
|
||||
return in_array($type, $validTypes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
@@ -149,5 +148,4 @@ class PermissionsRepo
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class TagRepo
|
||||
* @param $entityType
|
||||
* @param $entityId
|
||||
* @param string $action
|
||||
* @return \Illuminate\Database\Eloquent\Model|null|static
|
||||
*/
|
||||
public function getEntity($entityType, $entityId, $action = 'view')
|
||||
{
|
||||
@@ -51,7 +52,9 @@ class TagRepo
|
||||
public function getForEntity($entityType, $entityId)
|
||||
{
|
||||
$entity = $this->getEntity($entityType, $entityId);
|
||||
if ($entity === null) return collect();
|
||||
if ($entity === null) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $entity->tags;
|
||||
}
|
||||
@@ -94,7 +97,9 @@ class TagRepo
|
||||
$query = $query->orderBy('count', 'desc')->take(50);
|
||||
}
|
||||
|
||||
if ($tagName !== false) $query = $query->where('name', '=', $tagName);
|
||||
if ($tagName !== false) {
|
||||
$query = $query->where('name', '=', $tagName);
|
||||
}
|
||||
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($query, 'tags', 'entity_id', 'entity_type');
|
||||
return $query->get(['value'])->pluck('value');
|
||||
@@ -111,7 +116,9 @@ class TagRepo
|
||||
$entity->tags()->delete();
|
||||
$newTags = [];
|
||||
foreach ($tags as $tag) {
|
||||
if (trim($tag['name']) === '') continue;
|
||||
if (trim($tag['name']) === '') {
|
||||
continue;
|
||||
}
|
||||
$newTags[] = $this->newInstanceFromInput($tag);
|
||||
}
|
||||
|
||||
@@ -131,5 +138,4 @@ class TagRepo
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Image;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Exception;
|
||||
use Images;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
@@ -57,13 +61,13 @@ class UserRepo
|
||||
* @param $sortData
|
||||
* @return \Illuminate\Database\Eloquent\Builder|static
|
||||
*/
|
||||
public function getAllUsersPaginatedAndSorted($count = 20, $sortData)
|
||||
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
||||
{
|
||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
||||
|
||||
if ($sortData['search']) {
|
||||
$term = '%' . $sortData['search'] . '%';
|
||||
$query->where(function($query) use ($term) {
|
||||
$query->where(function ($query) use ($term) {
|
||||
$query->where('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
@@ -83,16 +87,7 @@ class UserRepo
|
||||
$this->attachDefaultRole($user);
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.disable_services')) {
|
||||
try {
|
||||
$avatar = \Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
$user->save();
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
}
|
||||
}
|
||||
$this->downloadGravatarToUserAvatar($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
@@ -104,10 +99,27 @@ class UserRepo
|
||||
public function attachDefaultRole($user)
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId === false) $roleId = $this->role->first()->id;
|
||||
if ($roleId === false) {
|
||||
$roleId = $this->role->first()->id;
|
||||
}
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a user to a system-level role.
|
||||
* @param User $user
|
||||
* @param $systemRoleName
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function attachSystemRole(User $user, $systemRoleName)
|
||||
{
|
||||
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
|
||||
if ($role === null) {
|
||||
throw new NotFoundException("Role '{$systemRoleName}' not found");
|
||||
}
|
||||
$user->attachRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the give user is the only admin.
|
||||
* @param User $user
|
||||
@@ -115,10 +127,14 @@ class UserRepo
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
{
|
||||
if (!$user->roles->pluck('name')->contains('admin')) return false;
|
||||
if (!$user->hasSystemRole('admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->getRole('admin');
|
||||
if ($adminRole->users->count() > 1) return false;
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
if ($adminRole->users->count() > 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,11 +156,18 @@ class UserRepo
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
* @param User $user
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroyImage($image);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +179,7 @@ class UserRepo
|
||||
*/
|
||||
public function getActivity(User $user, $count = 20, $page = 0)
|
||||
{
|
||||
return \Activity::userActivity($user, $count, $page);
|
||||
return Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,4 +236,27 @@ class UserRepo
|
||||
return $this->role->where('system_name', '!=', 'admin')->get();
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get a gravatar image for a user and set it as their avatar.
|
||||
* Does not run if gravatar disabled in config.
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function downloadGravatarToUserAvatar(User $user)
|
||||
{
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.gravatar')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = Images::saveUserGravatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
|
||||
@@ -40,7 +39,9 @@ class Role extends Model
|
||||
{
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->getRawAttribute('name') === $permissionName) return true;
|
||||
if ($permission->getRawAttribute('name') === $permissionName) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -91,5 +92,4 @@ class Role extends Model
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
@@ -8,7 +7,7 @@ class RolePermission extends Model
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'permission_role','permission_id', 'role_id');
|
||||
return $this->belongsToMany(Role::class, 'permission_role', 'permission_id', 'role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
17
app/SearchTerm.php
Normal file
17
app/SearchTerm.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the entity that this term belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
@@ -170,5 +170,4 @@ class ActivityService
|
||||
Session::flash('success', $message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,15 +8,31 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
class AttachmentService extends UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing files.
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
$storageType = config('filesystems.default');
|
||||
|
||||
// Override default location if set to local public to ensure not visible.
|
||||
if ($storageType === 'local') {
|
||||
$storageType = 'local_secure';
|
||||
}
|
||||
|
||||
return $this->fileSystem->disk($storageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param Attachment $attachment
|
||||
* @return string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment)
|
||||
{
|
||||
$attachmentPath = $this->getStorageBasePath() . $attachment->path;
|
||||
return $this->getStorage()->get($attachmentPath);
|
||||
return $this->getStorage()->get($attachment->path);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,16 +108,6 @@ class AttachmentService extends UploadService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file storage base path, amended for storage type.
|
||||
* This allows us to keep a generic path in the database.
|
||||
* @return string
|
||||
*/
|
||||
private function getStorageBasePath()
|
||||
{
|
||||
return $this->isLocal() ? 'storage/' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file ordering for a listing of attached files.
|
||||
* @param array $attachmentList
|
||||
@@ -138,6 +144,7 @@ class AttachmentService extends UploadService
|
||||
/**
|
||||
* Delete a File from the database and storage.
|
||||
* @param Attachment $attachment
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteFile(Attachment $attachment)
|
||||
{
|
||||
@@ -157,11 +164,10 @@ class AttachmentService extends UploadService
|
||||
*/
|
||||
protected function deleteFileInStorage(Attachment $attachment)
|
||||
{
|
||||
$storedFilePath = $this->getStorageBasePath() . $attachment->path;
|
||||
$storage = $this->getStorage();
|
||||
$dirPath = dirname($storedFilePath);
|
||||
$dirPath = dirname($attachment->path);
|
||||
|
||||
$storage->delete($storedFilePath);
|
||||
$storage->delete($attachment->path);
|
||||
if (count($storage->allFiles($dirPath)) === 0) {
|
||||
$storage->deleteDirectory($dirPath);
|
||||
}
|
||||
@@ -179,23 +185,20 @@ class AttachmentService extends UploadService
|
||||
$attachmentData = file_get_contents($uploadedFile->getRealPath());
|
||||
|
||||
$storage = $this->getStorage();
|
||||
$attachmentBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
|
||||
$storageBasePath = $this->getStorageBasePath() . $attachmentBasePath;
|
||||
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
|
||||
|
||||
$uploadFileName = $attachmentName;
|
||||
while ($storage->exists($storageBasePath . $uploadFileName)) {
|
||||
while ($storage->exists($basePath . $uploadFileName)) {
|
||||
$uploadFileName = str_random(3) . $uploadFileName;
|
||||
}
|
||||
|
||||
$attachmentPath = $attachmentBasePath . $uploadFileName;
|
||||
$attachmentStoragePath = $this->getStorageBasePath() . $attachmentPath;
|
||||
|
||||
$attachmentPath = $basePath . $uploadFileName;
|
||||
try {
|
||||
$storage->put($attachmentStoragePath, $attachmentData);
|
||||
$storage->put($attachmentPath, $attachmentData);
|
||||
} catch (Exception $e) {
|
||||
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentStoragePath]));
|
||||
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
|
||||
}
|
||||
|
||||
return $attachmentPath;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,4 @@ class EmailConfirmationService
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ class ExportService
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class ExportService
|
||||
public function chapterToContainedHtml(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$pages->each(function ($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
@@ -74,9 +74,9 @@ class ExportService
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class ExportService
|
||||
public function chapterToPdf(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$pages->each(function ($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
@@ -127,7 +127,7 @@ class ExportService
|
||||
$pdf = \SnappyPDF::loadHTML($containedHtml);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
$pdf = \DomPDF::loadHTML($containedHtml);
|
||||
}
|
||||
return $pdf->output();
|
||||
}
|
||||
@@ -136,6 +136,7 @@ class ExportService
|
||||
* Bundle of the contents of a html file to be self-contained.
|
||||
* @param $htmlContent
|
||||
* @return mixed|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function containHtml($htmlContent)
|
||||
{
|
||||
@@ -153,9 +154,31 @@ class ExportService
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
}
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
|
||||
// Attempt to find local files even if url not absolute
|
||||
$base = baseUrl('/');
|
||||
if (strpos($srcString, $base) === 0) {
|
||||
$isLocal = true;
|
||||
$relString = str_replace($base, '', $srcString);
|
||||
$pathString = public_path(trim($relString, '/'));
|
||||
}
|
||||
|
||||
if ($isLocal && !file_exists($pathString)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$imageContent = file_get_contents($pathString);
|
||||
if ($isLocal) {
|
||||
$imageContent = file_get_contents($pathString);
|
||||
} else {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
||||
$imageContent = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err) {
|
||||
throw new \Exception("Image fetch failed, Received error: " . $err);
|
||||
}
|
||||
}
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
} catch (\ErrorException $e) {
|
||||
@@ -238,17 +261,4 @@ class ExportService
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Activity extends Facade
|
||||
@@ -10,5 +9,8 @@ class Activity extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'activity'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'activity';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Images extends Facade
|
||||
@@ -10,5 +9,8 @@ class Images extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'images'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'images';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Setting extends Facade
|
||||
@@ -10,5 +9,8 @@ class Setting extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'setting'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,8 @@ class Views extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'views'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'views';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,23 @@ class ImageService extends UploadService
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the storage that will be used for storing images.
|
||||
* @param string $type
|
||||
* @return \Illuminate\Contracts\Filesystem\Filesystem
|
||||
*/
|
||||
protected function getStorage($type = '')
|
||||
{
|
||||
$storageType = config('filesystems.default');
|
||||
|
||||
// Override default location if set to local public to ensure not visible.
|
||||
if ($type === 'system' && $storageType === 'local_secure') {
|
||||
$storageType = 'local';
|
||||
}
|
||||
|
||||
return $this->fileSystem->disk($storageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new image from an upload.
|
||||
* @param UploadedFile $uploadedFile
|
||||
@@ -46,6 +63,49 @@ class ImageService extends UploadService
|
||||
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new image from a uri-encoded base64 string of data.
|
||||
* @param string $base64Uri
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @param int $uploadedTo
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function saveNewFromBase64Uri(string $base64Uri, string $name, string $type, $uploadedTo = 0)
|
||||
{
|
||||
$splitData = explode(';base64,', $base64Uri);
|
||||
if (count($splitData) < 2) {
|
||||
throw new ImageUploadException("Invalid base64 image data provided");
|
||||
}
|
||||
$data = base64_decode($splitData[1]);
|
||||
return $this->saveNew($name, $data, $type, $uploadedTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the data for an image via a Base64 encoded string.
|
||||
* @param Image $image
|
||||
* @param string $base64Uri
|
||||
* @return Image
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri)
|
||||
{
|
||||
$splitData = explode(';base64,', $base64Uri);
|
||||
if (count($splitData) < 2) {
|
||||
throw new ImageUploadException("Invalid base64 image data provided");
|
||||
}
|
||||
$data = base64_decode($splitData[1]);
|
||||
$storage = $this->getStorage();
|
||||
|
||||
try {
|
||||
$storage->put($image->path, $data);
|
||||
} catch (Exception $e) {
|
||||
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path]));
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an image from url and saves it to the database.
|
||||
@@ -59,7 +119,9 @@ class ImageService extends UploadService
|
||||
{
|
||||
$imageName = $imageName ? $imageName : basename($url);
|
||||
$imageData = file_get_contents($url);
|
||||
if($imageData === false) throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
if ($imageData === false) {
|
||||
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
}
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
}
|
||||
|
||||
@@ -74,16 +136,16 @@ class ImageService extends UploadService
|
||||
*/
|
||||
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
|
||||
{
|
||||
$storage = $this->getStorage();
|
||||
$storage = $this->getStorage($type);
|
||||
$secureUploads = setting('app-secure-images');
|
||||
$imageName = str_replace(' ', '-', $imageName);
|
||||
|
||||
if ($secureUploads) $imageName = str_random(16) . '-' . $imageName;
|
||||
if ($secureUploads) {
|
||||
$imageName = str_random(16) . '-' . $imageName;
|
||||
}
|
||||
|
||||
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
|
||||
|
||||
if ($this->isLocal()) $imagePath = '/public' . $imagePath;
|
||||
|
||||
while ($storage->exists($imagePath . $imageName)) {
|
||||
$imageName = str_random(3) . $imageName;
|
||||
}
|
||||
@@ -96,8 +158,6 @@ class ImageService extends UploadService
|
||||
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $fullPath = str_replace_first('/public', '', $fullPath);
|
||||
|
||||
$imageDetails = [
|
||||
'name' => $imageName,
|
||||
'path' => $fullPath,
|
||||
@@ -112,8 +172,8 @@ class ImageService extends UploadService
|
||||
$imageDetails['updated_by'] = $userId;
|
||||
}
|
||||
|
||||
$image = Image::forceCreate($imageDetails);
|
||||
|
||||
$image = (new Image());
|
||||
$image->forceFill($imageDetails)->save();
|
||||
return $image;
|
||||
}
|
||||
|
||||
@@ -124,14 +184,22 @@ class ImageService extends UploadService
|
||||
*/
|
||||
protected function getPath(Image $image)
|
||||
{
|
||||
return ($this->isLocal()) ? ('public/' . $image->path) : $image->path;
|
||||
return $image->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the image is a gif. Returns true if it is, else false.
|
||||
* @param Image $image
|
||||
* @return boolean
|
||||
*/
|
||||
protected function isGif(Image $image) {
|
||||
return strtolower(pathinfo($this->getPath($image), PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
|
||||
*
|
||||
* @param Image $image
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
@@ -142,6 +210,10 @@ class ImageService extends UploadService
|
||||
*/
|
||||
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
|
||||
{
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
return $this->getPublicUrl($this->getPath($image));
|
||||
}
|
||||
|
||||
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
|
||||
$imagePath = $this->getPath($image);
|
||||
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||
@@ -150,8 +222,7 @@ class ImageService extends UploadService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
$storage = $this->getStorage();
|
||||
|
||||
$storage = $this->getStorage($image->type);
|
||||
if ($storage->exists($thumbFilePath)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
@@ -161,9 +232,8 @@ class ImageService extends UploadService
|
||||
} catch (Exception $e) {
|
||||
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
|
||||
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($keepRatio) {
|
||||
@@ -183,10 +253,24 @@ class ImageService extends UploadService
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw data content from an image.
|
||||
* @param Image $image
|
||||
* @return string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
*/
|
||||
public function getImageData(Image $image)
|
||||
{
|
||||
$imagePath = $this->getPath($image);
|
||||
$storage = $this->getStorage();
|
||||
return $storage->get($imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys an Image object along with its files and thumbnails.
|
||||
* @param Image $image
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroyImage(Image $image)
|
||||
{
|
||||
@@ -205,9 +289,13 @@ class ImageService extends UploadService
|
||||
|
||||
// Cleanup of empty folders
|
||||
foreach ($storage->directories($imageFolder) as $directory) {
|
||||
if ($this->isFolderEmpty($directory)) $storage->deleteDirectory($directory);
|
||||
if ($this->isFolderEmpty($directory)) {
|
||||
$storage->deleteDirectory($directory);
|
||||
}
|
||||
}
|
||||
if ($this->isFolderEmpty($imageFolder)) {
|
||||
$storage->deleteDirectory($imageFolder);
|
||||
}
|
||||
if ($this->isFolderEmpty($imageFolder)) $storage->deleteDirectory($imageFolder);
|
||||
|
||||
$image->delete();
|
||||
return true;
|
||||
@@ -216,8 +304,9 @@ class ImageService extends UploadService
|
||||
/**
|
||||
* Save a gravatar image and set a the profile image for a user.
|
||||
* @param User $user
|
||||
* @param int $size
|
||||
* @param int $size
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveUserGravatar(User $user, $size = 500)
|
||||
{
|
||||
@@ -252,14 +341,10 @@ class ImageService extends UploadService
|
||||
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
||||
}
|
||||
}
|
||||
|
||||
$this->storageUrl = $storageUrl;
|
||||
}
|
||||
|
||||
if ($this->isLocal()) $filePath = str_replace_first('public/', '', $filePath);
|
||||
|
||||
return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath;
|
||||
$basePath = ($this->storageUrl == false) ? baseUrl('/') : $this->storageUrl;
|
||||
return rtrim($basePath, '/') . $filePath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
/**
|
||||
* Class Ldap
|
||||
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
||||
@@ -93,5 +92,4 @@ class Ldap
|
||||
{
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
@@ -42,8 +41,12 @@ class LdapService
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
|
||||
if ($users['count'] === 0) return null;
|
||||
if ($users['count'] === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $users[0];
|
||||
return [
|
||||
@@ -64,8 +67,12 @@ class LdapService
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) return false;
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) return false;
|
||||
if ($ldapUser === null) {
|
||||
return false;
|
||||
}
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
@@ -95,7 +102,9 @@ class LdapService
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
if (!$ldapBind) {
|
||||
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +115,9 @@ class LdapService
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) return $this->ldapConnection;
|
||||
if ($this->ldapConnection !== null) {
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
@@ -116,7 +127,9 @@ class LdapService
|
||||
// Get port from server string and protocol if specified.
|
||||
$ldapServer = explode(':', $this->config['server']);
|
||||
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
|
||||
if (!$hasProtocol) array_unshift($ldapServer, '');
|
||||
if (!$hasProtocol) {
|
||||
array_unshift($ldapServer, '');
|
||||
}
|
||||
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
|
||||
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
|
||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
||||
@@ -149,5 +162,4 @@ class LdapService
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
@@ -10,6 +11,7 @@ use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PermissionService
|
||||
@@ -28,22 +30,25 @@ class PermissionService
|
||||
|
||||
protected $jointPermission;
|
||||
protected $role;
|
||||
protected $entityPermission;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Connection $db
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
@@ -51,6 +56,15 @@ class PermissionService
|
||||
// TODO - Update so admin still goes through filters
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
*/
|
||||
@@ -74,7 +88,9 @@ class PermissionService
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
if ($book === null) $book = false;
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
if (isset($this->entityCache['books'])) {
|
||||
$this->entityCache['books']->put($bookId, $book);
|
||||
}
|
||||
@@ -94,7 +110,9 @@ class PermissionService
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
if ($chapter === null) $chapter = false;
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
if (isset($this->entityCache['chapters'])) {
|
||||
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||
}
|
||||
@@ -108,7 +126,9 @@ class PermissionService
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
if ($this->userRoles !== false) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
@@ -133,22 +153,51 @@ class PermissionService
|
||||
$this->readyEntityCache();
|
||||
|
||||
// Get all roles (Should be the most limited dimension)
|
||||
$roles = $this->role->with('permissions')->get();
|
||||
$roles = $this->role->with('permissions')->get()->all();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
|
||||
$this->createManyJointPermissions($chapters, $roles);
|
||||
});
|
||||
/**
|
||||
* Get a query for fetching a book with it's children.
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) {
|
||||
$this->createManyJointPermissions($pages, $roles);
|
||||
});
|
||||
/**
|
||||
* Build joint permissions for an array of books
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
foreach ($books->all() as $book) {
|
||||
foreach ($book->getRelation('chapters') as $chapter) {
|
||||
$entities->push($chapter);
|
||||
}
|
||||
foreach ($book->getRelation('pages') as $page) {
|
||||
$entities->push($page);
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,18 +206,27 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
$roles = $this->role->get();
|
||||
$entities = collect([$entity]);
|
||||
|
||||
$entities = [$entity];
|
||||
if ($entity->isA('book')) {
|
||||
$entities = $entities->merge($entity->chapters);
|
||||
$entities = $entities->merge($entity->pages);
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
$entities = $entities->merge($entity->pages);
|
||||
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
|
||||
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,8 +235,8 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
$roles = $this->role->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$roles = $this->role->newQuery()->get();
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
@@ -188,23 +246,12 @@ class PermissionService
|
||||
*/
|
||||
public function buildJointPermissionForRole(Role $role)
|
||||
{
|
||||
$roles = collect([$role]);
|
||||
|
||||
$roles = [$role];
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
|
||||
$this->createManyJointPermissions($books, $roles);
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,9 +270,10 @@ class PermissionService
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
foreach ($roles as $role) {
|
||||
$role->jointPermissions()->delete();
|
||||
}
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,54 +291,94 @@ class PermissionService
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
{
|
||||
if (count($entities) === 0) return;
|
||||
$query = $this->jointPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$query->orWhere(function($query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->transaction(function () use ($entities) {
|
||||
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
$query->orWhere(function (QueryBuilder $query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
$query->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param Collection $roles
|
||||
* @param array $roles
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
{
|
||||
$this->readyEntityCache();
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
$entityRestrictedMap = [];
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$permissions = $permissionFetch->get();
|
||||
|
||||
// Create a mapping of explicit entity permissions
|
||||
$permissionMap = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
|
||||
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
|
||||
$permissionMap[$key] = $isRestricted;
|
||||
}
|
||||
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->getRelationValue('permissions') as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Joint Permission Data
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($roles as $role) {
|
||||
foreach ($this->getActions($entity) as $action) {
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action);
|
||||
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->jointPermission->insert($jointPermissions);
|
||||
|
||||
$this->db->transaction(function () use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param $entity
|
||||
* @param Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions($entity)
|
||||
protected function getActions(Entity $entity)
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
$baseActions[] = 'page-create';
|
||||
} else if ($entity->isA('book')) {
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
|
||||
return $baseActions;
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,14 +386,16 @@ class PermissionService
|
||||
* for a particular action.
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param string $action
|
||||
* @param array $permissionMap
|
||||
* @param array $rolePermissionMap
|
||||
* @return array
|
||||
*/
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action)
|
||||
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
|
||||
{
|
||||
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
|
||||
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
|
||||
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
|
||||
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
|
||||
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
|
||||
$explodedAction = explode('-', $action);
|
||||
$restrictionAction = end($explodedAction);
|
||||
|
||||
@@ -313,54 +403,50 @@ class PermissionService
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
} else {
|
||||
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
} elseif ($entity->isA('chapter')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToBook = !$book->restricted;
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
|
||||
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
|
||||
} else {
|
||||
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
} elseif ($entity->isA('page')) {
|
||||
|
||||
if (!$entity->restricted) {
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToBook = !$book->restricted;
|
||||
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
|
||||
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
|
||||
$acknowledgeChapter = ($chapter && $chapter->restricted);
|
||||
|
||||
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
|
||||
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
} else {
|
||||
$hasAccess = $entity->hasRestriction($role->id, $action);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->restricted) {
|
||||
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
// For chapters and pages, Check if explicit permissions are set on the Book.
|
||||
$book = $this->getBook($entity->book_id);
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
|
||||
$hasPermissiveAccessToParents = !$book->restricted;
|
||||
|
||||
// For pages with a chapter, Check if explicit permissions are set on the Chapter
|
||||
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
|
||||
$chapter = $this->getChapter($entity->chapter_id);
|
||||
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
|
||||
if ($chapter->restricted) {
|
||||
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray(
|
||||
$entity,
|
||||
$role,
|
||||
$action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an active restriction in an entity map.
|
||||
* @param $entityMap
|
||||
* @param Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,11 +461,10 @@ class PermissionService
|
||||
*/
|
||||
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
|
||||
{
|
||||
$entityClass = get_class($entity);
|
||||
return [
|
||||
'role_id' => $role->getRawAttribute('id'),
|
||||
'entity_id' => $entity->getRawAttribute('id'),
|
||||
'entity_type' => $entityClass,
|
||||
'entity_type' => $entity->getMorphClass(),
|
||||
'action' => $action,
|
||||
'has_permission' => $permissionAll,
|
||||
'has_permission_own' => $permissionOwn,
|
||||
@@ -406,7 +491,7 @@ class PermissionService
|
||||
$action = end($explodedPermission);
|
||||
$this->currentAction = $action;
|
||||
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
|
||||
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
|
||||
|
||||
// Handle non entity specific jointPermissions
|
||||
if (in_array($explodedPermission[0], $nonJointPermissions)) {
|
||||
@@ -476,19 +561,19 @@ class PermissionService
|
||||
* @param integer $book_id
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $fetchPageContent
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
|
||||
$pageContentSelect = $fetchPageContent ? 'html' : "''";
|
||||
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, {$pageContentSelect} as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
|
||||
{
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
@@ -496,8 +581,8 @@ class PermissionService
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
});
|
||||
@@ -514,7 +599,7 @@ class PermissionService
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
* @return Builder
|
||||
*/
|
||||
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
|
||||
{
|
||||
@@ -540,7 +625,7 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items that have entities set a a polymorphic relation.
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param $query
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
@@ -649,5 +734,4 @@ class PermissionService
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
550
app/Services/SearchService.php
Normal file
550
app/Services/SearchService.php
Normal file
@@ -0,0 +1,550 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
protected $db;
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
* @var array
|
||||
*/
|
||||
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
|
||||
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the database connection
|
||||
* @param Connection $connection
|
||||
*/
|
||||
public function setConnection(Connection $connection)
|
||||
{
|
||||
$this->db = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all entities in the system.
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
} else if (isset($terms['filters']['type'])) {
|
||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
||||
}
|
||||
|
||||
$results = collect();
|
||||
$total = 0;
|
||||
$hasMore = false;
|
||||
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) {
|
||||
continue;
|
||||
}
|
||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
|
||||
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, true);
|
||||
if ($entityTotal > $page * $count) {
|
||||
$hasMore = true;
|
||||
}
|
||||
$total += $entityTotal;
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'has_more' => $hasMore,
|
||||
'results' => $results->sortByDesc('score')->values()
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $bookId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchBook($bookId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = ['page', 'chapter'];
|
||||
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
|
||||
|
||||
$results = collect();
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) {
|
||||
continue;
|
||||
}
|
||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
return $results->sortByDesc('score')->take(20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a book for entities
|
||||
* @param integer $chapterId
|
||||
* @param string $searchString
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchChapter($chapterId, $searchString)
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
|
||||
return $pages->sortByDesc('score');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search across a particular entity type.
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param bool $getCount Return the total count of the search
|
||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
|
||||
{
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType);
|
||||
if ($getCount) {
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where(function (Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
}
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
}
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tag searches
|
||||
foreach ($terms['tags'] as $inputTerm) {
|
||||
$this->applyTagSearch($entitySelect, $inputTerm);
|
||||
}
|
||||
|
||||
// Handle filters
|
||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
||||
$functionName = camel_case('filter_' . $filterTerm);
|
||||
if (method_exists($this, $functionName)) {
|
||||
$this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a search string into components.
|
||||
* @param $searchString
|
||||
* @return array
|
||||
*/
|
||||
protected function parseSearchString($searchString)
|
||||
{
|
||||
$terms = [
|
||||
'search' => [],
|
||||
'exact' => [],
|
||||
'tags' => [],
|
||||
'filters' => []
|
||||
];
|
||||
|
||||
$patterns = [
|
||||
'exact' => '/"(.*?)"/',
|
||||
'tags' => '/\[(.*?)\]/',
|
||||
'filters' => '/\{(.*?)\}/'
|
||||
];
|
||||
|
||||
// Parse special terms
|
||||
foreach ($patterns as $termType => $pattern) {
|
||||
$matches = [];
|
||||
preg_match_all($pattern, $searchString, $matches);
|
||||
if (count($matches) > 0) {
|
||||
$terms[$termType] = $matches[1];
|
||||
$searchString = preg_replace($pattern, '', $searchString);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse standard terms
|
||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||
if ($searchTerm !== '') {
|
||||
$terms['search'][] = $searchTerm;
|
||||
}
|
||||
}
|
||||
|
||||
// Split filter values out
|
||||
$splitFilters = [];
|
||||
foreach ($terms['filters'] as $filter) {
|
||||
$explodedFilter = explode(':', $filter, 2);
|
||||
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
|
||||
}
|
||||
$terms['filters'] = $splitFilters;
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available query operators as a regex escaped list.
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getRegexEscapedOperators()
|
||||
{
|
||||
$escapedOperators = [];
|
||||
foreach ($this->queryOperators as $operator) {
|
||||
$escapedOperators[] = preg_quote($operator);
|
||||
}
|
||||
return join('|', $escapedOperators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm)
|
||||
{
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||
if (!empty($tagName)) {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
|
||||
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
|
||||
$query->whereRaw("value ${tagOperator} ${tagValue}");
|
||||
} else {
|
||||
$query->where('value', $tagOperator, $tagValue);
|
||||
}
|
||||
} else {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
});
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function indexEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteEntityTerms($entity);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||
$terms = array_merge($nameTerms, $bodyTerms);
|
||||
foreach ($terms as $index => $term) {
|
||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||
$terms[$index]['entity_id'] = $entity->id;
|
||||
}
|
||||
$this->searchTerm->newQuery()->insert($terms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities)
|
||||
{
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
$terms[] = $term;
|
||||
}
|
||||
}
|
||||
|
||||
$chunkedTerms = array_chunk($terms, 500);
|
||||
foreach ($chunkedTerms as $termChunk) {
|
||||
$this->searchTerm->newQuery()->insert($termChunk);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete and re-index the terms for all entities in the system.
|
||||
*/
|
||||
public function indexAllEntities()
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete related Entity search terms.
|
||||
* @param Entity $entity
|
||||
*/
|
||||
public function deleteEntityTerms(Entity $entity)
|
||||
{
|
||||
$entity->searchTerms()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scored term array from the given text.
|
||||
* @param $text
|
||||
* @param float|int $scoreAdjustment
|
||||
* @return array
|
||||
*/
|
||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
|
||||
$token = strtok($text, $splitChars);
|
||||
|
||||
while ($token !== false) {
|
||||
if (!isset($tokenMap[$token])) {
|
||||
$tokenMap[$token] = 0;
|
||||
}
|
||||
$tokenMap[$token]++;
|
||||
$token = strtok($splitChars);
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
foreach ($tokenMap as $token => $count) {
|
||||
$terms[] = [
|
||||
'term' => $token,
|
||||
'score' => $count * $scoreAdjustment
|
||||
];
|
||||
}
|
||||
return $terms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$this->filterInName($query, $model, $input);
|
||||
}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$functionName = camel_case('sort_by_' . $input);
|
||||
if (method_exists($this, $functionName)) {
|
||||
$this->$functionName($query, $model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorting filter options
|
||||
*/
|
||||
|
||||
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
|
||||
{
|
||||
$commentsTable = $this->db->getTablePrefix() . 'comments';
|
||||
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
|
||||
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
|
||||
|
||||
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,12 @@ class SettingService
|
||||
*/
|
||||
public function get($key, $default = false)
|
||||
{
|
||||
if ($default === false) $default = config('setting-defaults.' . $key, false);
|
||||
if (isset($this->localCache[$key])) return $this->localCache[$key];
|
||||
if ($default === false) {
|
||||
$default = config('setting-defaults.' . $key, false);
|
||||
}
|
||||
if (isset($this->localCache[$key])) {
|
||||
return $this->localCache[$key];
|
||||
}
|
||||
|
||||
$value = $this->getValueFromStore($key, $default);
|
||||
$formatted = $this->formatValue($value, $default);
|
||||
@@ -72,12 +76,16 @@ class SettingService
|
||||
{
|
||||
// Check for an overriding value
|
||||
$overrideValue = $this->getOverrideValue($key);
|
||||
if ($overrideValue !== null) return $overrideValue;
|
||||
if ($overrideValue !== null) {
|
||||
return $overrideValue;
|
||||
}
|
||||
|
||||
// Check the cache
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
$cacheVal = $this->cache->get($cacheKey, null);
|
||||
if ($cacheVal !== null) return $cacheVal;
|
||||
if ($cacheVal !== null) {
|
||||
return $cacheVal;
|
||||
}
|
||||
|
||||
// Check the database
|
||||
$settingObject = $this->getSettingObjectByKey($key);
|
||||
@@ -98,6 +106,9 @@ class SettingService
|
||||
{
|
||||
$cacheKey = $this->cachePrefix . $key;
|
||||
$this->cache->forget($cacheKey);
|
||||
if (isset($this->localCache[$key])) {
|
||||
unset($this->localCache[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,11 +120,17 @@ class SettingService
|
||||
protected function formatValue($value, $default)
|
||||
{
|
||||
// Change string booleans to actual booleans
|
||||
if ($value === 'true') $value = true;
|
||||
if ($value === 'false') $value = false;
|
||||
if ($value === 'true') {
|
||||
$value = true;
|
||||
}
|
||||
if ($value === 'false') {
|
||||
$value = false;
|
||||
}
|
||||
|
||||
// Set to default if empty
|
||||
if ($value === '') $value = $default;
|
||||
if ($value === '') {
|
||||
$value = $default;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
@@ -222,8 +239,9 @@ class SettingService
|
||||
*/
|
||||
protected function getOverrideValue($key)
|
||||
{
|
||||
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') return false;
|
||||
if ($key === 'registration-enabled' && config('auth.method') === 'ldap') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Http\Requests\Request;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
@@ -14,7 +16,7 @@ class SocialAuthService
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch'];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
@@ -91,7 +93,6 @@ class SocialAuthService
|
||||
public function handleLoginCallback($socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
|
||||
// Get user details from social driver
|
||||
$socialUser = $this->socialite->driver($driver)->user();
|
||||
$socialId = $socialUser->getId();
|
||||
@@ -104,7 +105,8 @@ class SocialAuthService
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
if (!$isLoggedIn && $socialAccount !== null) {
|
||||
return $this->logUserIn($socialAccount->user);
|
||||
auth()->login($socialAccount->user);
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// When a user is logged in but the social account does not exist,
|
||||
@@ -134,14 +136,7 @@ class SocialAuthService
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
|
||||
private function logUserIn($user)
|
||||
{
|
||||
auth()->login($user);
|
||||
return redirect('/');
|
||||
throw new SocialSignInException($message, '/login');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,8 +150,12 @@ class SocialAuthService
|
||||
{
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
if (!in_array($driver, $this->validSocialDrivers)) {
|
||||
abort(404, trans('errors.social_driver_not_found'));
|
||||
}
|
||||
if (!$this->checkDriverConfigured($driver)) {
|
||||
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -225,5 +224,4 @@ class SocialAuthService
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect(user()->getEditUrl());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,6 @@ class UploadService
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* @var FileSystemInstance
|
||||
*/
|
||||
protected $storageInstance;
|
||||
|
||||
|
||||
/**
|
||||
* FileService constructor.
|
||||
@@ -32,15 +27,10 @@ class UploadService
|
||||
*/
|
||||
protected function getStorage()
|
||||
{
|
||||
if ($this->storageInstance !== null) return $this->storageInstance;
|
||||
|
||||
$storageType = config('filesystems.default');
|
||||
$this->storageInstance = $this->fileSystem->disk($storageType);
|
||||
|
||||
return $this->storageInstance;
|
||||
return $this->fileSystem->disk($storageType);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether or not a folder is empty.
|
||||
* @param $path
|
||||
@@ -52,13 +42,4 @@ class UploadService
|
||||
$folders = $this->getStorage()->directories($path);
|
||||
return (count($files) === 0 && count($folders) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if using a local filesystem.
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLocal()
|
||||
{
|
||||
return strtolower(config('filesystems.default')) === 'local';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ class ViewService
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return 0;
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return 0;
|
||||
}
|
||||
$view = $entity->views()->where('user_id', '=', $user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
@@ -62,7 +64,7 @@ class ViewService
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
};
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
@@ -77,12 +79,16 @@ class ViewService
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return collect();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModel) {
|
||||
$query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
}
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
@@ -97,5 +103,4 @@ class ViewService
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ class Tag extends Model
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
app/User.php
28
app/User.php
@@ -60,7 +60,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
if ($this->id === 0) return ;
|
||||
if ($this->id === 0) {
|
||||
return ;
|
||||
}
|
||||
return $this->belongsToMany(Role::class);
|
||||
}
|
||||
|
||||
@@ -81,7 +83,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function hasSystemRole($role)
|
||||
{
|
||||
return $this->roles->pluck('system_name')->contains('admin');
|
||||
return $this->roles->pluck('system_name')->contains($role);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,9 +93,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function permissions($cache = true)
|
||||
{
|
||||
if(isset($this->permissions) && $cache) return $this->permissions;
|
||||
if (isset($this->permissions) && $cache) {
|
||||
return $this->permissions;
|
||||
}
|
||||
$this->load('roles.permissions');
|
||||
$permissions = $this->roles->map(function($role) {
|
||||
$permissions = $this->roles->map(function ($role) {
|
||||
return $role->permissions;
|
||||
})->flatten()->unique();
|
||||
$this->permissions = $permissions;
|
||||
@@ -107,7 +111,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function can($permissionName)
|
||||
{
|
||||
if ($this->email === 'guest') return false;
|
||||
if ($this->email === 'guest') {
|
||||
return false;
|
||||
}
|
||||
return $this->permissions()->pluck('name')->contains($permissionName);
|
||||
}
|
||||
|
||||
@@ -162,7 +168,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
{
|
||||
$default = baseUrl('/user_avatar.png');
|
||||
$imageId = $this->image_id;
|
||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
|
||||
if ($imageId === 0 || $imageId === '0' || $imageId === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
@@ -206,10 +214,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
{
|
||||
if (strlen($this->name) <= $chars) return $this->name;
|
||||
if (strlen($this->name) <= $chars) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
$splitName = explode(' ', $this->name);
|
||||
if (strlen($splitName[0]) <= $chars) return $splitName[0];
|
||||
if (strlen($splitName[0]) <= $chars) {
|
||||
return $splitName[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ function userCan($permission, Ownable $ownable = null)
|
||||
function setting($key = null, $default = false)
|
||||
{
|
||||
$settingService = resolve(\BookStack\Services\SettingService::class);
|
||||
if (is_null($key)) return $settingService;
|
||||
if (is_null($key)) {
|
||||
return $settingService;
|
||||
}
|
||||
return $settingService->get($key, $default);
|
||||
}
|
||||
|
||||
@@ -87,7 +89,9 @@ function setting($key = null, $default = false)
|
||||
function baseUrl($path, $forceAppDomain = false)
|
||||
{
|
||||
$isFullUrl = strpos($path, 'http') === 0;
|
||||
if ($isFullUrl && !$forceAppDomain) return $path;
|
||||
if ($isFullUrl && !$forceAppDomain) {
|
||||
return $path;
|
||||
}
|
||||
$path = trim($path, '/');
|
||||
|
||||
// Remove non-specified domain if forced and we have a domain
|
||||
@@ -126,12 +130,50 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
|
||||
return app('redirect')->to($to, $status, $headers, $secure);
|
||||
}
|
||||
|
||||
function icon($name, $attrs = []) {
|
||||
$iconPath = resource_path('assets/icons/' . $name . '.svg');
|
||||
/**
|
||||
* Get a path to a theme resource.
|
||||
* @param string $path
|
||||
* @return string|boolean
|
||||
*/
|
||||
function theme_path($path = '')
|
||||
{
|
||||
$theme = config('view.theme');
|
||||
if (!$theme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fetch an SVG icon as a string.
|
||||
* Checks for icons defined within a custom theme before defaulting back
|
||||
* to the 'resources/assets/icons' folder.
|
||||
*
|
||||
* Returns an empty string if icon file not found.
|
||||
* @param $name
|
||||
* @param array $attrs
|
||||
* @return mixed
|
||||
*/
|
||||
function icon($name, $attrs = [])
|
||||
{
|
||||
$attrs = array_merge([
|
||||
'class' => 'svg-icon',
|
||||
'data-icon' => $name
|
||||
], $attrs);
|
||||
$attrString = ' ';
|
||||
foreach ($attrs as $attrName => $attr) {
|
||||
$attrString .= $attrName . '="' . $attr . '" ';
|
||||
}
|
||||
|
||||
$iconPath = resource_path('assets/icons/' . $name . '.svg');
|
||||
$themeIconPath = theme_path('icons/' . $name . '.svg');
|
||||
if ($themeIconPath && file_exists($themeIconPath)) {
|
||||
$iconPath = $themeIconPath;
|
||||
} else if (!file_exists($iconPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$fileContents = file_get_contents($iconPath);
|
||||
return str_replace('<svg', '<svg' . $attrString, $fileContents);
|
||||
}
|
||||
@@ -159,11 +201,15 @@ function sortUrl($path, $data, $overrideData = [])
|
||||
|
||||
foreach ($queryData as $name => $value) {
|
||||
$trimmedVal = trim($value);
|
||||
if ($trimmedVal === '') continue;
|
||||
if ($trimmedVal === '') {
|
||||
continue;
|
||||
}
|
||||
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
|
||||
}
|
||||
|
||||
if (count($queryStringSections) === 0) return $path;
|
||||
if (count($queryStringSections) === 0) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return baseUrl($path . '?' . implode('&', $queryStringSections));
|
||||
}
|
||||
}
|
||||
|
||||
16
artisan
16
artisan
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register The Auto Loader
|
||||
| Initialize The App
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Composer provides a convenient, automatically generated class loader
|
||||
| for our application. We just need to utilize it! We'll require it
|
||||
| into the script here so that we do not have to worry about the
|
||||
| loading of any our classes "manually". Feels great to relax.
|
||||
| We need to get things going before we start up the app.
|
||||
| The init file loads everything in, in the correct order.
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/bootstrap/autoload.php';
|
||||
require __DIR__.'/bootstrap/init.php';
|
||||
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
@@ -40,7 +40,7 @@ $status = $kernel->handle(
|
||||
| Shutdown The Application
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Once Artisan has finished running. We will fire off the shutdown events
|
||||
| Once Artisan has finished running, we will fire off the shutdown events
|
||||
| so that any final work may be done by the application before we shut
|
||||
| down the process. This is the last thing to happen to the request.
|
||||
|
|
||||
@@ -48,4 +48,4 @@ $status = $kernel->handle(
|
||||
|
||||
$kernel->terminate($input, $status);
|
||||
|
||||
exit($status);
|
||||
exit($status);
|
||||
@@ -1,6 +1,15 @@
|
||||
<?php
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Load Our Own Helpers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This custom function loads any helpers, before the Laravel Framework
|
||||
| is built so we can override any helpers as we please.
|
||||
|
|
||||
*/
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -13,23 +22,4 @@ define('LARAVEL_START', microtime(true));
|
||||
| loading of any our classes "manually". Feels great to relax.
|
||||
|
|
||||
*/
|
||||
|
||||
require __DIR__.'/../app/helpers.php';
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include The Compiled Class File
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| To dramatically increase your application's performance, you may use a
|
||||
| compiled class file which contains all of the classes commonly used
|
||||
| by a request. The Artisan "optimize" is used to create this file.
|
||||
|
|
||||
*/
|
||||
|
||||
$compiledPath = __DIR__.'/cache/compiled.php';
|
||||
|
||||
if (file_exists($compiledPath)) {
|
||||
require $compiledPath;
|
||||
}
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
@@ -1,31 +1,38 @@
|
||||
{
|
||||
"name": "ssddanbrown/bookstack",
|
||||
"name": "bookstackapp/bookstack",
|
||||
"description": "BookStack documentation platform",
|
||||
"keywords": ["BookStack", "Documentation"],
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=5.6.4",
|
||||
"laravel/framework": "5.4.*",
|
||||
"php": ">=7.0.0",
|
||||
"laravel/framework": "5.5.*",
|
||||
"fideloper/proxy": "~3.3",
|
||||
"ext-tidy": "*",
|
||||
"intervention/image": "^2.3",
|
||||
"intervention/image": "^2.4",
|
||||
"laravel/socialite": "^3.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.2.3",
|
||||
"barryvdh/laravel-debugbar": "^2.3.2",
|
||||
"league/flysystem-aws-s3-v3": "^1.0",
|
||||
"barryvdh/laravel-dompdf": "^0.8",
|
||||
"barryvdh/laravel-dompdf": "^0.8.1",
|
||||
"predis/predis": "^1.1",
|
||||
"gathercontent/htmldiff": "^0.2.1",
|
||||
"barryvdh/laravel-snappy": "^0.3.1",
|
||||
"laravel/browser-kit-testing": "^1.0",
|
||||
"socialiteproviders/slack": "^3.0"
|
||||
"barryvdh/laravel-snappy": "^0.4.0",
|
||||
"socialiteproviders/slack": "^3.0",
|
||||
"socialiteproviders/microsoft-azure": "^3.0",
|
||||
"socialiteproviders/okta": "^1.0",
|
||||
"socialiteproviders/gitlab": "^3.0",
|
||||
"socialiteproviders/twitch": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"filp/whoops": "~2.0",
|
||||
"fzaninotto/faker": "~1.4",
|
||||
"mockery/mockery": "0.9.*",
|
||||
"phpunit/phpunit": "~5.0",
|
||||
"mockery/mockery": "~1.0",
|
||||
"phpunit/phpunit": "~6.0",
|
||||
"symfony/css-selector": "3.1.*",
|
||||
"symfony/dom-crawler": "3.1.*"
|
||||
"symfony/dom-crawler": "3.1.*",
|
||||
"laravel/browser-kit-testing": "^2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.4.1",
|
||||
"barryvdh/laravel-debugbar": "^3.1.0",
|
||||
"squizlabs/php_codesniffer": "^3.2"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
@@ -56,17 +63,23 @@
|
||||
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postInstall",
|
||||
"php artisan optimize",
|
||||
"php artisan cache:clear",
|
||||
"php artisan view:clear"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
|
||||
"php artisan optimize"
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover"
|
||||
],
|
||||
"refresh-test-database": [
|
||||
"php artisan migrate:refresh --database=mysql_testing",
|
||||
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist"
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"platform": {
|
||||
"php": "7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6023
composer.lock
generated
6023
composer.lock
generated
File diff suppressed because it is too large
Load Diff
18
config/app.php
Normal file → Executable file
18
config/app.php
Normal file → Executable file
@@ -2,10 +2,13 @@
|
||||
|
||||
return [
|
||||
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
'editor' => env('APP_EDITOR', 'html'),
|
||||
'views' => [
|
||||
'books' => env('APP_VIEWS_BOOKS', 'list')
|
||||
],
|
||||
|
||||
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -58,6 +61,7 @@ return [
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -100,7 +104,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'log' => 'single',
|
||||
'log' => env('APP_LOGGING', 'single'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -146,8 +150,6 @@ return [
|
||||
*/
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
Barryvdh\DomPDF\ServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
Barryvdh\Snappy\ServiceProvider::class,
|
||||
|
||||
|
||||
@@ -162,7 +164,6 @@ return [
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
BookStack\Providers\CustomFacadeProvider::class,
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -218,9 +219,8 @@ return [
|
||||
*/
|
||||
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'PDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||
'Debugbar' => Barryvdh\Debugbar\Facade::class,
|
||||
|
||||
/**
|
||||
* Custom
|
||||
@@ -233,4 +233,6 @@ return [
|
||||
|
||||
],
|
||||
|
||||
'proxies' => env('APP_PROXIES', ''),
|
||||
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user