mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-17 19:06:49 +03:00
Compare commits
566 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab99a26ec | ||
|
|
9a7fecd269 | ||
|
|
eb5069ca66 | ||
|
|
0306253c45 | ||
|
|
71b6f09128 | ||
|
|
67e0c3d2a5 | ||
|
|
6aeb1387aa | ||
|
|
fa83e6bda4 | ||
|
|
ae89e05a25 | ||
|
|
a50153d221 | ||
|
|
cdb1c7ef88 | ||
|
|
0f7b0ad45a | ||
|
|
d34b91f2c9 | ||
|
|
dfadaa28f6 | ||
|
|
fae564ff32 | ||
|
|
502b22a0f2 | ||
|
|
f9feeef5c9 | ||
|
|
a6674a5a5e | ||
|
|
fb18576259 | ||
|
|
7238a01f89 | ||
|
|
93f92e9e16 | ||
|
|
d92efd4edc | ||
|
|
448f7d091b | ||
|
|
11470b85f9 | ||
|
|
9c07619099 | ||
|
|
60a224f7a1 | ||
|
|
e392e1fd8b | ||
|
|
64d5763d08 | ||
|
|
007059273e | ||
|
|
106432ee4e | ||
|
|
0ade9b5b9b | ||
|
|
736d7118b0 | ||
|
|
b612cf9e4c | ||
|
|
1a72208d27 | ||
|
|
7f437c2e3c | ||
|
|
cfdf5b93d9 | ||
|
|
a8dc0d449b | ||
|
|
a0381f76bf | ||
|
|
3cd08382e9 | ||
|
|
58a6b2df7d | ||
|
|
582158f70e | ||
|
|
03ee3d21ba | ||
|
|
b99229a5c3 | ||
|
|
2fc513984d | ||
|
|
34499658d5 | ||
|
|
a8f18c0102 | ||
|
|
c85cace48b | ||
|
|
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 | ||
|
|
8b82753218 | ||
|
|
3368fe42d8 | ||
|
|
c3ea0d333e | ||
|
|
d447355a61 | ||
|
|
8e2437498f | ||
|
|
9de85283cd | ||
|
|
b3d4c199ae | ||
|
|
4e71a5a47b | ||
|
|
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
|
||||
|
||||
84
.github/CODE_OF_CONDUCT.md
vendored
Normal file
84
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance, race,
|
||||
religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
### Project Maintainer Standards
|
||||
|
||||
Project maintainers should generally follow these additional standards:
|
||||
|
||||
* Avoid using a negative or harsh tone in communication, Even if the other party
|
||||
is being negative themselves.
|
||||
* When providing criticism, try to make it constructive to lead the other person
|
||||
down the correct path.
|
||||
* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)
|
||||
in mind when deciding what's in scope of the Project.
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior. In addition, Project
|
||||
maintainers are responsible for following the standards themselves.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
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
|
||||
|
||||
|
||||
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -2,22 +2,23 @@
|
||||
/node_modules
|
||||
Homestead.yaml
|
||||
.env
|
||||
/public/dist
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist/*.map
|
||||
/public/plugins
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
_ide_helper.php
|
||||
/storage/debugbar
|
||||
.phpstorm.meta.php
|
||||
yarn.lock
|
||||
/bin
|
||||
nbproject
|
||||
.buildpath
|
||||
|
||||
.project
|
||||
|
||||
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||
|
||||
.settings/org.eclipse.php.core.prefs
|
||||
.settings/
|
||||
webpack-stats.json
|
||||
@@ -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
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Dan Brown
|
||||
Copyright (c) 2018 Dan Brown and the BookStack Project contributors
|
||||
https://github.com/BookStackApp/BookStack/graphs/contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
33
app/Book.php
33
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
|
||||
@@ -64,5 +94,4 @@ class Book extends Entity
|
||||
{
|
||||
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Chapter extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
public $searchFactor = 1.3;
|
||||
|
||||
protected $with = ['book'];
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
@@ -59,5 +58,4 @@ class Chapter extends Entity
|
||||
{
|
||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
app/Comment.php
Normal file
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...');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ class RegenerateSearch extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
protected $description = 'Re-index all content for searching';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,7 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
Commands\ClearViews::class,
|
||||
Commands\ClearActivity::class,
|
||||
Commands\ClearRevisions::class,
|
||||
Commands\RegeneratePermissions::class,
|
||||
Commands\RegenerateSearch::class
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -28,4 +24,14 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string - Name of property where the main text content is found
|
||||
*/
|
||||
public $textField = 'description';
|
||||
|
||||
/**
|
||||
* @var float - Multiplier for search indexing.
|
||||
*/
|
||||
public $searchFactor = 1.0;
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -26,7 +35,9 @@ class Entity extends Ownable
|
||||
{
|
||||
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||
|
||||
if ($matches) return true;
|
||||
if ($matches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
||||
return $entity->book_id === $this->id;
|
||||
@@ -65,6 +76,17 @@ class Entity extends Ownable
|
||||
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comments for an entity
|
||||
* @param bool $orderByCreated
|
||||
* @return MorphMany
|
||||
*/
|
||||
public function comments($orderByCreated = true)
|
||||
{
|
||||
$query = $this->morphMany(Comment::class, 'entity');
|
||||
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related search terms.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
@@ -146,7 +168,9 @@ 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) . '...';
|
||||
}
|
||||
|
||||
@@ -163,13 +187,18 @@ class Entity extends Ownable
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery(){return '';}
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of this entity
|
||||
* @param $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path){return '/';}
|
||||
|
||||
public function getUrl($path = '/')
|
||||
{
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,19 @@ class BookController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$books = $this->entityRepo->getAllPaginated('book', 10);
|
||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||
$this->setPageTitle('Books');
|
||||
return view('books/index', ['books' => $books, 'recents' => $recents, 'popular' => $popular]);
|
||||
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
||||
$this->setPageTitle(trans('entities.books'));
|
||||
return view('books/index', [
|
||||
'books' => $books,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'booksViewType' => $booksViewType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +92,12 @@ class BookController extends Controller
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||
Views::add($book);
|
||||
$this->setPageTitle($book->getShortName());
|
||||
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
||||
return view('books/show', [
|
||||
'book' => $book,
|
||||
'current' => $book,
|
||||
'bookChildren' => $bookChildren,
|
||||
'activity' => Activity::entityActivity($book, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +109,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||
}
|
||||
|
||||
@@ -114,9 +127,9 @@ class BookController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000'
|
||||
]);
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||
Activity::add($book, 'book_update', $book->id);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,7 +155,7 @@ class BookController extends Controller
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||
$books = $this->entityRepo->getAll('book', false);
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
||||
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||
}
|
||||
@@ -172,47 +185,61 @@ class BookController extends Controller
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
|
||||
// Return if no map sent
|
||||
if (!$request->has('sort-tree')) {
|
||||
if (!$request->filled('sort-tree')) {
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
// Sort pages and chapters
|
||||
$sortedBooks = [];
|
||||
$updatedModels = collect();
|
||||
$sortMap = json_decode($request->get('sort-tree'));
|
||||
$defaultBookId = $book->id;
|
||||
$sortMap = collect(json_decode($request->get('sort-tree')));
|
||||
$bookIdsInvolved = collect([$book->id]);
|
||||
|
||||
// Loop through contents of provided map and update entities accordingly
|
||||
foreach ($sortMap as $bookChild) {
|
||||
$priority = $bookChild->sort;
|
||||
$id = intval($bookChild->id);
|
||||
$isPage = $bookChild->type == 'page';
|
||||
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
|
||||
// Load models into map
|
||||
$sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
|
||||
$mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
|
||||
$mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
|
||||
// Store source and target books
|
||||
$bookIdsInvolved->push(intval($mapItem->model->book_id));
|
||||
$bookIdsInvolved->push(intval($mapItem->book));
|
||||
});
|
||||
|
||||
// Update models only if there's a change in parent chain or ordering.
|
||||
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
|
||||
$model->priority = $priority;
|
||||
if ($isPage) $model->chapter_id = $chapterId;
|
||||
// Get the books involved in the sort
|
||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
||||
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
|
||||
// Throw permission error if invalid ids or inaccessible books given.
|
||||
if (count($bookIdsInvolved) !== count($booksInvolved)) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
// Check permissions of involved books
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
$this->checkOwnablePermission('book-update', $book);
|
||||
});
|
||||
|
||||
// Perform the sort
|
||||
$sortMap->each(function ($mapItem) {
|
||||
$model = $mapItem->model;
|
||||
|
||||
$priorityChanged = intval($model->priority) !== intval($mapItem->sort);
|
||||
$bookChanged = intval($model->book_id) !== intval($mapItem->book);
|
||||
$chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
|
||||
|
||||
if ($bookChanged) {
|
||||
$this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
|
||||
}
|
||||
if ($chapterChanged) {
|
||||
$model->chapter_id = intval($mapItem->parentChapter);
|
||||
$model->save();
|
||||
$updatedModels->push($model);
|
||||
}
|
||||
|
||||
// Store involved books to be sorted later
|
||||
if (!in_array($bookId, $sortedBooks)) {
|
||||
$sortedBooks[] = $bookId;
|
||||
if ($priorityChanged) {
|
||||
$model->priority = intval($mapItem->sort);
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add activity for books
|
||||
foreach ($sortedBooks as $bookId) {
|
||||
/** @var Book $updatedBook */
|
||||
$updatedBook = $this->entityRepo->getById('book', $bookId);
|
||||
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
|
||||
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||
}
|
||||
// Rebuild permissions and add activity for involved books.
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
$this->entityRepo->buildJointPermissionsForBook($book);
|
||||
Activity::add($book, 'book_sort', $book->id);
|
||||
});
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -107,17 +107,14 @@ class ChapterController extends Controller
|
||||
* @param $bookSlug
|
||||
* @param $chapterSlug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
if ($chapter->name !== $request->get('name')) {
|
||||
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
|
||||
}
|
||||
$chapter->fill($request->all());
|
||||
$chapter->updated_by = user()->id;
|
||||
$chapter->save();
|
||||
|
||||
$this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
|
||||
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
||||
return redirect($chapter->getUrl());
|
||||
}
|
||||
@@ -159,7 +156,8 @@ class ChapterController extends Controller
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showMove($bookSlug, $chapterSlug) {
|
||||
public function showMove($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
@@ -177,7 +175,8 @@ class ChapterController extends Controller
|
||||
* @return mixed
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||
public function move($bookSlug, $chapterSlug, Request $request)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||
|
||||
|
||||
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,23 +30,35 @@ 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() {
|
||||
public function getTranslations()
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||
@@ -76,4 +89,49 @@ 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for /robots.txt
|
||||
* @return $this
|
||||
*/
|
||||
public function getRobots()
|
||||
{
|
||||
$sitePublic = setting('app-public', false);
|
||||
$allowRobots = config('app.allow_robots');
|
||||
if ($allowRobots === null) {
|
||||
$allowRobots = $sitePublic;
|
||||
}
|
||||
return response()
|
||||
->view('robots', ['allowRobots' => $allowRobots])
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', [
|
||||
@@ -261,11 +275,10 @@ class PageController extends Controller
|
||||
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $utcUpdateTimestamp
|
||||
'timestamp' => $updateTime
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -323,9 +336,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 +390,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 +424,7 @@ class PageController extends Controller
|
||||
'page' => $page,
|
||||
'book' => $page->book,
|
||||
'diff' => $diff,
|
||||
'revision' => $revision
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -438,6 +454,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 +471,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',
|
||||
@@ -567,6 +585,8 @@ class PageController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$this->entityRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
||||
@@ -574,12 +594,70 @@ class PageController extends Controller
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view to copy a page.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showCopy($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flashInput(['name' => $page->name]);
|
||||
return view('pages/copy', [
|
||||
'book' => $page->book,
|
||||
'page' => $page
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of a page within the requested target destination.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function copy($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
$parent = $page->chapter ? $page->chapter : $page->book;
|
||||
} else {
|
||||
$stringExploded = explode(':', $entitySelection);
|
||||
$entityType = $stringExploded[0];
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
try {
|
||||
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
|
||||
|
||||
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
|
||||
session()->flash('success', trans('entities.pages_copy_success'));
|
||||
|
||||
return redirect($pageCopy->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions for this page.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
@@ -589,5 +667,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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,17 +36,16 @@ class SearchController extends Controller
|
||||
$searchTerm = $request->get('term');
|
||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||
|
||||
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
|
||||
$page = intval($request->get('page', '0')) ?: 1;
|
||||
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||
|
||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
|
||||
|
||||
return view('search/all', [
|
||||
'entities' => $results['results'],
|
||||
'totalResults' => $results['total'],
|
||||
'searchTerm' => $searchTerm,
|
||||
'hasNextPage' => $hasNextPage,
|
||||
'hasNextPage' => $results['has_more'],
|
||||
'nextPageLink' => $nextPageLink
|
||||
]);
|
||||
}
|
||||
@@ -88,23 +87,21 @@ class SearchController extends Controller
|
||||
*/
|
||||
public function searchEntitiesAjax(Request $request)
|
||||
{
|
||||
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||
$searchTerm = $request->get('term', false);
|
||||
$permission = $request->get('permission', 'view');
|
||||
|
||||
// Search for entities otherwise show most popular
|
||||
if ($searchTerm !== false) {
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
|
||||
}
|
||||
|
||||
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,7 +19,9 @@ class Localization
|
||||
$locale = $defaultLang;
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (!in_array($lang, $availableLocales)) continue;
|
||||
if (!in_array($lang, $availableLocales)) {
|
||||
continue;
|
||||
}
|
||||
$locale = $lang;
|
||||
break;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,4 @@ class ConfirmEmail extends Notification implements ShouldQueue
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
protected $fillable = ['name', 'html', 'priority', 'markdown'];
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
@@ -101,8 +99,8 @@ class Page extends Entity
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{ $htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
{
|
||||
$htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,6 +4,7 @@ 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;
|
||||
@@ -76,11 +77,15 @@ class EntityRepo
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
|
||||
ViewService $viewService, PermissionService $permissionService,
|
||||
TagRepo $tagRepo, SearchService $searchService
|
||||
)
|
||||
{
|
||||
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;
|
||||
@@ -112,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);
|
||||
}
|
||||
@@ -137,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);
|
||||
}
|
||||
|
||||
@@ -157,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;
|
||||
}
|
||||
|
||||
@@ -190,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();
|
||||
}
|
||||
|
||||
@@ -225,7 +240,9 @@ class EntityRepo
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
|
||||
->orderBy('created_at', 'desc');
|
||||
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
@@ -244,7 +261,9 @@ class EntityRepo
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type))
|
||||
->orderBy('updated_at', 'desc');
|
||||
if (strtolower($type) === 'page') $query = $query->where('draft', '=', false);
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
@@ -341,12 +360,16 @@ class EntityRepo
|
||||
$parents[$key] = $entities[$index];
|
||||
$parents[$key]->setAttribute('pages', collect());
|
||||
}
|
||||
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
|
||||
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
|
||||
$tree[] = $entities[$index];
|
||||
}
|
||||
$entities[$index]->book = $book;
|
||||
}
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
|
||||
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
|
||||
continue;
|
||||
}
|
||||
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
|
||||
if (!isset($parents[$parentKey])) {
|
||||
$tree[] = $entity;
|
||||
@@ -425,7 +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;
|
||||
}
|
||||
|
||||
@@ -436,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([
|
||||
@@ -448,6 +474,7 @@ class EntityRepo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
@@ -465,14 +492,19 @@ class EntityRepo
|
||||
public function createFromInput($type, $input = [], $book = false)
|
||||
{
|
||||
$isChapter = strtolower($type) === 'chapter';
|
||||
$entity = $this->getEntity($type)->newInstance($input);
|
||||
$entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
|
||||
$entity->created_by = user()->id;
|
||||
$entity->updated_by = user()->id;
|
||||
$isChapter ? $book->chapters()->save($entity) : $entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
$this->searchService->indexEntity($entity);
|
||||
return $entity;
|
||||
$entityModel = $this->getEntity($type)->newInstance($input);
|
||||
$entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
|
||||
$entityModel->created_by = user()->id;
|
||||
$entityModel->updated_by = user()->id;
|
||||
$isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -491,6 +523,11 @@ class EntityRepo
|
||||
$entityModel->fill($input);
|
||||
$entityModel->updated_by = user()->id;
|
||||
$entityModel->save();
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
@@ -547,12 +584,39 @@ class EntityRepo
|
||||
*/
|
||||
protected function nameToSlug($name)
|
||||
{
|
||||
$slug = str_replace(' ', '-', strtolower($name));
|
||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
|
||||
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
|
||||
$slug = preg_replace('/\s{2,}/', ' ', $slug);
|
||||
$slug = str_replace(' ', '-', $slug);
|
||||
if ($slug === "") {
|
||||
$slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|bool $chapter
|
||||
* @return Page
|
||||
*/
|
||||
public function getDraftPage(Book $book, $chapter = false)
|
||||
{
|
||||
$page = $this->page->newInstance();
|
||||
$page->name = trans('entities.pages_initial_name');
|
||||
$page->created_by = user()->id;
|
||||
$page->updated_by = user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) {
|
||||
$page->chapter_id = $chapter->id;
|
||||
}
|
||||
|
||||
$book->pages()->save($page);
|
||||
$page = $this->page->find($page->id);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a draft page to make it a normal page.
|
||||
* Sets the slug and updates the content.
|
||||
@@ -571,7 +635,7 @@ class EntityRepo
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = strip_tags($draftPage->html);
|
||||
$draftPage->text = $this->pageToPlainText($draftPage);
|
||||
$draftPage->draft = false;
|
||||
$draftPage->revision_count = 1;
|
||||
|
||||
@@ -581,6 +645,43 @@ class EntityRepo
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of a page in a new location with a new name.
|
||||
* @param Page $page
|
||||
* @param Entity $newParent
|
||||
* @param string $newName
|
||||
* @return Page
|
||||
*/
|
||||
public function copyPage(Page $page, Entity $newParent, $newName = '')
|
||||
{
|
||||
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
|
||||
$newChapter = $newParent->isA('chapter') ? $newParent : null;
|
||||
$copyPage = $this->getDraftPage($newBook, $newChapter);
|
||||
$pageData = $page->getAttributes();
|
||||
|
||||
// Update name
|
||||
if (!empty($newName)) {
|
||||
$pageData['name'] = $newName;
|
||||
}
|
||||
|
||||
// Copy tags from previous page if set
|
||||
if ($page->tags) {
|
||||
$pageData['tags'] = [];
|
||||
foreach ($page->tags as $tag) {
|
||||
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
|
||||
}
|
||||
}
|
||||
|
||||
// Set priority
|
||||
if ($newParent->isA('chapter')) {
|
||||
$pageData['priority'] = $this->getNewChapterPriority($newParent);
|
||||
} else {
|
||||
$pageData['priority'] = $this->getNewBookPriority($newParent);
|
||||
}
|
||||
|
||||
return $this->publishPageDraft($copyPage, $pageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
@@ -590,7 +691,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;
|
||||
@@ -618,7 +721,9 @@ class EntityRepo
|
||||
*/
|
||||
protected function formatHtml($htmlText)
|
||||
{
|
||||
if ($htmlText == '') return $htmlText;
|
||||
if ($htmlText == '') {
|
||||
return $htmlText;
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
@@ -632,7 +737,9 @@ class EntityRepo
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') continue;
|
||||
if (get_class($childNode) !== 'DOMElement') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Overwrite id if not a BookStack custom id
|
||||
if ($childNode->hasAttribute('id')) {
|
||||
@@ -671,41 +778,56 @@ class EntityRepo
|
||||
/**
|
||||
* Render the page for viewing, Parsing and performing features such as page transclusion.
|
||||
* @param Page $page
|
||||
* @param bool $ignorePermissions
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function renderPage(Page $page)
|
||||
public function renderPage(Page $page, $ignorePermissions = false)
|
||||
{
|
||||
$content = $page->html;
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($content);
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
|
||||
if (count($matches[0]) === 0) return $content;
|
||||
if (count($matches[0]) === 0) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$topLevelTags = ['table', 'ul', 'ol'];
|
||||
foreach ($matches[1] as $index => $includeId) {
|
||||
$splitInclude = explode('#', $includeId, 2);
|
||||
$pageId = intval($splitInclude[0]);
|
||||
if (is_nan($pageId)) continue;
|
||||
if (is_nan($pageId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$page = $this->getById('page', $pageId);
|
||||
if ($page === null) {
|
||||
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
|
||||
if ($matchedPage === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($splitInclude) === 1) {
|
||||
$content = str_replace($matches[0][$index], $page->html, $content);
|
||||
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||
if ($matchingElem === null) {
|
||||
$content = str_replace($matches[0][$index], '', $content);
|
||||
continue;
|
||||
}
|
||||
$innerContent = '';
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
|
||||
if ($isTopLevel) {
|
||||
$innerContent .= $doc->saveHTML($matchingElem);
|
||||
} else {
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
}
|
||||
}
|
||||
$content = str_replace($matches[0][$index], trim($innerContent), $content);
|
||||
}
|
||||
@@ -714,25 +836,34 @@ class EntityRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|bool $chapter
|
||||
* @return Page
|
||||
* Escape script tags within HTML content.
|
||||
* @param string $html
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDraftPage(Book $book, $chapter = false)
|
||||
protected function escapeScripts(string $html)
|
||||
{
|
||||
$page = $this->page->newInstance();
|
||||
$page->name = trans('entities.pages_initial_name');
|
||||
$page->created_by = user()->id;
|
||||
$page->updated_by = user()->id;
|
||||
$page->draft = true;
|
||||
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
|
||||
$matches = [];
|
||||
preg_match_all($scriptSearchRegex, $html, $matches);
|
||||
if (count($matches) === 0) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
if ($chapter) $page->chapter_id = $chapter->id;
|
||||
foreach ($matches[0] as $match) {
|
||||
$html = str_replace($match, htmlentities($match), $html);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
$book->pages()->save($page);
|
||||
$page = $this->page->find($page->id);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
/**
|
||||
* Get the plain text version of a page's content.
|
||||
* @param Page $page
|
||||
* @return string
|
||||
*/
|
||||
public function pageToPlainText(Page $page)
|
||||
{
|
||||
$html = $this->renderPage($page);
|
||||
return strip_tags($html);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -758,14 +889,18 @@ class EntityRepo
|
||||
*/
|
||||
public function getPageNav($pageContent)
|
||||
{
|
||||
if ($pageContent == '') return [];
|
||||
if ($pageContent == '') {
|
||||
return [];
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||
|
||||
if (is_null($headers)) return [];
|
||||
if (is_null($headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tree = collect([]);
|
||||
foreach ($headers as $header) {
|
||||
@@ -781,7 +916,7 @@ class EntityRepo
|
||||
// Normalise headers if only smaller headers have been used
|
||||
if (count($tree) > 0) {
|
||||
$minLevel = $tree->pluck('level')->min();
|
||||
$tree = $tree->map(function($header) use ($minLevel) {
|
||||
$tree = $tree->map(function ($header) use ($minLevel) {
|
||||
$header['level'] -= ($minLevel - 2);
|
||||
return $header;
|
||||
});
|
||||
@@ -816,8 +951,10 @@ class EntityRepo
|
||||
$userId = user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = strip_tags($page->html);
|
||||
if (setting('app-editor') !== 'markdown') $page->markdown = '';
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
$page->updated_by = $userId;
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
@@ -879,7 +1016,9 @@ class EntityRepo
|
||||
public function getUserPageDraftMessage(PageRevision $draft)
|
||||
{
|
||||
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
|
||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) return $message;
|
||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
|
||||
return $message;
|
||||
}
|
||||
return $message . "\n" . trans('entities.pages_draft_edited_notification');
|
||||
}
|
||||
|
||||
@@ -933,7 +1072,7 @@ class EntityRepo
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
|
||||
$page->text = strip_tags($page->html);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
$this->searchService->indexEntity($page);
|
||||
@@ -953,7 +1092,7 @@ class EntityRepo
|
||||
if ($page->draft) {
|
||||
$page->fill($data);
|
||||
if (isset($data['html'])) {
|
||||
$page->text = strip_tags($data['html']);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
}
|
||||
$page->save();
|
||||
return $page;
|
||||
@@ -975,7 +1114,9 @@ class EntityRepo
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') $draft->markdown = '';
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$draft->markdown = '';
|
||||
}
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
@@ -1056,6 +1197,7 @@ class EntityRepo
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param Page $page
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
@@ -1067,6 +1209,12 @@ class EntityRepo
|
||||
$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);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
@@ -1075,17 +1223,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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,5 +14,4 @@ class SearchTerm extends Model
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ class ExportService
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
protected $imageService;
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
* @param $entityRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->imageService = $imageService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,12 +26,13 @@ class ExportService
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$pageHtml = view('pages/export', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->containHtml($pageHtml);
|
||||
}
|
||||
@@ -38,11 +41,12 @@ class ExportService
|
||||
* Convert a chapter to a self-contained HTML file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function chapterToContainedHtml(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$pages->each(function ($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
@@ -56,6 +60,7 @@ class ExportService
|
||||
* Convert a book to a self-contained HTML file.
|
||||
* @param Book $book
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function bookToContainedHtml(Book $book)
|
||||
{
|
||||
@@ -71,12 +76,13 @@ class ExportService
|
||||
* Convert a page to a PDF file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
$this->entityRepo->renderPage($page);
|
||||
$html = view('pages/pdf', [
|
||||
'page' => $page,
|
||||
'pageContent' => $this->entityRepo->renderPage($page)
|
||||
'page' => $page
|
||||
])->render();
|
||||
return $this->htmlToPdf($html);
|
||||
}
|
||||
@@ -85,11 +91,12 @@ class ExportService
|
||||
* Convert a chapter to a PDF file.
|
||||
* @param Chapter $chapter
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function chapterToPdf(Chapter $chapter)
|
||||
{
|
||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||
$pages->each(function($page) {
|
||||
$pages->each(function ($page) {
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
});
|
||||
$html = view('chapters/export', [
|
||||
@@ -103,6 +110,7 @@ class ExportService
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @return string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
@@ -118,6 +126,7 @@ class ExportService
|
||||
* Convert normal webpage HTML to a PDF.
|
||||
* @param $html
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function htmlToPdf($html)
|
||||
{
|
||||
@@ -127,7 +136,7 @@ class ExportService
|
||||
$pdf = \SnappyPDF::loadHTML($containedHtml);
|
||||
$pdf->setOption('print-media-type', true);
|
||||
} else {
|
||||
$pdf = \PDF::loadHTML($containedHtml);
|
||||
$pdf = \DomPDF::loadHTML($containedHtml);
|
||||
}
|
||||
return $pdf->output();
|
||||
}
|
||||
@@ -136,6 +145,7 @@ class ExportService
|
||||
* Bundle of the contents of a html file to be self-contained.
|
||||
* @param $htmlContent
|
||||
* @return mixed|string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function containHtml($htmlContent)
|
||||
{
|
||||
@@ -145,23 +155,14 @@ class ExportService
|
||||
// Replace image src with base64 encoded image strings
|
||||
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
|
||||
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
|
||||
$oldImgString = $imgMatch;
|
||||
$oldImgTagString = $imgMatch;
|
||||
$srcString = $imageTagsOutput[2][$index];
|
||||
$isLocal = strpos(trim($srcString), 'http') !== 0;
|
||||
if ($isLocal) {
|
||||
$pathString = public_path(trim($srcString, '/'));
|
||||
} else {
|
||||
$pathString = $srcString;
|
||||
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
|
||||
if ($imageEncoded === null) {
|
||||
$imageEncoded = $srcString;
|
||||
}
|
||||
if ($isLocal && !file_exists($pathString)) continue;
|
||||
try {
|
||||
$imageContent = file_get_contents($pathString);
|
||||
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
|
||||
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
|
||||
} catch (\ErrorException $e) {
|
||||
$newImageString = '';
|
||||
}
|
||||
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
|
||||
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
|
||||
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,17 +239,4 @@ class ExportService
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,23 @@ 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 +211,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 +223,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 +233,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 +254,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 +290,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 +305,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)
|
||||
{
|
||||
@@ -231,6 +321,53 @@ class ImageService extends UploadService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a image URI to a Base64 encoded string.
|
||||
* Attempts to find locally via set storage method first.
|
||||
* @param string $uri
|
||||
* @return null|string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
*/
|
||||
public function imageUriToBase64(string $uri)
|
||||
{
|
||||
$isLocal = strpos(trim($uri), 'http') !== 0;
|
||||
|
||||
// Attempt to find local files even if url not absolute
|
||||
$base = baseUrl('/');
|
||||
if (!$isLocal && strpos($uri, $base) === 0) {
|
||||
$isLocal = true;
|
||||
$uri = str_replace($base, '', $uri);
|
||||
}
|
||||
|
||||
$imageData = null;
|
||||
|
||||
if ($isLocal) {
|
||||
$uri = trim($uri, '/');
|
||||
$storage = $this->getStorage();
|
||||
if ($storage->exists($uri)) {
|
||||
$imageData = $storage->get($uri);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
||||
$imageData = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err) {
|
||||
throw new \Exception("Image fetch failed, Received error: " . $err);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($imageData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a public facing url for an image by checking relevant environment variables.
|
||||
* @param string $filePath
|
||||
@@ -252,14 +389,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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +67,19 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache()
|
||||
protected function readyEntityCache($entities = [])
|
||||
{
|
||||
$this->entityCache = [
|
||||
'books' => collect(),
|
||||
'chapters' => collect()
|
||||
];
|
||||
$this->entityCache = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$type = $entity->getType();
|
||||
if (!isset($this->entityCache[$type])) {
|
||||
$this->entityCache[$type] = collect();
|
||||
}
|
||||
$this->entityCache[$type]->put($entity->id, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,14 +89,13 @@ class PermissionService
|
||||
*/
|
||||
protected function getBook($bookId)
|
||||
{
|
||||
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
|
||||
return $this->entityCache['books']->get($bookId);
|
||||
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
|
||||
return $this->entityCache['book']->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
if ($book === null) $book = false;
|
||||
if (isset($this->entityCache['books'])) {
|
||||
$this->entityCache['books']->put($bookId, $book);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
|
||||
return $book;
|
||||
@@ -103,14 +108,13 @@ class PermissionService
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
{
|
||||
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
|
||||
return $this->entityCache['chapters']->get($chapterId);
|
||||
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
|
||||
return $this->entityCache['chapter']->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
if ($chapter === null) $chapter = false;
|
||||
if (isset($this->entityCache['chapters'])) {
|
||||
$this->entityCache['chapters']->put($chapterId, $chapter);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
@@ -122,7 +126,9 @@ class PermissionService
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
if ($this->userRoles !== false) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
@@ -161,9 +167,9 @@ class PermissionService
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function($query) {
|
||||
}, 'pages' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
@@ -173,8 +179,10 @@ class PermissionService
|
||||
* @param Collection $books
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
|
||||
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
|
||||
{
|
||||
$entities = clone $books;
|
||||
|
||||
/** @var Book $book */
|
||||
@@ -187,7 +195,9 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
@@ -205,12 +215,17 @@ class PermissionService
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
@@ -236,7 +251,7 @@ class PermissionService
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
}
|
||||
@@ -256,15 +271,16 @@ class PermissionService
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
$roleIds = array_map(function($role) {
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('id', $roleIds)->delete();
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -274,24 +290,26 @@ class PermissionService
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
{
|
||||
if (count($entities) === 0) return;
|
||||
if (count($entities) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->transaction(function() use ($entities) {
|
||||
$this->db->transaction(function () use ($entities) {
|
||||
|
||||
foreach (array_chunk($entities, 1000) as $entityChunk) {
|
||||
$query = $this->db->table('joint_permissions');
|
||||
foreach ($entityChunk as $entity) {
|
||||
$query->orWhere(function(QueryBuilder $query) use ($entity) {
|
||||
$query->orWhere(function (QueryBuilder $query) use ($entity) {
|
||||
$query->where('entity_id', '=', $entity->id)
|
||||
->where('entity_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
$query->delete();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -299,10 +317,11 @@ class PermissionService
|
||||
* Create & Save entity jointPermissions for many entities and jointPermissions.
|
||||
* @param Collection $entities
|
||||
* @param array $roles
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function createManyJointPermissions($entities, $roles)
|
||||
{
|
||||
$this->readyEntityCache();
|
||||
$this->readyEntityCache($entities);
|
||||
$jointPermissions = [];
|
||||
|
||||
// Fetch Entity Permissions and create a mapping of entity restricted statuses
|
||||
@@ -310,7 +329,7 @@ class PermissionService
|
||||
$permissionFetch = $this->entityPermission->newQuery();
|
||||
foreach ($entities as $entity) {
|
||||
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
|
||||
$permissionFetch->orWhere(function($query) use ($entity) {
|
||||
$permissionFetch->orWhere(function ($query) use ($entity) {
|
||||
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
|
||||
});
|
||||
}
|
||||
@@ -327,7 +346,7 @@ class PermissionService
|
||||
// Create a mapping of role permissions
|
||||
$rolePermissionMap = [];
|
||||
foreach ($roles as $role) {
|
||||
foreach ($role->getRelationValue('permissions') as $permission) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
|
||||
}
|
||||
}
|
||||
@@ -341,7 +360,7 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transaction(function() use ($jointPermissions) {
|
||||
$this->db->transaction(function () use ($jointPermissions) {
|
||||
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
|
||||
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
|
||||
}
|
||||
@@ -357,8 +376,12 @@ class PermissionService
|
||||
protected function getActions(Entity $entity)
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
|
||||
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
@@ -407,7 +430,10 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
return $this->createJointPermissionDataArray(
|
||||
$entity,
|
||||
$role,
|
||||
$action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
@@ -421,7 +447,8 @@ class PermissionService
|
||||
* @param $action
|
||||
* @return bool
|
||||
*/
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
|
||||
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
|
||||
{
|
||||
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
|
||||
return isset($entityMap[$key]) ? $entityMap[$key] : false;
|
||||
}
|
||||
@@ -468,7 +495,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)) {
|
||||
@@ -540,11 +567,12 @@ class PermissionService
|
||||
* @param bool $fetchPageContent
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
|
||||
{
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
@@ -557,8 +585,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);
|
||||
});
|
||||
});
|
||||
@@ -606,16 +634,17 @@ class PermissionService
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @param string $action
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$this->currentAction = $action;
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
@@ -710,5 +739,4 @@ class PermissionService
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,15 +64,14 @@ class SearchService
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
$results = collect();
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
@@ -80,19 +79,28 @@ class SearchService
|
||||
$entityTypesToSearch = explode('|', $terms['filters']['type']);
|
||||
}
|
||||
|
||||
$results = collect();
|
||||
$total = 0;
|
||||
$hasMore = false;
|
||||
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
|
||||
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
|
||||
if (!in_array($entityType, $entityTypes)) {
|
||||
continue;
|
||||
}
|
||||
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
|
||||
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
|
||||
if ($entityTotal > $page * $count) {
|
||||
$hasMore = true;
|
||||
}
|
||||
$total += $entityTotal;
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'count' => count($results),
|
||||
'results' => $results->sortByDesc('score')
|
||||
'has_more' => $hasMore,
|
||||
'results' => $results->sortByDesc('score')->values()
|
||||
];
|
||||
}
|
||||
|
||||
@@ -111,7 +119,9 @@ class SearchService
|
||||
|
||||
$results = collect();
|
||||
foreach ($entityTypesToSearch as $entityType) {
|
||||
if (!in_array($entityType, $entityTypes)) continue;
|
||||
if (!in_array($entityType, $entityTypes)) {
|
||||
continue;
|
||||
}
|
||||
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
|
||||
$results = $results->merge($search);
|
||||
}
|
||||
@@ -137,13 +147,16 @@ class SearchService
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param string $action
|
||||
* @param bool $getCount Return the total count of the search
|
||||
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
|
||||
*/
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
|
||||
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
|
||||
{
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType);
|
||||
if ($getCount) return $query->count();
|
||||
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
|
||||
if ($getCount) {
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
$query = $query->skip(($page-1) * $count)->take($count);
|
||||
return $query->get();
|
||||
@@ -153,9 +166,10 @@ class SearchService
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param string $action
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
@@ -164,12 +178,12 @@ class SearchService
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where(function(Builder $query) use ($terms) {
|
||||
$subQuery->where(function (Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
}
|
||||
})->groupBy('entity_type', 'entity_id');
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
|
||||
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
|
||||
$join->on('id', '=', 'entity_id');
|
||||
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
|
||||
$entitySelect->mergeBindings($subQuery);
|
||||
@@ -177,7 +191,7 @@ class SearchService
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
@@ -195,10 +209,12 @@ class SearchService
|
||||
// Handle filters
|
||||
foreach ($terms['filters'] as $filterTerm => $filterValue) {
|
||||
$functionName = camel_case('filter_' . $filterTerm);
|
||||
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
|
||||
if (method_exists($this, $functionName)) {
|
||||
$this->$functionName($entitySelect, $entity, $filterValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
|
||||
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +250,9 @@ class SearchService
|
||||
|
||||
// Parse standard terms
|
||||
foreach (explode(' ', trim($searchString)) as $searchTerm) {
|
||||
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
|
||||
if ($searchTerm !== '') {
|
||||
$terms['search'][] = $searchTerm;
|
||||
}
|
||||
}
|
||||
|
||||
// Split filter values out
|
||||
@@ -267,15 +285,18 @@ class SearchService
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm)
|
||||
{
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
$validOperator = in_array($tagOperator, $this->queryOperators);
|
||||
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
|
||||
if (!empty($tagName)) $query->where('name', '=', $tagName);
|
||||
if (!empty($tagName)) {
|
||||
$query->where('name', '=', $tagName);
|
||||
}
|
||||
if (is_numeric($tagValue) && $tagOperator !== 'like') {
|
||||
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
|
||||
// search the value as a string which prevents being able to do number-based operations
|
||||
@@ -309,8 +330,8 @@ class SearchService
|
||||
public function indexEntity(Entity $entity)
|
||||
{
|
||||
$this->deleteEntityTerms($entity);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||
$terms = array_merge($nameTerms, $bodyTerms);
|
||||
foreach ($terms as $index => $term) {
|
||||
$terms[$index]['entity_type'] = $entity->getMorphClass();
|
||||
@@ -323,11 +344,12 @@ class SearchService
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities) {
|
||||
protected function indexEntities($entities)
|
||||
{
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
@@ -382,11 +404,15 @@ class SearchService
|
||||
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
|
||||
{
|
||||
$tokenMap = []; // {TextToken => OccurrenceCount}
|
||||
$splitText = explode(' ', $text);
|
||||
foreach ($splitText as $token) {
|
||||
if ($token === '') continue;
|
||||
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
|
||||
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
|
||||
$token = strtok($text, $splitChars);
|
||||
|
||||
while ($token !== false) {
|
||||
if (!isset($tokenMap[$token])) {
|
||||
$tokenMap[$token] = 0;
|
||||
}
|
||||
$tokenMap[$token]++;
|
||||
$token = strtok($splitChars);
|
||||
}
|
||||
|
||||
$terms = [];
|
||||
@@ -408,43 +434,63 @@ class SearchService
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
try { $date = date_create($input);
|
||||
} catch (\Exception $e) {return;}
|
||||
try {
|
||||
$date = date_create($input);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') return;
|
||||
if ($input === 'me') $input = user()->id;
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
}
|
||||
if ($input === 'me') {
|
||||
$input = user()->id;
|
||||
}
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
@@ -453,7 +499,10 @@ class SearchService
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$this->filterInName($query, $model, $input);
|
||||
}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
@@ -467,16 +516,37 @@ class SearchService
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function($query) {
|
||||
$query->whereHas('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function($query) {
|
||||
$query->whereDoesntHave('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
{
|
||||
$functionName = camel_case('sort_by_' . $input);
|
||||
if (method_exists($this, $functionName)) {
|
||||
$this->$functionName($query, $model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorting filter options
|
||||
*/
|
||||
|
||||
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
|
||||
{
|
||||
$commentsTable = $this->db->getTablePrefix() . 'comments';
|
||||
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
|
||||
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
|
||||
|
||||
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -49,11 +51,13 @@ class ViewService
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false|array $filterModel
|
||||
* @param string $action - used for permission checking
|
||||
* @return
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
@@ -62,7 +66,7 @@ class ViewService
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
};
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
}
|
||||
@@ -77,12 +81,16 @@ class ViewService
|
||||
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) return collect();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) $query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModel) {
|
||||
$query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
}
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
@@ -97,5 +105,4 @@ class ViewService
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +63,12 @@
|
||||
"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",
|
||||
@@ -71,6 +76,10 @@
|
||||
]
|
||||
},
|
||||
"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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user