mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 16:49:47 +03:00
Compare commits
976 Commits
v0.18.0
...
captcha_ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49454da28 | ||
|
|
23db81f2cc | ||
|
|
a35c1bfb13 | ||
|
|
0b75b33044 | ||
|
|
b8617eb3d1 | ||
|
|
1bf3666d6b | ||
|
|
caaed849d5 | ||
|
|
e98986fc2d | ||
|
|
c764208b25 | ||
|
|
8fbe677975 | ||
|
|
d6e27af14a | ||
|
|
0b55e52e4e | ||
|
|
0345ece890 | ||
|
|
739efdb0e2 | ||
|
|
ac5784d0d0 | ||
|
|
e1aa212db8 | ||
|
|
6758a42f80 | ||
|
|
10199d684e | ||
|
|
21e5685816 | ||
|
|
746ad01ebb | ||
|
|
26b42b6414 | ||
|
|
22a665a942 | ||
|
|
7a5fc800b5 | ||
|
|
3148b316cf | ||
|
|
1c2f43b3a6 | ||
|
|
1bb1b568af | ||
|
|
16d8a667b1 | ||
|
|
a1f89ad589 | ||
|
|
7a4425473b | ||
|
|
f421a2e1d6 | ||
|
|
79f1e87cd0 | ||
|
|
e9d42a2e8c | ||
|
|
712ea21efe | ||
|
|
36195c94df | ||
|
|
21f09a5920 | ||
|
|
aea5319256 | ||
|
|
444a23a419 | ||
|
|
fde3867313 | ||
|
|
5979f6667b | ||
|
|
64abe10dc4 | ||
|
|
7cc17934a8 | ||
|
|
2dfe6c2d56 | ||
|
|
9fbef8cd1b | ||
|
|
cf5d51e7b8 | ||
|
|
ae93a6ed07 | ||
|
|
b792108bc1 | ||
|
|
4bf77f67dd | ||
|
|
88e076a12a | ||
|
|
b27a5c7fb8 | ||
|
|
1b33a0c5b9 | ||
|
|
78f8a51664 | ||
|
|
666213a4d4 | ||
|
|
3acea12f1c | ||
|
|
eab0ca9648 | ||
|
|
42d8548960 | ||
|
|
e5155a5dcb | ||
|
|
44330bdd24 | ||
|
|
c6b1f36412 | ||
|
|
b608bb8859 | ||
|
|
9357620d55 | ||
|
|
4de432b50d | ||
|
|
20c36d58a6 | ||
|
|
5fdab3b8af | ||
|
|
de3e9ab094 | ||
|
|
421dd93ffd | ||
|
|
f417675b1d | ||
|
|
2955f414dd | ||
|
|
4de719b325 | ||
|
|
518d9df961 | ||
|
|
2ebbc6b658 | ||
|
|
0b1ece664d | ||
|
|
83ef086470 | ||
|
|
1b8a164412 | ||
|
|
84a387cf5b | ||
|
|
71ebb9df8b | ||
|
|
0ac50c0e50 | ||
|
|
4b0c4e621a | ||
|
|
fbf8378ae5 | ||
|
|
d63157175b | ||
|
|
30da105812 | ||
|
|
1e7df28238 | ||
|
|
629b7a674e | ||
|
|
ce1c70084e | ||
|
|
94f610d040 | ||
|
|
8fcb0e6820 | ||
|
|
c732970f6e | ||
|
|
94441832c5 | ||
|
|
71167426bb | ||
|
|
f7f7cd464c | ||
|
|
15c39c1976 | ||
|
|
6fa093d9d0 | ||
|
|
97fdfa6ebe | ||
|
|
5c43a5a59a | ||
|
|
e7508689de | ||
|
|
5c70413784 | ||
|
|
52b4c81aff | ||
|
|
9a080da29f | ||
|
|
762d1d7595 | ||
|
|
6504a6f599 | ||
|
|
bf1371d04c | ||
|
|
f08668706f | ||
|
|
7b506447c7 | ||
|
|
fbb2b7ac6a | ||
|
|
56e31a5df7 | ||
|
|
86f56dd22b | ||
|
|
282c45f088 | ||
|
|
214c09c2b2 | ||
|
|
dda0200a94 | ||
|
|
53ba5b7e33 | ||
|
|
b532ed0f86 | ||
|
|
fdd34a74ed | ||
|
|
239b3c8c2b | ||
|
|
7634a84334 | ||
|
|
2b929f5d95 | ||
|
|
ff841cff2e | ||
|
|
9e397a57a9 | ||
|
|
d87eb277dd | ||
|
|
2eba8c611e | ||
|
|
f12a7540c9 | ||
|
|
fe64248e86 | ||
|
|
a9f983f156 | ||
|
|
a602cdf401 | ||
|
|
5aa741cb60 | ||
|
|
3ad1b42a74 | ||
|
|
2b7362fa94 | ||
|
|
35f35bcba5 | ||
|
|
13c0386e84 | ||
|
|
78f5f44460 | ||
|
|
35e6635379 | ||
|
|
e42d90a5b6 | ||
|
|
8ae35f645a | ||
|
|
5470a9e035 | ||
|
|
dbfe63ccf6 | ||
|
|
114f10d5ca | ||
|
|
5226ddd959 | ||
|
|
60013f776a | ||
|
|
ac0a070fc8 | ||
|
|
b05659d7a3 | ||
|
|
3af4648dc3 | ||
|
|
e1e1ea6099 | ||
|
|
0c3dc50cd9 | ||
|
|
0a0ceb382e | ||
|
|
896f88174a | ||
|
|
0ee9e5c4db | ||
|
|
112f73c91c | ||
|
|
b47dc046e0 | ||
|
|
215d84d705 | ||
|
|
c459d86b58 | ||
|
|
adf0d5fce2 | ||
|
|
cb355c8aad | ||
|
|
d0e351b942 | ||
|
|
e3d570e928 | ||
|
|
e00c170d85 | ||
|
|
e430dad38c | ||
|
|
d62d2384cb | ||
|
|
a981dc41cb | ||
|
|
97ffbaa740 | ||
|
|
8051558d3a | ||
|
|
7ef059e254 | ||
|
|
4329fee2c9 | ||
|
|
b1cf5ab309 | ||
|
|
b67d9f4036 | ||
|
|
224d9e7a7d | ||
|
|
31419c3913 | ||
|
|
ba36d36597 | ||
|
|
ac7d6b8737 | ||
|
|
e52dab825a | ||
|
|
33f51d5b78 | ||
|
|
ae7529376b | ||
|
|
aefd4d423c | ||
|
|
bad44391d4 | ||
|
|
1880633be3 | ||
|
|
8273f8b16e | ||
|
|
0600f752eb | ||
|
|
342dc8948c | ||
|
|
3e24e44106 | ||
|
|
404d11d0eb | ||
|
|
07b889547d | ||
|
|
e47a7e0b97 | ||
|
|
989309fdff | ||
|
|
6797c91eeb | ||
|
|
b9ad3f9f65 | ||
|
|
7a8678e5f7 | ||
|
|
ba09dad1fe | ||
|
|
5910e00fb8 | ||
|
|
3f83c548f8 | ||
|
|
adc866cb3d | ||
|
|
ad542f0407 | ||
|
|
15786e2630 | ||
|
|
8c190324ac | ||
|
|
79f6dc00a3 | ||
|
|
cb832a2c10 | ||
|
|
a87ae16010 | ||
|
|
aeb1fc4d49 | ||
|
|
6428f32483 | ||
|
|
884e20cc5e | ||
|
|
fc761784a1 | ||
|
|
e0c229114f | ||
|
|
4e49d06182 | ||
|
|
2bb06463d5 | ||
|
|
4d6df37963 | ||
|
|
553d3ce861 | ||
|
|
0bc5ccba32 | ||
|
|
ed330f246c | ||
|
|
6c66a8935a | ||
|
|
c653618eab | ||
|
|
efc034bd8d | ||
|
|
c24764018a | ||
|
|
c8cf6731e2 | ||
|
|
d0db0f8e26 | ||
|
|
c380c10d54 | ||
|
|
95d4149d5e | ||
|
|
7f3f6e65b9 | ||
|
|
29f17fd154 | ||
|
|
84419005e7 | ||
|
|
d3cd369247 | ||
|
|
50a9c71de0 | ||
|
|
faa3a8b842 | ||
|
|
c836862d89 | ||
|
|
03073dd9e4 | ||
|
|
ee58bea8b7 | ||
|
|
9406b4d4c9 | ||
|
|
01be72d5e2 | ||
|
|
21e1123d12 | ||
|
|
8d358e4894 | ||
|
|
f797d2da20 | ||
|
|
d3cc261320 | ||
|
|
cc24d478aa | ||
|
|
07adfb2ff1 | ||
|
|
36481bb73f | ||
|
|
4d5e47a2d2 | ||
|
|
d66fab8bee | ||
|
|
2694bb8fab | ||
|
|
b12ae6d11b | ||
|
|
221a483b40 | ||
|
|
4a127c29a5 | ||
|
|
8c21b5345d | ||
|
|
0a06e2bce3 | ||
|
|
d9cde4123d | ||
|
|
7cda9b026e | ||
|
|
67ed4710b6 | ||
|
|
745a0bb98d | ||
|
|
aedff7dc6d | ||
|
|
17969c0bbf | ||
|
|
666ced9c3b | ||
|
|
37bf7f11e4 | ||
|
|
bda8aa414b | ||
|
|
60d175a9b9 | ||
|
|
42e908c7f0 | ||
|
|
53a26a365c | ||
|
|
4ee0fde0ac | ||
|
|
9879a0d12c | ||
|
|
5d2e80bff3 | ||
|
|
83818234c8 | ||
|
|
5c00187138 | ||
|
|
193e2ffebe | ||
|
|
b2a5d07787 | ||
|
|
15cf9f8b32 | ||
|
|
f9adfee47a | ||
|
|
2e988277f0 | ||
|
|
4e8e28c35f | ||
|
|
4cbfb2bf13 | ||
|
|
f5fe524e6c | ||
|
|
37b91b6b0e | ||
|
|
b3a4d8af2a | ||
|
|
8b7bee7c67 | ||
|
|
837d2bc582 | ||
|
|
64cd499542 | ||
|
|
5f2d226f09 | ||
|
|
00703fa817 | ||
|
|
44c537de1a | ||
|
|
6bccf0e64a | ||
|
|
042a6f9760 | ||
|
|
04287745e4 | ||
|
|
5c9b528517 | ||
|
|
6be2d3f28c | ||
|
|
6d20bdc1fb | ||
|
|
502ea608bf | ||
|
|
55b07c7076 | ||
|
|
33e999909f | ||
|
|
6be95cd2ac | ||
|
|
f467185e86 | ||
|
|
646fd822c5 | ||
|
|
d96baf2d4a | ||
|
|
1c312906bc | ||
|
|
9126c87f2b | ||
|
|
579d98a908 | ||
|
|
98a4359198 | ||
|
|
6f710225b5 | ||
|
|
b273b9d6d0 | ||
|
|
e471d0c52a | ||
|
|
0a431e3223 | ||
|
|
035a0d8efb | ||
|
|
e70423c73f | ||
|
|
8445304fe9 | ||
|
|
f1e571a57c | ||
|
|
e9be2b7174 | ||
|
|
352cbbd074 | ||
|
|
b00b319e83 | ||
|
|
a112c11df8 | ||
|
|
058cc2cbd6 | ||
|
|
edd98c00e5 | ||
|
|
19bb11a1c9 | ||
|
|
9511d10ec8 | ||
|
|
3286f29a61 | ||
|
|
77d3bd31a6 | ||
|
|
56004abdf4 | ||
|
|
df6f6e2d77 | ||
|
|
ba1b3fc181 | ||
|
|
67d4fa0c65 | ||
|
|
49deab3a02 | ||
|
|
5325870271 | ||
|
|
138f5d5c4f | ||
|
|
880d4f35da | ||
|
|
20988962fe | ||
|
|
32603362a6 | ||
|
|
9dba9ca178 | ||
|
|
8a4a81629f | ||
|
|
5ef0992d5b | ||
|
|
12be7d0086 | ||
|
|
ba0af9214e | ||
|
|
36424a24b5 | ||
|
|
a70ee9664a | ||
|
|
156c0a88e9 | ||
|
|
0efed43389 | ||
|
|
163a57cf70 | ||
|
|
a3ccde8698 | ||
|
|
9700b7ccea | ||
|
|
54c428c375 | ||
|
|
ebe5d643f3 | ||
|
|
e66ddbc17b | ||
|
|
0e0a17cc30 | ||
|
|
ffceb4092e | ||
|
|
50e5527483 | ||
|
|
f63fd4beca | ||
|
|
d682a0157f | ||
|
|
70ad707c3c | ||
|
|
3062bf1876 | ||
|
|
a2087fe3ff | ||
|
|
19770d2792 | ||
|
|
99c6d70c51 | ||
|
|
0830521e60 | ||
|
|
2317bf2350 | ||
|
|
2c48f4f7e8 | ||
|
|
456afdcd4c | ||
|
|
68017e2553 | ||
|
|
866187830a | ||
|
|
b56fc21aaf | ||
|
|
d673bf61c2 | ||
|
|
18b10153e5 | ||
|
|
7c8edf5673 | ||
|
|
f4ea5f1f55 | ||
|
|
f62843c861 | ||
|
|
5fe630b8d2 | ||
|
|
d910defbfd | ||
|
|
153adb055c | ||
|
|
26ec1cc3dc | ||
|
|
d476e30df0 | ||
|
|
37ab97af8c | ||
|
|
7fcd7a5d91 | ||
|
|
c67f76f776 | ||
|
|
9a444b4a04 | ||
|
|
106f32591d | ||
|
|
7f6929d716 | ||
|
|
651ae2f3be | ||
|
|
101a7b40b9 | ||
|
|
1930ed4d6a | ||
|
|
2753629dbe | ||
|
|
86a00a59d4 | ||
|
|
d6dd96e7fc | ||
|
|
1b1ddb6794 | ||
|
|
f65ff3a9a8 | ||
|
|
323bff7d6d | ||
|
|
0e3d507ec2 | ||
|
|
f943f0d401 | ||
|
|
1b01d65965 | ||
|
|
a2acd063f3 | ||
|
|
e9e3e8b6b1 | ||
|
|
7f95b51b00 | ||
|
|
ff0b9004bc | ||
|
|
8fd8652bbf | ||
|
|
e1474194db | ||
|
|
4c574c22a8 | ||
|
|
0b976d9f91 | ||
|
|
2a882a43ff | ||
|
|
aabd4c0412 | ||
|
|
d39fc84301 | ||
|
|
75ca430fd4 | ||
|
|
4cf43f67d6 | ||
|
|
86899864dd | ||
|
|
3c796b1ae7 | ||
|
|
b7915cc7b0 | ||
|
|
eac82c47a5 | ||
|
|
d3d3e2ad3e | ||
|
|
54b36cd305 | ||
|
|
f8396d3632 | ||
|
|
4df11701e7 | ||
|
|
4a872012c5 | ||
|
|
302b53562d | ||
|
|
ebd4c3327d | ||
|
|
d0c166c207 | ||
|
|
321b53c827 | ||
|
|
5e6c039b08 | ||
|
|
178b5af83a | ||
|
|
4be0c567cc | ||
|
|
0724ae3640 | ||
|
|
62a475e464 | ||
|
|
cfc1a2f045 | ||
|
|
b012f27ae3 | ||
|
|
58bec7287f | ||
|
|
9341ae4910 | ||
|
|
1328755f95 | ||
|
|
038b2418f7 | ||
|
|
e3230f8f21 | ||
|
|
fd37d95ffc | ||
|
|
3abde8bfe2 | ||
|
|
2ca8038df2 | ||
|
|
89de328439 | ||
|
|
c37e73b626 | ||
|
|
0283ab11b5 | ||
|
|
5b36ddb12f | ||
|
|
ffc1aa873e | ||
|
|
19b7093438 | ||
|
|
7799ba5c79 | ||
|
|
c356612612 | ||
|
|
1c89fcd20a | ||
|
|
730cb78b45 | ||
|
|
8e7f703af7 | ||
|
|
6c14c09880 | ||
|
|
773ab9d7ff | ||
|
|
0e395b1e21 | ||
|
|
6c7d87c836 | ||
|
|
89be30ff0e | ||
|
|
e8ab4fd91f | ||
|
|
5d1162fb64 | ||
|
|
216358c6e4 | ||
|
|
57d99130ee | ||
|
|
79afec9737 | ||
|
|
90929baa52 | ||
|
|
85f330c79a | ||
|
|
77d7f764f1 | ||
|
|
a76599bd2a | ||
|
|
4afc67a962 | ||
|
|
042d8b3274 | ||
|
|
1c0a196b9d | ||
|
|
a1fda37896 | ||
|
|
43758a7d60 | ||
|
|
c7d3db9751 | ||
|
|
fca7689e1a | ||
|
|
18bac4e673 | ||
|
|
ca2a9fbf1c | ||
|
|
cbebe7c8de | ||
|
|
0943221902 | ||
|
|
17ed1b7faf | ||
|
|
36d18f28ee | ||
|
|
495d18814a | ||
|
|
257a5a23ec | ||
|
|
919660678b | ||
|
|
19751ed1cb | ||
|
|
818c02ed44 | ||
|
|
9abdab3991 | ||
|
|
43122f86e8 | ||
|
|
935337862c | ||
|
|
dd671524f8 | ||
|
|
1cac3d43d3 | ||
|
|
9243c635f2 | ||
|
|
93f820d9da | ||
|
|
b62afcad1f | ||
|
|
7b32aa163f | ||
|
|
eebfd8904e | ||
|
|
0d84a0b976 | ||
|
|
88ac636c00 | ||
|
|
9dc26a8c52 | ||
|
|
6543020fd2 | ||
|
|
be4f3d62cd | ||
|
|
da58c41ab6 | ||
|
|
10d08e641c | ||
|
|
c4e4cebf14 | ||
|
|
3f58800ed1 | ||
|
|
0931ff38e9 | ||
|
|
07bc0612c0 | ||
|
|
6dec485b45 | ||
|
|
a441faf65c | ||
|
|
50ee1462ad | ||
|
|
1cb6ae39c8 | ||
|
|
c667c6e235 | ||
|
|
e3e484e561 | ||
|
|
73fa18a128 | ||
|
|
5c2e3f4e56 | ||
|
|
c47b578599 | ||
|
|
e60d11ee04 | ||
|
|
7ad8314bd7 | ||
|
|
131fcae4c7 | ||
|
|
c8d893fac7 | ||
|
|
b59e5942c8 | ||
|
|
8ff969dd17 | ||
|
|
6eead437d8 | ||
|
|
0b6f83837b | ||
|
|
81eb642f75 | ||
|
|
47b08888ba | ||
|
|
32e34f10ff | ||
|
|
f455b317ec | ||
|
|
08b967607f | ||
|
|
90883bb22b | ||
|
|
0c8b6b7324 | ||
|
|
81d3bdc168 | ||
|
|
54ca4487fa | ||
|
|
25da4d9a8b | ||
|
|
f0add69b61 | ||
|
|
714c7bbd3a | ||
|
|
e0b479efef | ||
|
|
9d09c4c7b0 | ||
|
|
b89411c108 | ||
|
|
c472b82ee3 | ||
|
|
d2f5313f92 | ||
|
|
572e75b783 | ||
|
|
d2a9b312e9 | ||
|
|
b224a2c8a0 | ||
|
|
fe6dfcedf9 | ||
|
|
01260d95f3 | ||
|
|
d69ba6b47a | ||
|
|
098128aafb | ||
|
|
92c9837157 | ||
|
|
18e5f86ffa | ||
|
|
c860645a5a | ||
|
|
fcb93dc7c8 | ||
|
|
fcdb39e428 | ||
|
|
1b3e1863f4 | ||
|
|
fbc2175789 | ||
|
|
8099c431bb | ||
|
|
efbfe0f7af | ||
|
|
66402b474c | ||
|
|
c3986cedfc | ||
|
|
b5a2d3c1c4 | ||
|
|
a6862362c1 | ||
|
|
7d4ec0d633 | ||
|
|
85544c04a9 | ||
|
|
0a8ff6ffad | ||
|
|
04274078c4 | ||
|
|
aac9fbf236 | ||
|
|
08e290f3ea | ||
|
|
86602854ac | ||
|
|
f47f0e05d6 | ||
|
|
c83a51f7e2 | ||
|
|
b922c8029e | ||
|
|
653761e67d | ||
|
|
d59ff132ab | ||
|
|
e6e740b2a1 | ||
|
|
af6f4e6c8c | ||
|
|
69a0f8d502 | ||
|
|
6d35fb5237 | ||
|
|
6eb63a1e03 | ||
|
|
8774f1a320 | ||
|
|
6ca8ccd330 | ||
|
|
df88ffa159 | ||
|
|
dcbb8ad960 | ||
|
|
0f2ffa9545 | ||
|
|
6bae16f7e9 | ||
|
|
f5ca7ab1c8 | ||
|
|
7dd11decb8 | ||
|
|
79d0f707e6 | ||
|
|
369dc02e78 | ||
|
|
9d2e65b73d | ||
|
|
f421d83627 | ||
|
|
be2ca9d4bb | ||
|
|
17bca662a7 | ||
|
|
1776204870 | ||
|
|
985e214d94 | ||
|
|
2bcc159fd6 | ||
|
|
fb7c12438d | ||
|
|
b2cd363539 | ||
|
|
f668bee88b | ||
|
|
642f2760cc | ||
|
|
37aa8b05f8 | ||
|
|
d640cc1eee | ||
|
|
c2d6e98985 | ||
|
|
84b4fe6176 | ||
|
|
decdf5714b | ||
|
|
9da600caf9 | ||
|
|
45aee2a1c1 | ||
|
|
f5df5ac7d5 | ||
|
|
fb29f4119d | ||
|
|
93795b6eda | ||
|
|
f7b808a9e6 | ||
|
|
4948b443b6 | ||
|
|
448068e318 | ||
|
|
7d81a95156 | ||
|
|
a9bf2ed398 | ||
|
|
771f781e7f | ||
|
|
78be8535f7 | ||
|
|
6c4c1ccb58 | ||
|
|
562225a77b | ||
|
|
b936e1f403 | ||
|
|
b3cc3130f0 | ||
|
|
0363fc4ea1 | ||
|
|
134a96fa32 | ||
|
|
56f444a8a7 | ||
|
|
86f43c8a65 | ||
|
|
d886c6a32e | ||
|
|
f399e60910 | ||
|
|
173eaf1c98 | ||
|
|
64eabaf882 | ||
|
|
6b84a76af1 | ||
|
|
2bd6ba9895 | ||
|
|
1df0bcaf85 | ||
|
|
c31e6a03ce | ||
|
|
61c9324229 | ||
|
|
8c4c8cd95b | ||
|
|
0c9c1e4c6b | ||
|
|
9ec114641c | ||
|
|
295c7918a4 | ||
|
|
3ac34b5849 | ||
|
|
6e7adcc095 | ||
|
|
a1ecdcacba | ||
|
|
019b8196ad | ||
|
|
63f96c1c6f | ||
|
|
8df9dab80a | ||
|
|
93147f4340 | ||
|
|
77727e7e50 | ||
|
|
9f4c64a676 | ||
|
|
e0ebae19aa | ||
|
|
6cdb943916 | ||
|
|
d3d8ddbe52 | ||
|
|
57c312ec3f | ||
|
|
13ad0031d6 | ||
|
|
d5b922aa50 | ||
|
|
28823c4fae | ||
|
|
b6bb078e0a | ||
|
|
8254c3be8d | ||
|
|
47cb99a2d6 | ||
|
|
86b2ddbd28 | ||
|
|
2e4863edb1 | ||
|
|
b0d027a4a9 | ||
|
|
0c3c8fc9c3 | ||
|
|
624c568008 | ||
|
|
58a0a59d7e | ||
|
|
3d0d7f8be2 | ||
|
|
eb5069ca66 | ||
|
|
0306253c45 | ||
|
|
71b6f09128 | ||
|
|
67e0c3d2a5 | ||
|
|
6aeb1387aa | ||
|
|
fa83e6bda4 | ||
|
|
ae89e05a25 | ||
|
|
a50153d221 | ||
|
|
cdb1c7ef88 | ||
|
|
0f7b0ad45a | ||
|
|
6c5304a3de | ||
|
|
d34b91f2c9 | ||
|
|
dfadaa28f6 | ||
|
|
fae564ff32 | ||
|
|
502b22a0f2 | ||
|
|
f9feeef5c9 | ||
|
|
a6674a5a5e | ||
|
|
fb18576259 | ||
|
|
7238a01f89 | ||
|
|
93f92e9e16 | ||
|
|
d92efd4edc | ||
|
|
448f7d091b | ||
|
|
11470b85f9 | ||
|
|
9c07619099 | ||
|
|
60a224f7a1 | ||
|
|
e392e1fd8b | ||
|
|
64d5763d08 | ||
|
|
007059273e | ||
|
|
106432ee4e | ||
|
|
0ade9b5b9b | ||
|
|
736d7118b0 | ||
|
|
b612cf9e4c | ||
|
|
1a72208d27 | ||
|
|
7f437c2e3c | ||
|
|
cfdf5b93d9 | ||
|
|
3cd08382e9 | ||
|
|
58a6b2df7d | ||
|
|
582158f70e | ||
|
|
03ee3d21ba | ||
|
|
b99229a5c3 | ||
|
|
2fc513984d | ||
|
|
34499658d5 | ||
|
|
a8f18c0102 | ||
|
|
c85cace48b | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
65579214e2 | ||
|
|
d89440d198 | ||
|
|
08e58bab79 | ||
|
|
d29b177c84 | ||
|
|
151d72e42c | ||
|
|
711ba258f1 | ||
|
|
df4d4f30f1 | ||
|
|
f094837709 | ||
|
|
e27cbb9dce | ||
|
|
bdba25b6f2 | ||
|
|
6b2581de63 | ||
|
|
1031c61d0c | ||
|
|
46fc0e5026 | ||
|
|
332f678ed0 | ||
|
|
0d5d77d8ab | ||
|
|
db51cee2d8 | ||
|
|
a988438946 | ||
|
|
3bf7cac030 | ||
|
|
79c3a07e9a | ||
|
|
9758872baf | ||
|
|
b711bc6816 | ||
|
|
247e6dba85 | ||
|
|
2b3d6e4e4a | ||
|
|
6b1980c4f3 | ||
|
|
9ba29770e1 | ||
|
|
3d375fae55 | ||
|
|
c99a50de2c | ||
|
|
1a32b25b5e | ||
|
|
481aa5b5b0 | ||
|
|
c943eb4d0d | ||
|
|
aca6de49b0 | ||
|
|
5fd04fa470 | ||
|
|
87339e4cd0 | ||
|
|
a9eb058dad | ||
|
|
61fad6a665 | ||
|
|
fa4bee2d98 | ||
|
|
ce63260fa6 | ||
|
|
a3557d5bb2 | ||
|
|
9ca22976c3 | ||
|
|
9e2934fe17 | ||
|
|
2259263214 | ||
|
|
762cf5f183 | ||
|
|
07175f2b3e | ||
|
|
0c4ddf16a5 | ||
|
|
74a5e3113e | ||
|
|
df0a982433 | ||
|
|
212f924ffa | ||
|
|
09936566dd | ||
|
|
9469c04eab | ||
|
|
941bb73a68 | ||
|
|
8e652f5d8f | ||
|
|
eec1b21928 | ||
|
|
1baeb7bec9 | ||
|
|
61475ca0a3 | ||
|
|
244c5a3ebb | ||
|
|
39e7ac1c15 | ||
|
|
ab7f5def04 | ||
|
|
5034f21394 | ||
|
|
e02fcbe983 | ||
|
|
1c88d21abf | ||
|
|
c1a1bc0135 | ||
|
|
6200948eec | ||
|
|
7f902e41c7 | ||
|
|
3079a9f4de | ||
|
|
a7d2cfdee2 | ||
|
|
a149e87ca7 | ||
|
|
854fd52a27 | ||
|
|
3d808ac75f | ||
|
|
39b924f158 | ||
|
|
a488ef6b00 | ||
|
|
6d66c38c12 | ||
|
|
922964ecf2 | ||
|
|
0c70416b5c | ||
|
|
770f30c3a8 | ||
|
|
b4044e6c3a | ||
|
|
9872767f20 | ||
|
|
dd4d2f4696 | ||
|
|
e5dc0e6bb8 | ||
|
|
85fbe820c4 | ||
|
|
832f8eaa94 | ||
|
|
3435dcc91e | ||
|
|
1ed74b8598 | ||
|
|
fd36978c13 | ||
|
|
1278a0b818 | ||
|
|
6a6516ddd5 | ||
|
|
1fe8f13503 | ||
|
|
8f3adcda5d | ||
|
|
21a8df78ee | ||
|
|
7f8351e044 | ||
|
|
afc1ecafe9 | ||
|
|
ab6ff5fda2 | ||
|
|
b0ba1a43a9 | ||
|
|
e919cab3d1 | ||
|
|
f37509062e | ||
|
|
24ee78ccd8 | ||
|
|
d37b398e79 | ||
|
|
7a724f9134 | ||
|
|
f3b2e0fb91 | ||
|
|
844976c85b | ||
|
|
f0d914abbf | ||
|
|
0ed3023b42 | ||
|
|
3fd61a3600 | ||
|
|
a663fc8aa8 | ||
|
|
d84315fff8 | ||
|
|
144a6e469d | ||
|
|
c5f11e4516 | ||
|
|
16a09e8ff6 | ||
|
|
f51db4b9f6 | ||
|
|
6ad24a6bee | ||
|
|
5b736c3b36 | ||
|
|
cc553cc93d | ||
|
|
e88a06291e | ||
|
|
6eccb3d5b9 | ||
|
|
026de8c5ca | ||
|
|
e10d4b91cf | ||
|
|
d089eaf754 | ||
|
|
bb2d85965f | ||
|
|
d99fd1fd65 | ||
|
|
947c58f227 | ||
|
|
bce5fdd5cd | ||
|
|
fdf139edb2 | ||
|
|
af72f0d490 | ||
|
|
8924618d12 | ||
|
|
6557fbb666 |
70
.env.example
70
.env.example
@@ -1,11 +1,14 @@
|
||||
# Environment
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
# Application key
|
||||
# Used for encryption where needed.
|
||||
# Run `php artisan key:generate` to generate a valid key.
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# The below url has to be set if using social auth options
|
||||
# or if you are not using BookStack at the root path of your domain.
|
||||
# APP_URL=http://bookstack.dev
|
||||
# Application URL
|
||||
# Remove the hash below and set a URL if using BookStack behind
|
||||
# a proxy, if using a third-party authentication option.
|
||||
# This must be the root URL that you want to host BookStack on.
|
||||
# All URL's in BookStack will be generated using this value.
|
||||
#APP_URL=https://example.com
|
||||
|
||||
# Database details
|
||||
DB_HOST=localhost
|
||||
@@ -13,55 +16,16 @@ DB_DATABASE=database_database
|
||||
DB_USERNAME=database_username
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Cache and session
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
# If using Memcached, comment the above and uncomment these
|
||||
#CACHE_DRIVER=memcached
|
||||
#SESSION_DRIVER=memcached
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# Memcached settings
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
# This follows the following format: HOST:PORT:WEIGHT
|
||||
# For multiple servers separate with a comma
|
||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
||||
|
||||
# Storage
|
||||
STORAGE_TYPE=local
|
||||
# Amazon S3 Config
|
||||
STORAGE_S3_KEY=false
|
||||
STORAGE_S3_SECRET=false
|
||||
STORAGE_S3_REGION=false
|
||||
STORAGE_S3_BUCKET=false
|
||||
# Storage URL
|
||||
# Used to prefix image urls for when using custom domains/cdns
|
||||
STORAGE_URL=false
|
||||
|
||||
# General auth
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Social Authentication information. Defaults as off.
|
||||
GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
|
||||
# External services such as Gravatar
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# LDAP Settings
|
||||
LDAP_SERVER=false
|
||||
LDAP_BASE_DN=false
|
||||
LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
|
||||
# Mail settings
|
||||
# Mail system to use
|
||||
# Can be 'smtp', 'mail' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# SMTP mail options
|
||||
MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
|
||||
# A full list of options can be found in the '.env.example.complete' file.
|
||||
237
.env.example.complete
Normal file
237
.env.example.complete
Normal file
@@ -0,0 +1,237 @@
|
||||
# Full list of environment variables that can be used with BookStack.
|
||||
# Selectively copy these to your '.env' file as required.
|
||||
# Each option is shown with it's default value.
|
||||
# Do not copy this whole file to use as your '.env' file.
|
||||
|
||||
# Application environment
|
||||
# Can be 'production', 'development', 'testing' or 'demo'
|
||||
APP_ENV=production
|
||||
|
||||
# Enable debug mode
|
||||
# Shows advanced debug information and errors.
|
||||
# CAN EXPOSE OTHER VARIABLES, LEAVE DISABLED
|
||||
APP_DEBUG=false
|
||||
|
||||
# Application key
|
||||
# Used for encryption where needed.
|
||||
# Run `php artisan key:generate` to generate a valid key.
|
||||
APP_KEY=SomeRandomString
|
||||
|
||||
# Application URL
|
||||
# This must be the root URL that you want to host BookStack on.
|
||||
# All URL's in BookStack will be generated using this value.
|
||||
APP_URL=https://example.com
|
||||
|
||||
# Application default language
|
||||
# The default language choice to show.
|
||||
# May be overridden by user-preference or visitor browser settings.
|
||||
APP_LANG=en
|
||||
|
||||
# Auto-detect language for public visitors.
|
||||
# Uses browser-sent headers to infer a language.
|
||||
# APP_LANG will be used if such a header is not provided.
|
||||
APP_AUTO_LANG_PUBLIC=true
|
||||
|
||||
# Application timezone
|
||||
# Used where dates are displayed such as on exported content.
|
||||
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
|
||||
APP_TIMEZONE=UTC
|
||||
|
||||
# Database details
|
||||
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=database_database
|
||||
DB_USERNAME=database_username
|
||||
DB_PASSWORD=database_user_password
|
||||
|
||||
# Mail system to use
|
||||
# Can be 'smtp', 'mail' or 'sendmail'
|
||||
MAIL_DRIVER=smtp
|
||||
|
||||
# Mail sending options
|
||||
MAIL_FROM=mail@bookstackapp.com
|
||||
MAIL_FROM_NAME=BookStack
|
||||
|
||||
# SMTP mail options
|
||||
MAIL_HOST=localhost
|
||||
MAIL_PORT=1025
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
# Cache & Session driver to use
|
||||
# Can be 'file', 'database', 'memcached' or 'redis'
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
|
||||
# Session configuration
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_COOKIE_NAME=bookstack_session
|
||||
SESSION_SECURE_COOKIE=false
|
||||
|
||||
# Cache key prefix
|
||||
# Can be used to prevent conflicts multiple BookStack instances use the same store.
|
||||
CACHE_PREFIX=bookstack
|
||||
|
||||
# Memcached server configuration
|
||||
# If using a UNIX socket path for the host, set the port to 0
|
||||
# This follows the following format: HOST:PORT:WEIGHT
|
||||
# For multiple servers separate with a comma
|
||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
||||
|
||||
# Redis server configuration
|
||||
# This follows the following format: HOST:PORT:DATABASE
|
||||
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
|
||||
# For multiple servers separate with a comma. These will be clustered.
|
||||
REDIS_SERVERS=127.0.0.1:6379:0
|
||||
|
||||
# Queue driver to use
|
||||
# Queue not really currently used but may be configurable in the future.
|
||||
# Would advise not to change this for now.
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# Storage system to use
|
||||
# Can be 'local', 'local_secure' or 's3'
|
||||
STORAGE_TYPE=local
|
||||
|
||||
# Image storage system to use
|
||||
# Defaults to the value of STORAGE_TYPE if unset.
|
||||
# Accepts the same values as STORAGE_TYPE.
|
||||
STORAGE_IMAGE_TYPE=local
|
||||
|
||||
# Attachment storage system to use
|
||||
# Defaults to the value of STORAGE_TYPE if unset.
|
||||
# Accepts the same values as STORAGE_TYPE although 'local' will be forced to 'local_secure'.
|
||||
STORAGE_ATTACHMENT_TYPE=local_secure
|
||||
|
||||
# Amazon S3 storage configuration
|
||||
STORAGE_S3_KEY=your-s3-key
|
||||
STORAGE_S3_SECRET=your-s3-secret
|
||||
STORAGE_S3_BUCKET=s3-bucket-name
|
||||
STORAGE_S3_REGION=s3-bucket-region
|
||||
|
||||
# S3 endpoint to use for storage calls
|
||||
# Only set this if using a non-Amazon s3-compatible service such as Minio
|
||||
STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
|
||||
|
||||
# Storage URL prefix
|
||||
# Used as a base for any generated image urls.
|
||||
# An s3-format URL will be generated if not set.
|
||||
STORAGE_URL=false
|
||||
|
||||
# Authentication method to use
|
||||
# Can be 'standard' or 'ldap'
|
||||
AUTH_METHOD=standard
|
||||
|
||||
# Social authentication configuration
|
||||
# All disabled by default.
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
|
||||
|
||||
AZURE_APP_ID=false
|
||||
AZURE_APP_SECRET=false
|
||||
AZURE_TENANT=false
|
||||
AZURE_AUTO_REGISTER=false
|
||||
AZURE_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
DISCORD_APP_ID=false
|
||||
DISCORD_APP_SECRET=false
|
||||
DISCORD_AUTO_REGISTER=false
|
||||
DISCORD_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
FACEBOOK_APP_ID=false
|
||||
FACEBOOK_APP_SECRET=false
|
||||
FACEBOOK_AUTO_REGISTER=false
|
||||
FACEBOOK_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
GITHUB_APP_ID=false
|
||||
GITHUB_APP_SECRET=false
|
||||
GITHUB_AUTO_REGISTER=false
|
||||
GITHUB_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
GITLAB_APP_ID=false
|
||||
GITLAB_APP_SECRET=false
|
||||
GITLAB_BASE_URI=false
|
||||
GITLAB_AUTO_REGISTER=false
|
||||
GITLAB_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
GOOGLE_APP_ID=false
|
||||
GOOGLE_APP_SECRET=false
|
||||
GOOGLE_SELECT_ACCOUNT=false
|
||||
GOOGLE_AUTO_REGISTER=false
|
||||
GOOGLE_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
OKTA_BASE_URL=false
|
||||
OKTA_APP_ID=false
|
||||
OKTA_APP_SECRET=false
|
||||
OKTA_AUTO_REGISTER=false
|
||||
OKTA_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
SLACK_APP_ID=false
|
||||
SLACK_APP_SECRET=false
|
||||
SLACK_AUTO_REGISTER=false
|
||||
SLACK_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
TWITCH_APP_ID=false
|
||||
TWITCH_APP_SECRET=false
|
||||
TWITCH_AUTO_REGISTER=false
|
||||
TWITCH_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
TWITTER_APP_ID=false
|
||||
TWITTER_APP_SECRET=false
|
||||
TWITTER_AUTO_REGISTER=false
|
||||
TWITTER_AUTO_CONFIRM_EMAIL=false
|
||||
|
||||
# LDAP authentication configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
|
||||
LDAP_SERVER=false
|
||||
LDAP_BASE_DN=false
|
||||
LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=false
|
||||
LDAP_TLS_INSECURE=false
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
||||
LDAP_FOLLOW_REFERRALS=true
|
||||
|
||||
# LDAP group sync configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
|
||||
LDAP_USER_TO_GROUPS=false
|
||||
LDAP_GROUP_ATTRIBUTE="memberOf"
|
||||
LDAP_REMOVE_FROM_GROUPS=false
|
||||
|
||||
# Disable default third-party services such as Gravatar and Draw.IO
|
||||
# Service-specific options will override this option
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# Use custom avatar service, Sets fetch URL
|
||||
# Possible placeholders: ${hash} ${size} ${email}
|
||||
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
|
||||
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
|
||||
AVATAR_URL=
|
||||
|
||||
# Enable Draw.io integration
|
||||
DRAWIO=true
|
||||
|
||||
# Default item listing view
|
||||
# Used for public visitors and user's without a preference
|
||||
# Can be 'list' or 'grid'
|
||||
APP_VIEWS_BOOKS=list
|
||||
APP_VIEWS_BOOKSHELVES=grid
|
||||
|
||||
# Page revision limit
|
||||
# Number of page revisions to keep in the system before deleting old revisions.
|
||||
# If set to 'false' a limit will not be enforced.
|
||||
REVISION_LIMIT=50
|
||||
|
||||
# Allow <script> tags in page content
|
||||
# Note, if set to 'true' the page editor may still escape scripts.
|
||||
ALLOW_CONTENT_SCRIPTS=false
|
||||
|
||||
# Indicate if robots/crawlers should crawl your instance.
|
||||
# Can be 'true', 'false' or 'null'.
|
||||
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.
|
||||
# Contents of the robots.txt file can be overridden, making this option obsolete.
|
||||
ALLOW_ROBOTS=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
|
||||
13
.github/ISSUE_TEMPLATE.md
vendored
13
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,13 +0,0 @@
|
||||
### For Feature Requests
|
||||
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
|
||||
* BookStack Version:
|
||||
* PHP Version:
|
||||
* MySQL Version:
|
||||
|
||||
##### Expected Behavior
|
||||
|
||||
##### Actual Behavior
|
||||
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Your Configuration (please complete the following information):**
|
||||
- Exact BookStack Version (Found in settings):
|
||||
- PHP Version:
|
||||
- Hosting Method (Nginx/Apache/Docker):
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
A clear description of the feature you'd like implemented in BookStack.
|
||||
|
||||
**Describe the benefits this feature would bring to BookStack users**
|
||||
Explain the measurable benefits this feature would achieve.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,8 +2,10 @@
|
||||
/node_modules
|
||||
Homestead.yaml
|
||||
.env
|
||||
/public/dist
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist
|
||||
/public/plugins
|
||||
/public/css
|
||||
/public/js
|
||||
@@ -18,5 +20,5 @@ yarn.lock
|
||||
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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
@@ -16,7 +19,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 +48,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,7 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
use Session;
|
||||
|
||||
class ActivityService
|
||||
@@ -12,7 +12,7 @@ class ActivityService
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param Activity $activity
|
||||
* @param \BookStack\Actions\Activity $activity
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
@@ -103,18 +103,22 @@ class ActivityService
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function entityActivity($entity, $count = 20, $page = 0)
|
||||
public function entityActivity($entity, $count = 20, $page = 1)
|
||||
{
|
||||
if ($entity->isA('book')) {
|
||||
$query = $this->activity->where('book_id', '=', $entity->id);
|
||||
} else {
|
||||
$query = $this->activity->where('entity_type', '=', get_class($entity))
|
||||
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
|
||||
->orderBy('created_at', 'desc')
|
||||
->with(['entity', 'user.avatar'])
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get();
|
||||
|
||||
return $this->filterSimilar($activity);
|
||||
}
|
||||
@@ -170,5 +174,4 @@ class ActivityService
|
||||
Session::flash('success', $message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Ownable;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
@@ -1,22 +1,22 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Entities\Entity;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
* @package BookStack\Repos
|
||||
*/
|
||||
class CommentRepo {
|
||||
class CommentRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
* @var \BookStack\Actions\Comment $comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* CommentRepo constructor.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
*/
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
@@ -26,7 +26,7 @@ class CommentRepo {
|
||||
/**
|
||||
* Get a comment by ID.
|
||||
* @param $id
|
||||
* @return Comment|\Illuminate\Database\Eloquent\Model
|
||||
* @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
@@ -35,11 +35,11 @@ class CommentRepo {
|
||||
|
||||
/**
|
||||
* Create a new comment on an entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param array $data
|
||||
* @return Comment
|
||||
* @return \BookStack\Actions\Comment
|
||||
*/
|
||||
public function create (Entity $entity, $data = [])
|
||||
public function create(Entity $entity, $data = [])
|
||||
{
|
||||
$userId = user()->id;
|
||||
$comment = $this->comment->newInstance($data);
|
||||
@@ -52,7 +52,7 @@ class CommentRepo {
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
* @param array $input
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -65,7 +65,7 @@ class CommentRepo {
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($comment)
|
||||
@@ -75,13 +75,15 @@ class CommentRepo {
|
||||
|
||||
/**
|
||||
* Get the next local ID relative to the linked entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
protected function getNextLocalId(Entity $entity)
|
||||
{
|
||||
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
||||
if ($comments === null) return 1;
|
||||
if ($comments === null) {
|
||||
return 1;
|
||||
}
|
||||
return $comments->local_id + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
@@ -16,4 +18,4 @@ class Tag extends Model
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
@@ -17,9 +16,9 @@ class TagRepo
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
* @param \BookStack\Actions\Tag $attr
|
||||
* @param \BookStack\Entities\Entity $ent
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
@@ -52,7 +51,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;
|
||||
}
|
||||
@@ -95,7 +96,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');
|
||||
@@ -103,7 +106,7 @@ class TagRepo
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
@@ -112,7 +115,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);
|
||||
}
|
||||
|
||||
@@ -122,7 +127,7 @@ class TagRepo
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return Tag
|
||||
* @return \BookStack\Actions\Tag
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
@@ -132,5 +137,4 @@ class TagRepo
|
||||
$values = ['name' => $name, 'value' => $value];
|
||||
return $this->tag->newInstance($values);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
@@ -1,22 +1,27 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
protected $view;
|
||||
protected $permissionService;
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Actions\View $view
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +32,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) {
|
||||
@@ -48,20 +55,21 @@ class ViewService
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false|array $filterModel
|
||||
* @param string|array $filterModels
|
||||
* @param string $action - used for permission checking
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false)
|
||||
public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $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');
|
||||
|
||||
if ($filterModel && is_array($filterModel)) {
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
@@ -77,12 +85,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', '=', $filterModel->getMorphClass());
|
||||
}
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
|
||||
@@ -97,5 +109,4 @@ class ViewService
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
19
app/Application.php
Normal file
19
app/Application.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
|
||||
class Application extends \Illuminate\Foundation\Application
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the path to the application configuration files.
|
||||
*
|
||||
* @param string $path Optionally, a path to append to the config path
|
||||
* @return string
|
||||
*/
|
||||
public function configPath($path = '')
|
||||
{
|
||||
return $this->basePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
|
||||
}
|
||||
|
||||
}
|
||||
40
app/Auth/Access/EmailConfirmationService.php
Normal file
40
app/Auth/Access/EmailConfirmationService.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Notifications\ConfirmEmail;
|
||||
|
||||
class EmailConfirmationService extends UserTokenService
|
||||
{
|
||||
protected $tokenTable = 'email_confirmations';
|
||||
protected $expiryTime = 24;
|
||||
|
||||
/**
|
||||
* Create new confirmation for a user,
|
||||
* Also removes any existing old ones.
|
||||
* @param User $user
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
public function sendConfirmation(User $user)
|
||||
{
|
||||
if ($user->email_confirmed) {
|
||||
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
||||
}
|
||||
|
||||
$this->deleteByUser($user);
|
||||
$token = $this->createTokenForUser($user);
|
||||
|
||||
$user->notify(new ConfirmEmail($token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if confirmation is required in this instance.
|
||||
* @return bool
|
||||
*/
|
||||
public function confirmationRequired() : bool
|
||||
{
|
||||
return setting('registration-confirmation')
|
||||
|| setting('registration-restrict');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
/**
|
||||
* Class Ldap
|
||||
@@ -94,4 +93,26 @@ class Ldap
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explode a LDAP dn string into an array of components.
|
||||
* @param string $dn
|
||||
* @param int $withAttrib
|
||||
* @return array
|
||||
*/
|
||||
public function explodeDn(string $dn, int $withAttrib)
|
||||
{
|
||||
return ldap_explode_dn($dn, $withAttrib);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use in an LDAP filter.
|
||||
* @param string $value
|
||||
* @param string $ignore
|
||||
* @param int $flags
|
||||
* @return string
|
||||
*/
|
||||
public function escape(string $value, string $ignore = "", int $flags = 0)
|
||||
{
|
||||
return ldap_escape($value, $ignore, $flags);
|
||||
}
|
||||
}
|
||||
415
app/Auth/Access/LdapService.php
Normal file
415
app/Auth/Access/LdapService.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\Access;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
* Handles any app-specific LDAP tasks.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
protected $userRepo;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
* @param Ldap $ldap
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(Access\Ldap $ldap, UserRepo $userRepo)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
$this->userRepo = $userRepo;
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if groups should be synced.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSyncGroups()
|
||||
{
|
||||
return $this->enabled && $this->config['user_to_groups'] !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for attributes for a specific user on the ldap
|
||||
* @param string $userName
|
||||
* @param array $attributes
|
||||
* @return null|array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getUserWithAttributes($userName, $attributes)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
|
||||
if ($users['count'] === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
* @param $userName
|
||||
* @return array|null
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails($userName)
|
||||
{
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$displayNameAttr = $this->config['display_name_attribute'];
|
||||
|
||||
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
|
||||
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userCn = $this->getUserResponseProperty($user, 'cn', null);
|
||||
return [
|
||||
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a property from an LDAP user response fetch.
|
||||
* Handles properties potentially being part of an array.
|
||||
* @param array $userDetails
|
||||
* @param string $propertyKey
|
||||
* @param $defaultValue
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
|
||||
{
|
||||
if (isset($userDetails[$propertyKey])) {
|
||||
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
|
||||
}
|
||||
|
||||
return $defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Authenticatable $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
} catch (\ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
||||
return $ldapBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the system user to the LDAP connection using the given credentials
|
||||
* otherwise anonymous access is attempted.
|
||||
* @param $connection
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function bindSystemUser($connection)
|
||||
{
|
||||
$ldapDn = $this->config['dn'];
|
||||
$ldapPass = $this->config['pass'];
|
||||
|
||||
$isAnonymous = ($ldapDn === false || $ldapPass === false);
|
||||
if ($isAnonymous) {
|
||||
$ldapBind = $this->ldap->bind($connection);
|
||||
} else {
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) {
|
||||
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) {
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
throw new LdapException(trans('errors.ldap_extension_not_installed'));
|
||||
}
|
||||
|
||||
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
|
||||
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
|
||||
if ($this->config['tls_insecure']) {
|
||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||
}
|
||||
|
||||
$serverDetails = $this->parseServerString($this->config['server']);
|
||||
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a LDAP server string and return the host and port for
|
||||
* a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
|
||||
* @param $serverString
|
||||
* @return array
|
||||
*/
|
||||
protected function parseServerString($serverString)
|
||||
{
|
||||
$serverNameParts = explode(':', $serverString);
|
||||
|
||||
// If we have a protocol just return the full string since PHP will ignore a separate port.
|
||||
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
|
||||
return ['host' => $serverString, 'port' => 389];
|
||||
}
|
||||
|
||||
// Otherwise, extract the port out
|
||||
$hostName = $serverNameParts[0];
|
||||
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
||||
return ['host' => $hostName, 'port' => $ldapPort];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filter string by injecting common variables.
|
||||
* @param string $filterString
|
||||
* @param array $attrs
|
||||
* @return string
|
||||
*/
|
||||
protected function buildFilter($filterString, array $attrs)
|
||||
{
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $attrText) {
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $this->ldap->escape($attrText);
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the groups a user is a part of on ldap
|
||||
* @param string $userName
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserGroups($userName)
|
||||
{
|
||||
$groupsAttr = $this->config['group_attribute'];
|
||||
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
|
||||
|
||||
if ($user === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userGroups = $this->groupFilter($user);
|
||||
$userGroups = $this->getGroupsRecursive($userGroups, []);
|
||||
return $userGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent groups of an array of groups
|
||||
* @param array $groupsArray
|
||||
* @param array $checked
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupsRecursive($groupsArray, $checked)
|
||||
{
|
||||
$groups_to_add = [];
|
||||
foreach ($groupsArray as $groupName) {
|
||||
if (in_array($groupName, $checked)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$groupsToAdd = $this->getGroupGroups($groupName);
|
||||
$groups_to_add = array_merge($groups_to_add, $groupsToAdd);
|
||||
$checked[] = $groupName;
|
||||
}
|
||||
$groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
|
||||
|
||||
if (!empty($groups_to_add)) {
|
||||
return $this->getGroupsRecursive($groupsArray, $checked);
|
||||
} else {
|
||||
return $groupsArray;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent groups of a single group
|
||||
* @param string $groupName
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupGroups($groupName)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$groupsAttr = strtolower($this->config['group_attribute']);
|
||||
|
||||
$groupFilter = 'CN=' . $this->ldap->escape($groupName);
|
||||
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]);
|
||||
if ($groups['count'] === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupGroups = $this->groupFilter($groups[0]);
|
||||
return $groupGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out LDAP CN and DN language in a ldap search return
|
||||
* Gets the base CN (common name) of the string
|
||||
* @param array $userGroupSearchResponse
|
||||
* @return array
|
||||
*/
|
||||
protected function groupFilter(array $userGroupSearchResponse)
|
||||
{
|
||||
$groupsAttr = strtolower($this->config['group_attribute']);
|
||||
$ldapGroups = [];
|
||||
$count = 0;
|
||||
|
||||
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
|
||||
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
|
||||
if (!in_array($dnComponents[0], $ldapGroups)) {
|
||||
$ldapGroups[] = $dnComponents[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $ldapGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the LDAP groups to the user roles for the current user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param string $username
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function syncGroups(User $user, string $username)
|
||||
{
|
||||
$userLdapGroups = $this->getUserGroups($username);
|
||||
|
||||
// Get the ids for the roles from the names
|
||||
$ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups);
|
||||
|
||||
// Sync groups
|
||||
if ($this->config['remove_from_groups']) {
|
||||
$user->roles()->sync($ldapGroupsAsRoles);
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
} else {
|
||||
$user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an array of group names from LDAP to BookStack system roles.
|
||||
* Formats LDAP group names to be lower-case and hyphenated.
|
||||
* @param array $groupNames
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function matchLdapGroupsToSystemsRoles(array $groupNames)
|
||||
{
|
||||
foreach ($groupNames as $i => $groupName) {
|
||||
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
|
||||
}
|
||||
|
||||
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
|
||||
$query->whereIn('name', $groupNames);
|
||||
foreach ($groupNames as $groupName) {
|
||||
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
|
||||
}
|
||||
})->get();
|
||||
|
||||
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
|
||||
return $this->roleMatchesGroupNames($role, $groupNames);
|
||||
});
|
||||
|
||||
return $matchedRoles->pluck('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a role against an array of group names to see if it matches.
|
||||
* Checked against role 'external_auth_id' if set otherwise the name of the role.
|
||||
* @param \BookStack\Auth\Role $role
|
||||
* @param array $groupNames
|
||||
* @return bool
|
||||
*/
|
||||
protected function roleMatchesGroupNames(Role $role, array $groupNames)
|
||||
{
|
||||
if ($role->external_auth_id) {
|
||||
$externalAuthIds = explode(',', strtolower($role->external_auth_id));
|
||||
foreach ($externalAuthIds as $externalAuthId) {
|
||||
if (in_array(trim($externalAuthId), $groupNames)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
|
||||
return in_array($roleName, $groupNames);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\SocialAccount;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
@@ -14,11 +15,11 @@ class SocialAuthService
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
* @param Socialite $socialite
|
||||
* @param SocialAccount $socialAccount
|
||||
*/
|
||||
@@ -39,7 +40,7 @@ class SocialAuthService
|
||||
public function startLogIn($socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->socialite->driver($driver)->redirect();
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,23 +52,18 @@ class SocialAuthService
|
||||
public function startRegister($socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->socialite->driver($driver)->redirect();
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the social registration process on callback.
|
||||
* @param $socialDriver
|
||||
* @return \Laravel\Socialite\Contracts\User
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return SocialUser
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function handleRegistrationCallback($socialDriver)
|
||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
|
||||
// Get user details from social driver
|
||||
$socialUser = $this->socialite->driver($driver)->user();
|
||||
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
@@ -82,18 +78,26 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the login process on a oAuth callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* Get the social user details via the social driver.
|
||||
* @param string $socialDriver
|
||||
* @return SocialUser
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws SocialSignInException
|
||||
*/
|
||||
public function handleLoginCallback($socialDriver)
|
||||
public function getSocialUser(string $socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->socialite->driver($driver)->user();
|
||||
}
|
||||
|
||||
// Get user details from social driver
|
||||
$socialUser = $this->socialite->driver($driver)->user();
|
||||
/**
|
||||
* Handle the login process on a oAuth callback.
|
||||
* @param $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws SocialSignInAccountNotUsed
|
||||
*/
|
||||
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialId = $socialUser->getId();
|
||||
|
||||
// Get any attached social accounts or users
|
||||
@@ -104,7 +108,8 @@ class SocialAuthService
|
||||
// When a user is not logged in and a matching SocialAccount exists,
|
||||
// Simply log the user into the application.
|
||||
if (!$isLoggedIn && $socialAccount !== null) {
|
||||
return $this->logUserIn($socialAccount->user);
|
||||
auth()->login($socialAccount->user);
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
// When a user is logged in but the social account does not exist,
|
||||
@@ -134,14 +139,7 @@ class SocialAuthService
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message . '.', '/login');
|
||||
}
|
||||
|
||||
|
||||
private function logUserIn($user)
|
||||
{
|
||||
auth()->login($user);
|
||||
return redirect('/');
|
||||
throw new SocialSignInAccountNotUsed($message, '/login');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,8 +153,12 @@ class SocialAuthService
|
||||
{
|
||||
$driver = trim(strtolower($socialDriver));
|
||||
|
||||
if (!in_array($driver, $this->validSocialDrivers)) abort(404, trans('errors.social_driver_not_found'));
|
||||
if (!$this->checkDriverConfigured($driver)) throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
if (!in_array($driver, $this->validSocialDrivers)) {
|
||||
abort(404, trans('errors.social_driver_not_found'));
|
||||
}
|
||||
if (!$this->checkDriverConfigured($driver)) {
|
||||
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => title_case($socialDriver)]));
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
@@ -200,8 +202,28 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
* Check if the current config for the given driver allows auto-registration.
|
||||
* @param string $driver
|
||||
* @return bool
|
||||
*/
|
||||
public function driverAutoRegisterEnabled(string $driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.auto_register') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current config for the given driver allow email address auto-confirmation.
|
||||
* @param string $driver
|
||||
* @return bool
|
||||
*/
|
||||
public function driverAutoConfirmEmailEnabled(string $driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return SocialAccount
|
||||
*/
|
||||
public function fillSocialAccount($socialDriver, $socialUser)
|
||||
@@ -226,4 +248,19 @@ class SocialAuthService
|
||||
return redirect(user()->getEditUrl());
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Provide redirect options per service for the Laravel Socialite driver
|
||||
* @param $driverName
|
||||
* @return \Laravel\Socialite\Contracts\Provider
|
||||
*/
|
||||
public function getSocialDriver(string $driverName)
|
||||
{
|
||||
$driver = $this->socialite->driver($driverName);
|
||||
|
||||
if ($driverName === 'google' && config('services.google.select_account')) {
|
||||
$driver->with(['prompt' => 'select_account']);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
}
|
||||
23
app/Auth/Access/UserInviteService.php
Normal file
23
app/Auth/Access/UserInviteService.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Notifications\UserInvite;
|
||||
|
||||
class UserInviteService extends UserTokenService
|
||||
{
|
||||
protected $tokenTable = 'user_invites';
|
||||
protected $expiryTime = 336; // Two weeks
|
||||
|
||||
/**
|
||||
* Send an invitation to a user to sign into BookStack
|
||||
* Removes existing invitation tokens.
|
||||
* @param User $user
|
||||
*/
|
||||
public function sendInvitation(User $user)
|
||||
{
|
||||
$this->deleteByUser($user);
|
||||
$token = $this->createTokenForUser($user);
|
||||
$user->notify(new UserInvite($token));
|
||||
}
|
||||
|
||||
}
|
||||
134
app/Auth/Access/UserTokenService.php
Normal file
134
app/Auth/Access/UserTokenService.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Connection as Database;
|
||||
use stdClass;
|
||||
|
||||
class UserTokenService
|
||||
{
|
||||
|
||||
/**
|
||||
* Name of table where user tokens are stored.
|
||||
* @var string
|
||||
*/
|
||||
protected $tokenTable = 'user_tokens';
|
||||
|
||||
/**
|
||||
* Token expiry time in hours.
|
||||
* @var int
|
||||
*/
|
||||
protected $expiryTime = 24;
|
||||
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* UserTokenService constructor.
|
||||
* @param Database $db
|
||||
*/
|
||||
public function __construct(Database $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
* @param User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteByUser(User $user)
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
->where('user_id', '=', $user->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user id from a token, while check the token exists and has not expired.
|
||||
* @param string $token
|
||||
* @return int
|
||||
* @throws UserTokenNotFoundException
|
||||
* @throws UserTokenExpiredException
|
||||
*/
|
||||
public function checkTokenAndGetUserId(string $token) : int
|
||||
{
|
||||
$entry = $this->getEntryByToken($token);
|
||||
|
||||
if (is_null($entry)) {
|
||||
throw new UserTokenNotFoundException('Token "' . $token . '" not found');
|
||||
}
|
||||
|
||||
if ($this->entryExpired($entry)) {
|
||||
throw new UserTokenExpiredException("Token of id {$entry->id} has expired.", $entry->user_id);
|
||||
}
|
||||
|
||||
return $entry->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique token within the email confirmation database.
|
||||
* @return string
|
||||
*/
|
||||
protected function generateToken() : string
|
||||
{
|
||||
$token = str_random(24);
|
||||
while ($this->tokenExists($token)) {
|
||||
$token = str_random(25);
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and store a token for the given user.
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
protected function createTokenForUser(User $user) : string
|
||||
{
|
||||
$token = $this->generateToken();
|
||||
$this->db->table($this->tokenTable)->insert([
|
||||
'user_id' => $user->id,
|
||||
'token' => $token,
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given token exists.
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
protected function tokenExists(string $token) : bool
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
->where('token', '=', $token)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a token entry for the given token.
|
||||
* @param string $token
|
||||
* @return object|null
|
||||
*/
|
||||
protected function getEntryByToken(string $token)
|
||||
{
|
||||
return $this->db->table($this->tokenTable)
|
||||
->where('token', '=', $token)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given token entry has expired.
|
||||
* @param stdClass $tokenEntry
|
||||
* @return bool
|
||||
*/
|
||||
protected function entryExpired(stdClass $tokenEntry) : bool
|
||||
{
|
||||
return Carbon::now()->subHours($this->expiryTime)
|
||||
->gt(new Carbon($tokenEntry->created_at));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
@@ -1,4 +1,8 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Model;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
@@ -1,14 +1,14 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
@@ -22,38 +22,53 @@ class PermissionService
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var JointPermission
|
||||
*/
|
||||
protected $jointPermission;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var EntityPermission
|
||||
*/
|
||||
protected $entityPermission;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Connection $db
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
* @param Connection $db
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
public function __construct(
|
||||
JointPermission $jointPermission,
|
||||
Permissions\EntityPermission $entityPermission,
|
||||
Role $role,
|
||||
Connection $db,
|
||||
EntityProvider $entityProvider
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
// TODO - Update so admin still goes through filters
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,13 +82,19 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param \BookStack\Entities\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 +104,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);
|
||||
$book = $this->entityProvider->book->find($bookId);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
|
||||
return $book;
|
||||
@@ -99,18 +119,17 @@ class PermissionService
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return Book
|
||||
* @return \BookStack\Entities\Book
|
||||
*/
|
||||
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);
|
||||
$chapter = $this->entityProvider->chapter->find($chapterId);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
|
||||
return $chapter;
|
||||
@@ -122,7 +141,9 @@ class PermissionService
|
||||
*/
|
||||
protected function getRoles()
|
||||
{
|
||||
if ($this->userRoles !== false) return $this->userRoles;
|
||||
if ($this->userRoles !== false) {
|
||||
return $this->userRoles;
|
||||
}
|
||||
|
||||
$roles = [];
|
||||
|
||||
@@ -153,6 +174,12 @@ class PermissionService
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,11 +188,26 @@ class PermissionService
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
return $this->entityProvider->book->newQuery()
|
||||
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id']);
|
||||
}, 'pages' => function ($query) {
|
||||
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
|
||||
}]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,8 +215,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,13 +231,16 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($entities->all());
|
||||
}
|
||||
$this->createManyJointPermissions($entities, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -204,20 +251,27 @@ class PermissionService
|
||||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
}
|
||||
|
||||
if ($entity->isA('chapter')) {
|
||||
foreach ($entity->pages as $page) {
|
||||
$entities[] = $page;
|
||||
}
|
||||
}
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
@@ -236,9 +290,15 @@ class PermissionService
|
||||
$this->deleteManyJointPermissionsForRoles($roles);
|
||||
|
||||
// Chunk through all books
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,7 +316,7 @@ class PermissionService
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForRoles($roles)
|
||||
{
|
||||
$roleIds = array_map(function($role) {
|
||||
$roleIds = array_map(function ($role) {
|
||||
return $role->id;
|
||||
}, $roles);
|
||||
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
|
||||
@@ -265,6 +325,7 @@ class PermissionService
|
||||
/**
|
||||
* Delete the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function deleteJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -273,25 +334,27 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
* @param \BookStack\Entities\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 +362,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 +374,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 +391,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 +405,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);
|
||||
}
|
||||
@@ -351,14 +415,18 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
{
|
||||
$baseActions = ['view', 'update', 'delete'];
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
|
||||
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
|
||||
if ($entity->isA('chapter') || $entity->isA('book')) {
|
||||
$baseActions[] = 'page-create';
|
||||
}
|
||||
if ($entity->isA('book')) {
|
||||
$baseActions[] = 'chapter-create';
|
||||
}
|
||||
return $baseActions;
|
||||
}
|
||||
|
||||
@@ -389,7 +457,7 @@ class PermissionService
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
@@ -407,7 +475,10 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action,
|
||||
return $this->createJointPermissionDataArray(
|
||||
$entity,
|
||||
$role,
|
||||
$action,
|
||||
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
|
||||
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
|
||||
);
|
||||
@@ -421,7 +492,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;
|
||||
}
|
||||
@@ -429,7 +501,7 @@ class PermissionService
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
@@ -457,11 +529,6 @@ class PermissionService
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
@@ -489,10 +556,43 @@ class PermissionService
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has the given permission for any items in the system.
|
||||
* Can be passed an entity instance to filter on a specific type.
|
||||
* @param string $permission
|
||||
* @param string $entityClass
|
||||
* @return bool
|
||||
*/
|
||||
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
|
||||
{
|
||||
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
|
||||
$userId = $this->currentUser()->id;
|
||||
|
||||
$permissionQuery = $this->db->table('joint_permissions')
|
||||
->where('action', '=', $permission)
|
||||
->whereIn('role_id', $userRoleIds)
|
||||
->where(function ($query) use ($userId) {
|
||||
$query->where('has_permission', '=', 1)
|
||||
->orWhere(function ($query2) use ($userId) {
|
||||
$query2->where('has_permission_own', '=', 1)
|
||||
->where('created_by', '=', $userId);
|
||||
});
|
||||
});
|
||||
|
||||
if (!is_null($entityClass)) {
|
||||
$entityInstance = app()->make($entityClass);
|
||||
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
|
||||
}
|
||||
|
||||
$hasPermission = $permissionQuery->count() > 0;
|
||||
$this->clean();
|
||||
return $hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has restrictions set on itself or its
|
||||
* parent tree.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
@@ -540,30 +640,32 @@ 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) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function($query) {
|
||||
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
|
||||
{
|
||||
$entities = $this->entityProvider;
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
|
||||
->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
|
||||
$query->where('draft', '=', 0);
|
||||
if (!$filterDrafts) {
|
||||
$query->orWhere(function ($query) {
|
||||
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
|
||||
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
|
||||
|
||||
if (!$this->isAdmin()) {
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
// Add joint permission filter
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
}
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
|
||||
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
|
||||
$this->clean();
|
||||
@@ -573,7 +675,7 @@ class PermissionService
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param Builder|\BookStack\Entities\Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
*/
|
||||
@@ -591,11 +693,6 @@ class PermissionService
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
@@ -606,16 +703,13 @@ class PermissionService
|
||||
* @param string $tableName
|
||||
* @param string $entityIdColumn
|
||||
* @param string $entityTypeColumn
|
||||
* @return mixed
|
||||
* @param string $action
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
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) {
|
||||
@@ -638,28 +732,27 @@ class PermissionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pages that are a direct relation to another item.
|
||||
* Add conditions to a query to filter the selection to related entities
|
||||
* where permissions are granted.
|
||||
* @param $entityType
|
||||
* @param $query
|
||||
* @param $tableName
|
||||
* @param $entityIdColumn
|
||||
* @return mixed
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', 'Bookstack\\Page')
|
||||
->where('entity_type', '=', $pageMorphClass)
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
@@ -671,26 +764,15 @@ class PermissionService
|
||||
});
|
||||
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
|
||||
});
|
||||
|
||||
$this->clean();
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin.
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin()
|
||||
{
|
||||
if ($this->isAdminUser === null) {
|
||||
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
|
||||
}
|
||||
|
||||
return $this->isAdminUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return User
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
private function currentUser()
|
||||
{
|
||||
@@ -710,5 +792,4 @@ class PermissionService
|
||||
$this->userRoles = false;
|
||||
$this->isAdminUser = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
@@ -20,9 +17,9 @@ class PermissionsRepo
|
||||
* PermissionsRepo constructor.
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
@@ -81,7 +78,7 @@ class PermissionsRepo
|
||||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always has all permissions.
|
||||
* Ensure Admin role always have core permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
@@ -91,13 +88,18 @@ class PermissionsRepo
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
$permissions = array_merge($permissions, [
|
||||
'users-manage',
|
||||
'user-roles-manage',
|
||||
'restrictions-manage-all',
|
||||
'restrictions-manage-own',
|
||||
'settings-manage',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
@@ -149,5 +151,4 @@ class PermissionsRepo
|
||||
$this->permissionService->deleteJointPermissionsForRole($role);
|
||||
$role->delete();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Model;
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
@@ -8,7 +10,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');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,17 +1,20 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\RolePermission;
|
||||
use BookStack\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['display_name', 'description'];
|
||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +43,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 +96,4 @@ class Role extends Model
|
||||
{
|
||||
return static::where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Model;
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use BookStack\Uploads\Image;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
@@ -8,6 +11,20 @@ use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
/**
|
||||
* Class User
|
||||
* @package BookStack\Auth
|
||||
* @property string $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $password
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property bool $email_confirmed
|
||||
* @property int $image_id
|
||||
* @property string $external_auth_id
|
||||
* @property string $system_name
|
||||
*/
|
||||
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
|
||||
{
|
||||
use Authenticatable, CanResetPassword, Notifiable;
|
||||
@@ -22,7 +39,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
* The attributes that are mass assignable.
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['name', 'email', 'image_id'];
|
||||
protected $fillable = ['name', 'email'];
|
||||
|
||||
/**
|
||||
* The attributes excluded from the model's JSON form.
|
||||
@@ -60,7 +77,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 +100,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 +110,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 +128,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);
|
||||
}
|
||||
|
||||
@@ -160,12 +183,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getAvatar($size = 50)
|
||||
{
|
||||
$default = baseUrl('/user_avatar.png');
|
||||
$default = url('/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;
|
||||
$avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$avatar = $default;
|
||||
}
|
||||
@@ -187,7 +212,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return baseUrl('/settings/users/' . $this->id);
|
||||
return url('/settings/users/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +221,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getProfileUrl()
|
||||
{
|
||||
return baseUrl('/user/' . $this->id);
|
||||
return url('/user/' . $this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,10 +231,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
*/
|
||||
public function getShortName($chars = 8)
|
||||
{
|
||||
if (strlen($this->name) <= $chars) return $this->name;
|
||||
if (mb_strlen($this->name) <= $chars) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
$splitName = explode(' ', $this->name);
|
||||
if (strlen($splitName[0]) <= $chars) return $splitName[0];
|
||||
if (mb_strlen($splitName[0]) <= $chars) {
|
||||
return $splitName[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
295
app/Auth/UserRepo.php
Normal file
295
app/Auth/UserRepo.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\Image;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Images;
|
||||
|
||||
class UserRepo
|
||||
{
|
||||
|
||||
protected $user;
|
||||
protected $role;
|
||||
protected $entityRepo;
|
||||
|
||||
/**
|
||||
* UserRepo constructor.
|
||||
* @param User $user
|
||||
* @param Role $role
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->role = $role;
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $email
|
||||
* @return User|null
|
||||
*/
|
||||
public function getByEmail($email)
|
||||
{
|
||||
return $this->user->where('email', '=', $email)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return User
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->user->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions.
|
||||
* @return Builder|static
|
||||
*/
|
||||
public function getAllUsers()
|
||||
{
|
||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the users with their permissions in a paginated format.
|
||||
* @param int $count
|
||||
* @param $sortData
|
||||
* @return Builder|static
|
||||
*/
|
||||
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('name', 'like', $term)
|
||||
->orWhere('email', 'like', $term);
|
||||
});
|
||||
}
|
||||
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
* @param boolean $verifyEmail
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
public function registerNew(array $data, $verifyEmail = false)
|
||||
{
|
||||
$user = $this->create($data, $verifyEmail);
|
||||
$this->attachDefaultRole($user);
|
||||
$this->downloadAndAssignUserAvatar($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a user the default role. Used when creating a new user.
|
||||
* @param User $user
|
||||
*/
|
||||
public function attachDefaultRole(User $user)
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a user to a system-level role.
|
||||
* @param User $user
|
||||
* @param $systemRoleName
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function attachSystemRole(User $user, $systemRoleName)
|
||||
{
|
||||
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
|
||||
if ($role === null) {
|
||||
throw new NotFoundException("Role '{$systemRoleName}' not found");
|
||||
}
|
||||
$user->attachRole($role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the give user is the only admin.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
{
|
||||
if (!$user->hasSystemRole('admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
if ($adminRole->users->count() > 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the assigned user roles via an array of role IDs.
|
||||
* @param User $user
|
||||
* @param array $roles
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function setUserRoles(User $user, array $roles)
|
||||
{
|
||||
if ($this->demotingLastAdmin($user, $roles)) {
|
||||
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
|
||||
}
|
||||
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given user is the last admin and their new roles no longer
|
||||
* contains the admin role.
|
||||
* @param User $user
|
||||
* @param array $newRoles
|
||||
* @return bool
|
||||
*/
|
||||
protected function demotingLastAdmin(User $user, array $newRoles) : bool
|
||||
{
|
||||
if ($this->isOnlyAdmin($user)) {
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
if (!in_array(strval($adminRole->id), $newRoles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new basic instance of user.
|
||||
* @param array $data
|
||||
* @param boolean $verifyEmail
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
public function create(array $data, $verifyEmail = false)
|
||||
{
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => $verifyEmail
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroy($image);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest activity for a user.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
*/
|
||||
public function getActivity(User $user, $count = 20, $page = 0)
|
||||
{
|
||||
return Activity::userActivity($user, $count, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreated(User $user, $count = 20)
|
||||
{
|
||||
$createdByUserQuery = function (Builder $query) use ($user) {
|
||||
$query->where('created_by', '=', $user->id);
|
||||
};
|
||||
|
||||
return [
|
||||
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
|
||||
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
|
||||
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
|
||||
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
|
||||
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
|
||||
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
|
||||
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles in the system that are assignable to a user.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->newQuery()->orderBy('name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles which can be given restricted access to
|
||||
* other entities in the system.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRestrictableRoles()
|
||||
{
|
||||
return $this->role->where('system_name', '!=', 'admin')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an avatar image for a user and set it as their avatar.
|
||||
* Returns early if avatars disabled or not set in config.
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function downloadAndAssignUserAvatar(User $user)
|
||||
{
|
||||
if (!Images::avatarFetchEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = Images::saveUserAvatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user avatar image');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/Book.php
68
app/Book.php
@@ -1,68 +0,0 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
|
||||
protected $fillable = ['name', 'description'];
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\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,124 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Global app configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
|
||||
// The environment to run BookStack in.
|
||||
// Options: production, development, demo, testing
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
'editor' => env('APP_EDITOR', 'html'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
// Enter the application in debug mode.
|
||||
// Shows much more verbose error messages. Has potential to show
|
||||
// private configuration variables so should remain disabled in public.
|
||||
'debug' => env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| your application so that it is used when running Artisan tasks.
|
||||
|
|
||||
*/
|
||||
// Set the default view type for various lists. Can be overridden by user preferences.
|
||||
// These will be used for public viewers and users that have not set a preference.
|
||||
'views' => [
|
||||
'books' => env('APP_VIEWS_BOOKS', 'list'),
|
||||
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||
],
|
||||
|
||||
// The number of revisions to keep in the database.
|
||||
// Once this limit is reached older revisions will be deleted.
|
||||
// If set to false then a limit will not be enforced.
|
||||
'revision_limit' => env('REVISION_LIMIT', 50),
|
||||
|
||||
// Allow <script> tags to entered within page content.
|
||||
// <script> tags are escaped by default.
|
||||
// Even when overridden the WYSIWYG editor may still escape script content.
|
||||
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
|
||||
|
||||
// Override the default behaviour for allowing crawlers to crawl the instance.
|
||||
// May be ignored if view has be overridden or modified.
|
||||
// Defaults to null since, if not set, 'app-public' status used instead.
|
||||
'allow_robots' => env('ALLOW_ROBOTS', null),
|
||||
|
||||
// Application Base URL, Used by laravel in development commands
|
||||
// and used by BookStack in URL generation.
|
||||
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. We have gone
|
||||
| ahead and set this to a sensible default for you out of the box.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by the translation service provider. You are free to set this value
|
||||
| to any of the locales which will be supported by the application.
|
||||
|
|
||||
*/
|
||||
// Application timezone for back-end date functions.
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
// Default locale to use
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Fallback Locale
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The fallback locale determines the locale to use when the current one
|
||||
| is not available. You may change the value to correspond to any of
|
||||
| the language folders that are provided through your application.
|
||||
|
|
||||
*/
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is used by the Illuminate encrypter service and should be set
|
||||
| to a random, 32 character string, otherwise these encrypted strings
|
||||
| will not be safe. Please do this before deploying an application!
|
||||
|
|
||||
*/
|
||||
// Enable right-to-left text control.
|
||||
'rtl' => false,
|
||||
|
||||
// Auto-detect the locale for public users
|
||||
// For public users their locale can be guessed by headers sent by their
|
||||
// browser. This is usually set by users in their browser settings.
|
||||
// If not found the default app locale will be used.
|
||||
'auto_detect_locale' => env('APP_AUTO_LANG_PUBLIC', true),
|
||||
|
||||
// Encryption key
|
||||
'key' => env('APP_KEY', 'AbAZchsay4uBTU33RubBzLKw203yqSqr'),
|
||||
|
||||
// Encryption cipher
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Logging Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log settings for your application. Out of
|
||||
| the box, Laravel uses the Monolog PHP logging library. This gives
|
||||
| you a variety of powerful log handlers / formatters to utilize.
|
||||
|
|
||||
| Available Settings: "single", "daily", "syslog", "errorlog"
|
||||
|
|
||||
*/
|
||||
|
||||
// Logging configuration
|
||||
// Options: single, daily, syslog, errorlog
|
||||
'log' => env('APP_LOGGING', 'single'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Autoloaded Service Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The service providers listed here will be automatically loaded on the
|
||||
| request to your application. Feel free to add your own services to
|
||||
| this array to grant expanded functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
// Application Services Provides
|
||||
'providers' => [
|
||||
|
||||
/*
|
||||
* Laravel Framework Service Providers...
|
||||
*/
|
||||
// Laravel Framework Service Providers...
|
||||
Illuminate\Auth\AuthServiceProvider::class,
|
||||
Illuminate\Broadcasting\BroadcastServiceProvider::class,
|
||||
Illuminate\Bus\BusServiceProvider::class,
|
||||
@@ -136,34 +97,28 @@ return [
|
||||
Illuminate\Redis\RedisServiceProvider::class,
|
||||
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
|
||||
Illuminate\Session\SessionServiceProvider::class,
|
||||
Illuminate\Translation\TranslationServiceProvider::class,
|
||||
Illuminate\Validation\ValidationServiceProvider::class,
|
||||
Illuminate\View\ViewServiceProvider::class,
|
||||
Illuminate\Notifications\NotificationServiceProvider::class,
|
||||
SocialiteProviders\Manager\ServiceProvider::class,
|
||||
|
||||
/**
|
||||
* Third Party
|
||||
*/
|
||||
// Third party service providers
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
Barryvdh\DomPDF\ServiceProvider::class,
|
||||
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
|
||||
Barryvdh\Debugbar\ServiceProvider::class,
|
||||
Barryvdh\Snappy\ServiceProvider::class,
|
||||
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
*/
|
||||
// BookStack replacement service providers (Extends Laravel)
|
||||
BookStack\Providers\PaginationServiceProvider::class,
|
||||
BookStack\Providers\TranslationServiceProvider::class,
|
||||
|
||||
// BookStack custom service providers
|
||||
BookStack\Providers\AuthServiceProvider::class,
|
||||
BookStack\Providers\AppServiceProvider::class,
|
||||
BookStack\Providers\BroadcastServiceProvider::class,
|
||||
BookStack\Providers\EventServiceProvider::class,
|
||||
BookStack\Providers\RouteServiceProvider::class,
|
||||
BookStack\Providers\CustomFacadeProvider::class,
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -177,8 +132,10 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
// Class aliases, Registered on application start
|
||||
'aliases' => [
|
||||
|
||||
// Laravel
|
||||
'App' => Illuminate\Support\Facades\App::class,
|
||||
'Artisan' => Illuminate\Support\Facades\Artisan::class,
|
||||
'Auth' => Illuminate\Support\Facades\Auth::class,
|
||||
@@ -214,24 +171,20 @@ return [
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
|
||||
|
||||
/**
|
||||
* Third Party
|
||||
*/
|
||||
|
||||
// Third Party
|
||||
'ImageTool' => Intervention\Image\Facades\Image::class,
|
||||
'PDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'DomPDF' => Barryvdh\DomPDF\Facade::class,
|
||||
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
|
||||
'Debugbar' => Barryvdh\Debugbar\Facade::class,
|
||||
|
||||
/**
|
||||
* Custom
|
||||
*/
|
||||
|
||||
'Activity' => BookStack\Services\Facades\Activity::class,
|
||||
'Setting' => BookStack\Services\Facades\Setting::class,
|
||||
'Views' => BookStack\Services\Facades\Views::class,
|
||||
'Images' => \BookStack\Services\Facades\Images::class,
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Setting' => BookStack\Facades\Setting::class,
|
||||
'Views' => BookStack\Facades\Views::class,
|
||||
'Images' => BookStack\Facades\Images::class,
|
||||
|
||||
],
|
||||
|
||||
// Proxy configuration
|
||||
'proxies' => env('APP_PROXIES', ''),
|
||||
|
||||
];
|
||||
72
app/Config/auth.php
Normal file
72
app/Config/auth.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Authentication configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Method of authentication to use
|
||||
// Options: standard, ldap
|
||||
'method' => env('AUTH_METHOD', 'standard'),
|
||||
|
||||
// Authentication Defaults
|
||||
// This option controls the default authentication "guard" and password
|
||||
// reset options for your application.
|
||||
'defaults' => [
|
||||
'guard' => 'web',
|
||||
'passwords' => 'users',
|
||||
],
|
||||
|
||||
// Authentication Guards
|
||||
// All authentication drivers have a user provider. This defines how the
|
||||
// users are actually retrieved out of your database or other storage
|
||||
// mechanisms used by this application to persist your user's data.
|
||||
// Supported: "session", "token"
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'token',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
// User Providers
|
||||
// All authentication drivers have a user provider. This defines how the
|
||||
// users are actually retrieved out of your database or other storage
|
||||
// mechanisms used by this application to persist your user's data.
|
||||
// Supported: database, eloquent, ldap
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
// Resetting Passwords
|
||||
// The expire time is the number of minutes that the reset token should be
|
||||
// considered valid. This security feature keeps tokens short-lived so
|
||||
// they have less time to be guessed. You may change this as needed.
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'email' => 'emails.password',
|
||||
'table' => 'password_resets',
|
||||
'expire' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
43
app/Config/broadcasting.php
Normal file
43
app/Config/broadcasting.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Broadcasting configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Default Broadcaster
|
||||
// This option controls the default broadcaster that will be used by the
|
||||
// framework when an event needs to be broadcast. This can be set to
|
||||
// any of the connections defined in the "connections" array below.
|
||||
'default' => env('BROADCAST_DRIVER', 'pusher'),
|
||||
|
||||
// Broadcast Connections
|
||||
// Here you may define all of the broadcast connections that will be used
|
||||
// to broadcast events to other systems or over websockets. Samples of
|
||||
// each available type of connection are provided inside this array.
|
||||
'connections' => [
|
||||
|
||||
'pusher' => [
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_KEY'),
|
||||
'secret' => env('PUSHER_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,5 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Caching configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
// MEMCACHED - Split out configuration into an array
|
||||
if (env('CACHE_DRIVER') === 'memcached') {
|
||||
$memcachedServerKeys = ['host', 'port', 'weight'];
|
||||
@@ -14,30 +22,11 @@ if (env('CACHE_DRIVER') === 'memcached') {
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache connection that gets used while
|
||||
| using this caching library. This connection is used when another is
|
||||
| not explicitly specified when executing a given caching function.
|
||||
|
|
||||
*/
|
||||
|
||||
// Default cache store to use
|
||||
// Can be overridden at cache call-time
|
||||
'default' => env('CACHE_DRIVER', 'file'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
*/
|
||||
|
||||
// Available caches stores
|
||||
'stores' => [
|
||||
|
||||
'apc' => [
|
||||
@@ -71,17 +60,8 @@ return [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing a RAM based store such as APC or Memcached, there might
|
||||
| be other applications utilizing the same cache. So, we'll specify a
|
||||
| value to get prefixed to all our keys so we can avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
// Cache key prefix
|
||||
// Used to prevent collisions in shared cache systems.
|
||||
'prefix' => env('CACHE_PREFIX', 'bookstack'),
|
||||
|
||||
];
|
||||
127
app/Config/database.php
Normal file
127
app/Config/database.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Database configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
// REDIS
|
||||
// Split out configuration into an array
|
||||
if (env('REDIS_SERVERS', false)) {
|
||||
|
||||
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
|
||||
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
|
||||
$redisConfig = [];
|
||||
$cluster = count($redisServers) > 1;
|
||||
|
||||
if ($cluster) {
|
||||
$redisConfig['clusters'] = ['default' => []];
|
||||
}
|
||||
|
||||
foreach ($redisServers as $index => $redisServer) {
|
||||
$redisServerDetails = explode(':', $redisServer);
|
||||
|
||||
$serverConfig = [];
|
||||
$configIndex = 0;
|
||||
foreach ($redisDefaults as $configKey => $configDefault) {
|
||||
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
|
||||
$configIndex++;
|
||||
}
|
||||
|
||||
if ($cluster) {
|
||||
$redisConfig['clusters']['default'][] = $serverConfig;
|
||||
} else {
|
||||
$redisConfig['default'] = $serverConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MYSQL
|
||||
// Split out port from host if set
|
||||
$mysql_host = env('DB_HOST', 'localhost');
|
||||
$mysql_host_exploded = explode(':', $mysql_host);
|
||||
$mysql_port = env('DB_PORT', 3306);
|
||||
if (count($mysql_host_exploded) > 1) {
|
||||
$mysql_host = $mysql_host_exploded[0];
|
||||
$mysql_port = intval($mysql_host_exploded[1]);
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
// Default database connection name.
|
||||
// Options: mysql, mysql_testing
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
|
||||
// Available database connections
|
||||
// Many of those shown here are unsupported by BookStack.
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => storage_path('database.sqlite'),
|
||||
'prefix' => '',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => $mysql_host,
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'port' => $mysql_port,
|
||||
'charset' => 'utf8mb4',
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
'engine' => null,
|
||||
],
|
||||
|
||||
'mysql_testing' => [
|
||||
'driver' => 'mysql',
|
||||
'host' => '127.0.0.1',
|
||||
'database' => 'bookstack-test',
|
||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_unicode_ci',
|
||||
'prefix' => '',
|
||||
'strict' => false,
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
'schema' => 'public',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'database' => env('DB_DATABASE', 'forge'),
|
||||
'username' => env('DB_USERNAME', 'forge'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => 'utf8',
|
||||
'prefix' => '',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
// Migration Repository Table
|
||||
// This table keeps track of all the migrations that have already run for
|
||||
// your application. Using this information, we can determine which of
|
||||
// the migrations on disk haven't actually been run in the database.
|
||||
'migrations' => 'migrations',
|
||||
|
||||
// Redis configuration to use if set
|
||||
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
|
||||
|
||||
];
|
||||
132
app/Config/debugbar.php
Normal file
132
app/Config/debugbar.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Debugbar Configuration Options
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Debugbar is enabled by default, when debug is set to true in app.php.
|
||||
// You can override the value by setting enable to true or false instead of null.
|
||||
//
|
||||
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
||||
'except' => [
|
||||
'telescope*'
|
||||
],
|
||||
|
||||
|
||||
// DebugBar stores data for session/ajax requests.
|
||||
// You can disable this, so the debugbar stores data in headers/session,
|
||||
// but this can cause problems with large data collectors.
|
||||
// By default, file storage (in the storage folder) is used. Redis and PDO
|
||||
// can also be used. For PDO, run the package migrations first.
|
||||
'storage' => [
|
||||
'enabled' => true,
|
||||
'driver' => 'file', // redis, file, pdo, custom
|
||||
'path' => storage_path('debugbar'), // For file driver
|
||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
||||
'provider' => '' // Instance of StorageInterface for custom driver
|
||||
],
|
||||
|
||||
// Vendor files are included by default, but can be set to false.
|
||||
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
||||
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
||||
// and for js: jquery and and highlight.js
|
||||
// So if you want syntax highlighting, set it to true.
|
||||
// jQuery is set to not conflict with existing jQuery scripts.
|
||||
'include_vendors' => true,
|
||||
|
||||
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
||||
// you can use this option to disable sending the data through the headers.
|
||||
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||
|
||||
'capture_ajax' => true,
|
||||
'add_ajax_timing' => false,
|
||||
|
||||
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
||||
// in the Messages tab.
|
||||
'error_handler' => false,
|
||||
|
||||
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
||||
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
||||
'clockwork' => false,
|
||||
|
||||
// Enable/disable DataCollectors
|
||||
'collectors' => [
|
||||
'phpinfo' => true, // Php version
|
||||
'messages' => true, // Messages
|
||||
'time' => true, // Time Datalogger
|
||||
'memory' => true, // Memory usage
|
||||
'exceptions' => true, // Exception displayer
|
||||
'log' => true, // Logs from Monolog (merged in messages if enabled)
|
||||
'db' => true, // Show database (PDO) queries and bindings
|
||||
'views' => true, // Views with their data
|
||||
'route' => true, // Current route information
|
||||
'auth' => true, // Display Laravel authentication status
|
||||
'gate' => true, // Display Laravel Gate checks
|
||||
'session' => true, // Display session data
|
||||
'symfony_request' => true, // Only one can be enabled..
|
||||
'mail' => true, // Catch mail messages
|
||||
'laravel' => false, // Laravel version and environment
|
||||
'events' => false, // All events fired
|
||||
'default_request' => false, // Regular or special Symfony request logger
|
||||
'logs' => false, // Add the latest log messages
|
||||
'files' => false, // Show the included files
|
||||
'config' => false, // Display config settings
|
||||
'cache' => false, // Display cache events
|
||||
],
|
||||
|
||||
// Configure some DataCollectors
|
||||
'options' => [
|
||||
'auth' => [
|
||||
'show_name' => true, // Also show the users name/email in the debugbar
|
||||
],
|
||||
'db' => [
|
||||
'with_params' => true, // Render SQL with the parameters substituted
|
||||
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
||||
'timeline' => false, // Add the queries to the timeline
|
||||
'explain' => [ // Show EXPLAIN output on queries
|
||||
'enabled' => false,
|
||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||
],
|
||||
'hints' => true, // Show hints for common mistakes
|
||||
],
|
||||
'mail' => [
|
||||
'full_log' => false
|
||||
],
|
||||
'views' => [
|
||||
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
||||
],
|
||||
'route' => [
|
||||
'label' => true // show complete route on bar
|
||||
],
|
||||
'logs' => [
|
||||
'file' => null
|
||||
],
|
||||
'cache' => [
|
||||
'values' => true // collect cache values
|
||||
],
|
||||
],
|
||||
|
||||
// Inject Debugbar into the response
|
||||
// Usually, the debugbar is added just before </body>, by listening to the
|
||||
// Response after the App is done. If you disable this, you have to add them
|
||||
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
||||
'inject' => true,
|
||||
|
||||
// DebugBar route prefix
|
||||
// Sometimes you want to set route prefix to be used by DebugBar to load
|
||||
// its resources from. Usually the need comes from misconfigured web server or
|
||||
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
||||
'route_prefix' => '_debugbar',
|
||||
|
||||
// DebugBar route domain
|
||||
// By default DebugBar route served from the same domain that request served.
|
||||
// To override default domain, specify it as a non-empty value.
|
||||
'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
||||
];
|
||||
@@ -1,16 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DOMPDF configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set some default values. It is possible to add all defines that can be set
|
||||
| in dompdf_config.inc.php. You can also override the entire config file.
|
||||
|
|
||||
*/
|
||||
|
||||
'show_warnings' => false, // Throw an Exception on warnings from dompdf
|
||||
'orientation' => 'portrait',
|
||||
'defines' => [
|
||||
74
app/Config/filesystems.php
Normal file
74
app/Config/filesystems.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Filesystem configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Default Filesystem Disk
|
||||
// Options: local, local_secure, s3
|
||||
'default' => env('STORAGE_TYPE', 'local'),
|
||||
|
||||
// Filesystem to use specifically for image uploads.
|
||||
'images' => env('STORAGE_IMAGE_TYPE', env('STORAGE_TYPE', 'local')),
|
||||
|
||||
// Filesystem to use specifically for file attachments.
|
||||
'attachments' => env('STORAGE_ATTACHMENT_TYPE', env('STORAGE_TYPE', 'local')),
|
||||
|
||||
// Storage URL
|
||||
// This is the url to where the storage is located for when using an external
|
||||
// file storage service, such as s3, to store publicly accessible assets.
|
||||
'url' => env('STORAGE_URL', false),
|
||||
|
||||
// Default Cloud Filesystem Disk
|
||||
'cloud' => 's3',
|
||||
|
||||
// Available filesystem disks
|
||||
// Only local, local_secure & s3 are supported by BookStack
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => public_path(),
|
||||
],
|
||||
|
||||
'local_secure' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path(),
|
||||
],
|
||||
|
||||
'ftp' => [
|
||||
'driver' => 'ftp',
|
||||
'host' => 'ftp.example.com',
|
||||
'username' => 'your-username',
|
||||
'password' => 'your-password',
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
||||
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
||||
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
||||
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
||||
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
||||
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
||||
],
|
||||
|
||||
'rackspace' => [
|
||||
'driver' => 'rackspace',
|
||||
'username' => 'your-username',
|
||||
'key' => 'your-key',
|
||||
'container' => 'your-container',
|
||||
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
|
||||
'region' => 'IAD',
|
||||
'url_type' => 'publicURL',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
49
app/Config/mail.php
Normal file
49
app/Config/mail.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Mail configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Mail driver to use.
|
||||
// Options: smtp, mail, sendmail, log
|
||||
'driver' => env('MAIL_DRIVER', 'smtp'),
|
||||
|
||||
// SMTP host address
|
||||
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
||||
|
||||
// SMTP host port
|
||||
'port' => env('MAIL_PORT', 587),
|
||||
|
||||
// Global "From" address & name
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
|
||||
'name' => env('MAIL_FROM_NAME','BookStack')
|
||||
],
|
||||
|
||||
// Email encryption protocol
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
|
||||
// SMTP server username
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
|
||||
// SMTP server password
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
|
||||
// Sendmail application path
|
||||
'sendmail' => '/usr/sbin/sendmail -bs',
|
||||
|
||||
// Email markdown configuration
|
||||
'markdown' => [
|
||||
'theme' => 'default',
|
||||
'paths' => [
|
||||
resource_path('views/vendor/mail'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
69
app/Config/queue.php
Normal file
69
app/Config/queue.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Queue configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Default driver to use for the queue
|
||||
// Options: null, sync, redis
|
||||
'default' => env('QUEUE_DRIVER', 'sync'),
|
||||
|
||||
// Queue connection configuration
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'expire' => 60,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => 'localhost',
|
||||
'queue' => 'default',
|
||||
'ttr' => 60,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => 'your-public-key',
|
||||
'secret' => 'your-secret-key',
|
||||
'queue' => 'your-queue-url',
|
||||
'region' => 'us-east-1',
|
||||
],
|
||||
|
||||
'iron' => [
|
||||
'driver' => 'iron',
|
||||
'host' => 'mq-aws-us-east-1.iron.io',
|
||||
'token' => 'your-token',
|
||||
'project' => 'your-project-id',
|
||||
'queue' => 'your-queue-name',
|
||||
'encrypt' => true,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'queue' => 'default',
|
||||
'expire' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
// Failed queue job logging
|
||||
'failed' => [
|
||||
'database' => 'mysql', 'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
152
app/Config/services.php
Normal file
152
app/Config/services.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Third party service configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Single option to disable non-auth external services such as Gravatar and Draw.io
|
||||
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
|
||||
|
||||
// Draw.io integration active
|
||||
'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)),
|
||||
|
||||
// URL for fetching avatars
|
||||
'avatar_url' => env('AVATAR_URL', ''),
|
||||
|
||||
// Callback URL for social authentication methods
|
||||
'callback_url' => env('APP_URL', false),
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => '',
|
||||
'secret' => '',
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
'region' => 'us-east-1',
|
||||
],
|
||||
|
||||
'stripe' => [
|
||||
'model' => \BookStack\Auth\User::class,
|
||||
'key' => '',
|
||||
'secret' => '',
|
||||
],
|
||||
|
||||
'github' => [
|
||||
'client_id' => env('GITHUB_APP_ID', false),
|
||||
'client_secret' => env('GITHUB_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/github/callback',
|
||||
'name' => 'GitHub',
|
||||
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_APP_ID', false),
|
||||
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
||||
'name' => 'Google',
|
||||
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
||||
'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'client_id' => env('SLACK_APP_ID', false),
|
||||
'client_secret' => env('SLACK_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/slack/callback',
|
||||
'name' => 'Slack',
|
||||
'auto_register' => env('SLACK_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'facebook' => [
|
||||
'client_id' => env('FACEBOOK_APP_ID', false),
|
||||
'client_secret' => env('FACEBOOK_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
|
||||
'name' => 'Facebook',
|
||||
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'twitter' => [
|
||||
'client_id' => env('TWITTER_APP_ID', false),
|
||||
'client_secret' => env('TWITTER_APP_SECRET', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
|
||||
'name' => 'Twitter',
|
||||
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'azure' => [
|
||||
'client_id' => env('AZURE_APP_ID', false),
|
||||
'client_secret' => env('AZURE_APP_SECRET', false),
|
||||
'tenant' => env('AZURE_TENANT', false),
|
||||
'redirect' => env('APP_URL') . '/login/service/azure/callback',
|
||||
'name' => 'Microsoft Azure',
|
||||
'auto_register' => env('AZURE_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'okta' => [
|
||||
'client_id' => env('OKTA_APP_ID'),
|
||||
'client_secret' => env('OKTA_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
||||
'base_url' => env('OKTA_BASE_URL'),
|
||||
'name' => 'Okta',
|
||||
'auto_register' => env('OKTA_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'gitlab' => [
|
||||
'client_id' => env('GITLAB_APP_ID'),
|
||||
'client_secret' => env('GITLAB_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
|
||||
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
|
||||
'name' => 'GitLab',
|
||||
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'twitch' => [
|
||||
'client_id' => env('TWITCH_APP_ID'),
|
||||
'client_secret' => env('TWITCH_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
||||
'name' => 'Twitch',
|
||||
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'discord' => [
|
||||
'client_id' => env('DISCORD_APP_ID'),
|
||||
'client_secret' => env('DISCORD_APP_SECRET'),
|
||||
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
||||
'name' => 'Discord',
|
||||
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
|
||||
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
||||
],
|
||||
|
||||
'ldap' => [
|
||||
'server' => env('LDAP_SERVER', false),
|
||||
'dn' => env('LDAP_DN', false),
|
||||
'pass' => env('LDAP_PASS', false),
|
||||
'base_dn' => env('LDAP_BASE_DN', false),
|
||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
||||
'version' => env('LDAP_VERSION', false),
|
||||
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
||||
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
|
||||
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
||||
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
|
||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
]
|
||||
|
||||
];
|
||||
80
app/Config/session.php
Normal file
80
app/Config/session.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Session configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
// Default session driver
|
||||
// Options: file, cookie, database, redis, memcached, array
|
||||
'driver' => env('SESSION_DRIVER', 'file'),
|
||||
|
||||
// Session lifetime, in minutes
|
||||
'lifetime' => env('SESSION_LIFETIME', 120),
|
||||
|
||||
// Expire session on browser close
|
||||
'expire_on_close' => false,
|
||||
|
||||
// Encrypt session data
|
||||
'encrypt' => false,
|
||||
|
||||
// Location to store session files
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
// Session Database Connection
|
||||
// When using the "database" or "redis" session drivers, you can specify a
|
||||
// connection that should be used to manage these sessions. This should
|
||||
// correspond to a connection in your database configuration options.
|
||||
'connection' => null,
|
||||
|
||||
// Session database table, if database driver is in use
|
||||
'table' => 'sessions',
|
||||
|
||||
// Session Sweeping Lottery
|
||||
// Some session drivers must manually sweep their storage location to get
|
||||
// rid of old sessions from storage. Here are the chances that it will
|
||||
// happen on a given request. By default, the odds are 2 out of 100.
|
||||
'lottery' => [2, 100],
|
||||
|
||||
|
||||
// Session Cookie Name
|
||||
// Here you may change the name of the cookie used to identify a session
|
||||
// instance by ID. The name specified here will get used every time a
|
||||
// new session cookie is created by the framework for every driver.
|
||||
'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
|
||||
|
||||
// Session Cookie Path
|
||||
// The session cookie path determines the path for which the cookie will
|
||||
// be regarded as available. Typically, this will be the root path of
|
||||
// your application but you are free to change this when necessary.
|
||||
'path' => '/',
|
||||
|
||||
// Session Cookie Domain
|
||||
// Here you may change the domain of the cookie used to identify a session
|
||||
// in your application. This will determine which domains the cookie is
|
||||
// available to in your application. A sensible default has been set.
|
||||
'domain' => env('SESSION_DOMAIN', null),
|
||||
|
||||
// HTTPS Only Cookies
|
||||
// By setting this option to true, session cookies will only be sent back
|
||||
// to the server if the browser has a HTTPS connection. This will keep
|
||||
// the cookie from being sent to you if it can not be done securely.
|
||||
'secure' => env('SESSION_SECURE_COOKIE', false),
|
||||
|
||||
// HTTP Access Only
|
||||
// Setting this value to true will prevent JavaScript from accessing the
|
||||
// value of the cookie and the cookie will only be accessible through the HTTP protocol.
|
||||
'http_only' => true,
|
||||
|
||||
// Same-Site Cookies
|
||||
// This option determines how your cookies behave when cross-site requests
|
||||
// take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
// do not enable this as other CSRF protection services are in place.
|
||||
// Options: lax, strict
|
||||
'same_site' => null,
|
||||
];
|
||||
22
app/Config/setting-defaults.php
Normal file
22
app/Config/setting-defaults.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Default system settings.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'app-name' => 'BookStack',
|
||||
'app-logo' => '',
|
||||
'app-name-header' => true,
|
||||
'app-editor' => 'wysiwyg',
|
||||
'app-color' => '#206ea7',
|
||||
'app-color-light' => 'rgba(32,110,167,0.15)',
|
||||
'app-custom-head' => false,
|
||||
'registration-enabled' => false,
|
||||
|
||||
];
|
||||
@@ -1,5 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SnappyPDF configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
return [
|
||||
'pdf' => [
|
||||
'enabled' => true,
|
||||
37
app/Config/view.php
Normal file
37
app/Config/view.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* View configuration options.
|
||||
*
|
||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
||||
* Configuration should be altered via the `.env` file or environment variables.
|
||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
||||
*/
|
||||
|
||||
// Join up possible view locations
|
||||
$viewPaths = [realpath(base_path('resources/views'))];
|
||||
if ($theme = env('APP_THEME', false)) {
|
||||
array_unshift($viewPaths, base_path('themes/' . $theme));
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
// App theme
|
||||
// This option defines the theme to use for the application. When a theme
|
||||
// is set there must be a `themes/<theme_name>` folder to hold the
|
||||
// custom theme overrides.
|
||||
'theme' => env('APP_THEME', false),
|
||||
|
||||
// View Storage Paths
|
||||
// Most templating systems load templates from disk. Here you may specify
|
||||
// an array of paths that should be checked for your views. Of course
|
||||
// the usual Laravel view path has already been registered for you.
|
||||
'paths' => $viewPaths,
|
||||
|
||||
// Compiled View Path
|
||||
// This option determines where all the compiled Blade templates will be
|
||||
// stored for your application. Typically, this is within the storage
|
||||
// directory. However, as usual, you are free to change this value.
|
||||
'compiled' => realpath(storage_path('framework/views')),
|
||||
|
||||
];
|
||||
85
app/Console/Commands/CleanupImages.php
Normal file
85
app/Console/Commands/CleanupImages.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class CleanupImages extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'bookstack:cleanup-images
|
||||
{--a|all : Include images that are used in page revisions}
|
||||
{--f|force : Actually run the deletions}
|
||||
';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Cleanup images and drawings';
|
||||
|
||||
|
||||
protected $imageService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
* @param \BookStack\Uploads\ImageService $imageService
|
||||
*/
|
||||
public function __construct(ImageService $imageService)
|
||||
{
|
||||
$this->imageService = $imageService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$checkRevisions = $this->option('all') ? false : true;
|
||||
$dryRun = $this->option('force') ? false : true;
|
||||
|
||||
if (!$dryRun) {
|
||||
$proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
|
||||
if (!$proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
||||
$deleteCount = count($deleted);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->comment('Dry run, No images have been deleted');
|
||||
$this->comment($deleteCount . ' images found that would have been deleted');
|
||||
$this->showDeletedImages($deleted);
|
||||
$this->comment('Run with -f or --force to perform deletions');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->showDeletedImages($deleted);
|
||||
$this->comment($deleteCount . ' images deleted');
|
||||
}
|
||||
|
||||
protected function showDeletedImages($paths)
|
||||
{
|
||||
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
|
||||
return;
|
||||
}
|
||||
if (count($paths) > 0) {
|
||||
$this->line('Images to delete:');
|
||||
}
|
||||
foreach ($paths as $path) {
|
||||
$this->line($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Actions\Activity;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearActivity extends Command
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\PageRevision;
|
||||
use BookStack\Entities\PageRevision;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearRevisions extends Command
|
||||
|
||||
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\Auth\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 (mb_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 (mb_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 (mb_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->downloadAndAssignUserAvatar($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\Auth\User;
|
||||
use BookStack\Auth\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...');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
@@ -31,7 +31,7 @@ class RegeneratePermissions extends Command
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Entities\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
@@ -19,14 +19,14 @@ class RegenerateSearch extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
protected $description = 'Re-index all content for searching';
|
||||
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
* @param \BookStack\Entities\SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
|
||||
@@ -11,12 +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,
|
||||
Commands\UpgradeDatabaseEncoding::class
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -29,4 +24,14 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
}
|
||||
}
|
||||
|
||||
118
app/Entities/Book.php
Normal file
118
app/Entities/Book.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
public $searchFactor = 2;
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Book';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
if ($path !== false) {
|
||||
return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return url('/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 = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
if (!$this->image_id) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cover = $this->cover ? url($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 all pages within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->hasMany(Page::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct child pages of this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function directPages()
|
||||
{
|
||||
return $this->pages()->where('chapter_id', '=', '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all chapters within this book.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function chapters()
|
||||
{
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shelves this book is contained within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function shelves()
|
||||
{
|
||||
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
}
|
||||
107
app/Entities/Bookshelf.php
Normal file
107
app/Entities/Bookshelf.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
|
||||
class Bookshelf extends Entity
|
||||
{
|
||||
protected $table = 'bookshelves';
|
||||
|
||||
public $searchFactor = 3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Bookshelf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the books in this shelf.
|
||||
* Should not be used directly since does not take into account permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function books()
|
||||
{
|
||||
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')
|
||||
->withPivot('order')
|
||||
->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this bookshelf.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
if ($path !== false) {
|
||||
return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return url('/shelves/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
{
|
||||
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
|
||||
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
if (!$this->image_id) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the shelf
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function cover()
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this shelf contains the given book.
|
||||
* @param Book $book
|
||||
* @return bool
|
||||
*/
|
||||
public function contains(Book $book)
|
||||
{
|
||||
return $this->books()->where('id', '=', $book->id)->count() > 0;
|
||||
}
|
||||
}
|
||||
34
app/Entities/BreadcrumbsViewComposer.php
Normal file
34
app/Entities/BreadcrumbsViewComposer.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use Illuminate\View\View;
|
||||
|
||||
class BreadcrumbsViewComposer
|
||||
{
|
||||
|
||||
protected $entityContextManager;
|
||||
|
||||
/**
|
||||
* BreadcrumbsViewComposer constructor.
|
||||
* @param EntityContextManager $entityContextManager
|
||||
*/
|
||||
public function __construct(EntityContextManager $entityContextManager)
|
||||
{
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify data when the view is composed.
|
||||
* @param View $view
|
||||
*/
|
||||
public function compose(View $view)
|
||||
{
|
||||
$crumbs = $view->getData()['crumbs'];
|
||||
if (array_first($crumbs) instanceof Book) {
|
||||
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
|
||||
if ($shelf) {
|
||||
array_unshift($crumbs, $shelf);
|
||||
$view->with('crumbs', $crumbs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
class Chapter extends Entity
|
||||
{
|
||||
public $searchFactor = 1.3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
protected $with = ['book'];
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Chapter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
@@ -34,10 +42,13 @@ class Chapter extends Entity
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||
$fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
|
||||
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
$fullPath .= '/' . trim($path, '/');
|
||||
}
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
|
||||
|
||||
return url($fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,10 +56,10 @@ class Chapter extends Entity
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
$description = $this->text ?? $this->description;
|
||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,4 +71,12 @@ 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this chapter has any child pages.
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren()
|
||||
{
|
||||
return count($this->pages) > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,55 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Actions\Activity;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
/**
|
||||
* Class Entity
|
||||
* The base class for book-like items such as pages, chapters & books.
|
||||
* This is not a database model in itself but extended.
|
||||
*
|
||||
* @property integer $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property boolean $restricted
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* Set here since, due to folder changes, the namespace used
|
||||
* in the database no longer matches the class namespace.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Entity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -28,7 +70,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;
|
||||
@@ -58,6 +102,11 @@ class Entity extends Ownable
|
||||
return $this->morphMany(View::class, 'viewable');
|
||||
}
|
||||
|
||||
public function viewCountQuery()
|
||||
{
|
||||
return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Tag models that have been user assigned to this entity.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
@@ -143,13 +192,13 @@ class Entity extends Ownable
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
return app('BookStack\\Entities\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,8 +208,10 @@ class Entity extends Ownable
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if (strlen($this->name) <= $length) return $this->name;
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
if (mb_strlen($this->name) <= $length) {
|
||||
return $this->name;
|
||||
}
|
||||
return mb_substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,17 +223,36 @@ class Entity extends Ownable
|
||||
return $this->{$this->textField};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this entity's descriptive content to the specified length.
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public function getExcerpt(int $length = 100)
|
||||
{
|
||||
$text = $this->getText();
|
||||
if (mb_strlen($text) > $length) {
|
||||
$text = mb_substr($text, 0, $length-3) . '...';
|
||||
}
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
60
app/Entities/EntityContextManager.php
Normal file
60
app/Entities/EntityContextManager.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use Illuminate\Session\Store;
|
||||
|
||||
class EntityContextManager
|
||||
{
|
||||
protected $session;
|
||||
protected $entityRepo;
|
||||
|
||||
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
|
||||
|
||||
/**
|
||||
* EntityContextManager constructor.
|
||||
* @param Store $session
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
public function __construct(Store $session, EntityRepo $entityRepo)
|
||||
{
|
||||
$this->session = $session;
|
||||
$this->entityRepo = $entityRepo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current bookshelf context for the given book.
|
||||
* @param Book $book
|
||||
* @return Bookshelf|null
|
||||
*/
|
||||
public function getContextualShelfForBook(Book $book)
|
||||
{
|
||||
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
|
||||
if (is_int($contextBookshelfId)) {
|
||||
|
||||
/** @var Bookshelf $shelf */
|
||||
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
|
||||
|
||||
if ($shelf && $shelf->contains($book)) {
|
||||
return $shelf;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the current contextual shelf ID.
|
||||
* @param int $shelfId
|
||||
*/
|
||||
public function setShelfContext(int $shelfId)
|
||||
{
|
||||
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the session stored shelf context id.
|
||||
*/
|
||||
public function clearShelfContext()
|
||||
{
|
||||
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
|
||||
}
|
||||
}
|
||||
106
app/Entities/EntityProvider.php
Normal file
106
app/Entities/EntityProvider.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
/**
|
||||
* Class EntityProvider
|
||||
*
|
||||
* Provides access to the core entity models.
|
||||
* Wrapped up in this provider since they are often used together
|
||||
* so this is a neater alternative to injecting all in individually.
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class EntityProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Bookshelf
|
||||
*/
|
||||
public $bookshelf;
|
||||
|
||||
/**
|
||||
* @var Book
|
||||
*/
|
||||
public $book;
|
||||
|
||||
/**
|
||||
* @var Chapter
|
||||
*/
|
||||
public $chapter;
|
||||
|
||||
/**
|
||||
* @var Page
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var PageRevision
|
||||
*/
|
||||
public $pageRevision;
|
||||
|
||||
/**
|
||||
* EntityProvider constructor.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(
|
||||
Bookshelf $bookshelf,
|
||||
Book $book,
|
||||
Chapter $chapter,
|
||||
Page $page,
|
||||
PageRevision $pageRevision
|
||||
) {
|
||||
$this->bookshelf = $bookshelf;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all core entity types as an associated array
|
||||
* with their basic names as the keys.
|
||||
* @return Entity[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return [
|
||||
'bookshelf' => $this->bookshelf,
|
||||
'book' => $this->book,
|
||||
'chapter' => $this->chapter,
|
||||
'page' => $this->page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance by it's basic name.
|
||||
* @param string $type
|
||||
* @return Entity
|
||||
*/
|
||||
public function get(string $type)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
return $this->all()[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the morph classes, as an array, for a single or multiple types.
|
||||
* @param string|array $types
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getMorphClasses($types)
|
||||
{
|
||||
if (is_string($types)) {
|
||||
$types = [$types];
|
||||
}
|
||||
|
||||
$morphClasses = [];
|
||||
foreach ($types as $type) {
|
||||
$model = $this->get($type);
|
||||
$morphClasses[] = $model->getMorphClass();
|
||||
}
|
||||
return $morphClasses;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,31 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
protected $imageService;
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
* @param $entityRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ImageService $imageService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo)
|
||||
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->imageService = $imageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
* @param Page $page
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pageToContainedHtml(Page $page)
|
||||
{
|
||||
@@ -36,13 +38,14 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter to a self-contained HTML file.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\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 +59,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,6 +75,7 @@ class ExportService
|
||||
* Convert a page to a PDF file.
|
||||
* @param Page $page
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function pageToPdf(Page $page)
|
||||
{
|
||||
@@ -83,13 +88,14 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter to a PDF file.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\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', [
|
||||
@@ -101,8 +107,9 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @param \BookStack\Entities\Book $book
|
||||
* @return string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function bookToPdf(Book $book)
|
||||
{
|
||||
@@ -118,6 +125,7 @@ class ExportService
|
||||
* Convert normal webpage HTML to a PDF.
|
||||
* @param $html
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function htmlToPdf($html)
|
||||
{
|
||||
@@ -127,7 +135,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 +144,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 +154,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter into a plain text string.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return string
|
||||
*/
|
||||
public function chapterToPlainText(Chapter $chapter)
|
||||
@@ -238,17 +238,4 @@ class ExportService
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Attachment;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
@@ -7,9 +8,17 @@ class Page extends Entity
|
||||
|
||||
protected $simpleAttributes = ['name', 'id', 'slug'];
|
||||
|
||||
protected $with = ['book'];
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Page';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
@@ -30,6 +39,15 @@ class Page extends Entity
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent item
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->chapter_id ? $this->chapter() : $this->book();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
@@ -78,21 +96,10 @@ class Page extends Entity
|
||||
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
|
||||
|
||||
if ($path !== false) {
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
|
||||
return url('/books/' . urlencode($bookSlug) . $midText . $idComponent . '/' . trim($path, '/'));
|
||||
}
|
||||
|
||||
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this page's content to the specified length.
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
|
||||
return mb_convert_encoding($text, 'UTF-8');
|
||||
return url('/books/' . urlencode($bookSlug) . $midText . $idComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,8 +108,17 @@ class Page extends Entity
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery($withContent = false)
|
||||
{ $htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
{
|
||||
$htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing
|
||||
* @return \BookStack\Entities\PageRevision|null
|
||||
*/
|
||||
public function getCurrentRevision()
|
||||
{
|
||||
return $this->revisions()->first();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
@@ -31,7 +33,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;
|
||||
}
|
||||
|
||||
@@ -58,5 +62,4 @@ class PageRevision extends Model
|
||||
{
|
||||
return $type === 'revision';
|
||||
}
|
||||
|
||||
}
|
||||
924
app/Entities/Repos/EntityRepo.php
Normal file
924
app/Entities/Repos/EntityRepo.php
Normal file
@@ -0,0 +1,924 @@
|
||||
<?php namespace BookStack\Entities\Repos;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Actions\TagRepo;
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Entities\SearchService;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use DOMXPath;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Throwable;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* @var ViewService
|
||||
*/
|
||||
protected $viewService;
|
||||
|
||||
/**
|
||||
* @var TagRepo
|
||||
*/
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* @var SearchService
|
||||
*/
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* EntityRepo constructor.
|
||||
* @param EntityProvider $entityProvider
|
||||
* @param ViewService $viewService
|
||||
* @param PermissionService $permissionService
|
||||
* @param TagRepo $tagRepo
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
EntityProvider $entityProvider,
|
||||
ViewService $viewService,
|
||||
PermissionService $permissionService,
|
||||
TagRepo $tagRepo,
|
||||
SearchService $searchService
|
||||
) {
|
||||
$this->entityProvider = $entityProvider;
|
||||
$this->viewService = $viewService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->tagRepo = $tagRepo;
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for searching entities via permission system
|
||||
* @param string $type
|
||||
* @param bool $allowDrafts
|
||||
* @param string $permission
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
|
||||
{
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
|
||||
if (strtolower($type) === 'page' && !$allowDrafts) {
|
||||
$q = $q->where('draft', '=', false);
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity with the given id exists.
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function exists($type, $id)
|
||||
{
|
||||
return $this->entityQuery($type)->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity by ID
|
||||
* @param string $type
|
||||
* @param integer $id
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return Entity
|
||||
*/
|
||||
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
$query = $this->entityQuery($type, $allowDrafts);
|
||||
|
||||
if ($ignorePermissions) {
|
||||
$query = $this->entityProvider->get($type)->newQuery();
|
||||
}
|
||||
|
||||
return $query->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param []int $ids
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
|
||||
*/
|
||||
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
$query = $this->entityQuery($type, $allowDrafts);
|
||||
|
||||
if ($ignorePermissions) {
|
||||
$query = $this->entityProvider->get($type)->newQuery();
|
||||
}
|
||||
|
||||
return $query->whereIn('id', $ids)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity by its url slug.
|
||||
* @param string $type
|
||||
* @param string $slug
|
||||
* @param string|bool $bookSlug
|
||||
* @return Entity
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($type, $slug, $bookSlug = false)
|
||||
{
|
||||
$q = $this->entityQuery($type)->where('slug', '=', $slug);
|
||||
|
||||
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
|
||||
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
|
||||
$query->select('id')
|
||||
->from($this->entityProvider->book->getTable())
|
||||
->where('slug', '=', $bookSlug)->limit(1);
|
||||
});
|
||||
}
|
||||
$entity = $q->first();
|
||||
if ($entity === null) {
|
||||
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
|
||||
}
|
||||
return $entity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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, $permission = 'view')
|
||||
{
|
||||
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
|
||||
if ($count !== false) {
|
||||
$q = $q->take($count);
|
||||
}
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities in a paginated format
|
||||
* @param $type
|
||||
* @param int $count
|
||||
* @param string $sort
|
||||
* @param string $order
|
||||
* @param null|callable $queryAddition
|
||||
* @return LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
|
||||
{
|
||||
$query = $this->entityQuery($type);
|
||||
$query = $this->addSortToQuery($query, $sort, $order);
|
||||
if ($queryAddition) {
|
||||
$queryAddition($query);
|
||||
}
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sorting operations to an entity query.
|
||||
* @param Builder $query
|
||||
* @param string $sort
|
||||
* @param string $order
|
||||
* @return Builder
|
||||
*/
|
||||
protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
|
||||
{
|
||||
$order = ($order === 'asc') ? 'asc' : 'desc';
|
||||
$propertySorts = ['name', 'created_at', 'updated_at'];
|
||||
|
||||
if (in_array($sort, $propertySorts)) {
|
||||
return $query->orderBy($sort, $order);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently created entities of the given type.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
|
||||
->orderBy('created_at', 'desc');
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated entities of the given type.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
|
||||
->orderBy('updated_at', 'desc');
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently viewed entities.
|
||||
* @param string|bool $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyViewed($type, $count = 10, $page = 0)
|
||||
{
|
||||
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
|
||||
return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system with pagination.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($type, $count = 20)
|
||||
{
|
||||
return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system with pagination.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($type, $count = 20)
|
||||
{
|
||||
return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most popular entities base on all views.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPopular(string $type, int $count = 10, int $page = 0)
|
||||
{
|
||||
return $this->viewService->getPopular($count, $page, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft pages owned by the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return Collection
|
||||
*/
|
||||
public function getUserDraftPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->entityProvider->page->where('draft', '=', true)
|
||||
->where('created_by', '=', user()->id)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of entities the given user has created.
|
||||
* @param string $type
|
||||
* @param User $user
|
||||
* @return int
|
||||
*/
|
||||
public function getUserTotalCreated(string $type, User $user)
|
||||
{
|
||||
return $this->entityProvider->get($type)
|
||||
->where('created_by', '=', $user->id)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getBookshelfChildren(Bookshelf $bookshelf)
|
||||
{
|
||||
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct children of a book.
|
||||
* @param Book $book
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getBookDirectChildren(Book $book)
|
||||
{
|
||||
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
|
||||
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
|
||||
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child objects of a book.
|
||||
* Returns a sorted collection of Pages and Chapters.
|
||||
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
||||
* @param Book $book
|
||||
* @param bool $filterDrafts
|
||||
* @param bool $renderPages
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
|
||||
{
|
||||
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
|
||||
$entities = [];
|
||||
$parents = [];
|
||||
$tree = [];
|
||||
|
||||
foreach ($q as $index => $rawEntity) {
|
||||
if ($rawEntity->entity_type === $this->entityProvider->page->getMorphClass()) {
|
||||
$entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
|
||||
if ($renderPages) {
|
||||
$entities[$index]->html = $rawEntity->html;
|
||||
$entities[$index]->html = $this->renderPage($entities[$index]);
|
||||
};
|
||||
} else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
|
||||
$entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
|
||||
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
|
||||
$parents[$key] = $entities[$index];
|
||||
$parents[$key]->setAttribute('pages', collect());
|
||||
}
|
||||
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;
|
||||
}
|
||||
$parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
|
||||
if (!isset($parents[$parentKey])) {
|
||||
$tree[] = $entity;
|
||||
continue;
|
||||
}
|
||||
$chapter = $parents[$parentKey];
|
||||
$chapter->pages->push($entity);
|
||||
}
|
||||
|
||||
return collect($tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param Chapter $chapter
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getChapterChildren(Chapter $chapter)
|
||||
{
|
||||
return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
|
||||
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the next sequential priority for a new child element in the given book.
|
||||
* @param Book $book
|
||||
* @return int
|
||||
*/
|
||||
public function getNewBookPriority(Book $book)
|
||||
{
|
||||
$lastElem = $this->getBookChildren($book)->pop();
|
||||
return $lastElem ? $lastElem->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority for a new page to be added to the given chapter.
|
||||
* @param Chapter $chapter
|
||||
* @return int
|
||||
*/
|
||||
public function getNewChapterPriority(Chapter $chapter)
|
||||
{
|
||||
$lastPage = $chapter->pages('DESC')->first();
|
||||
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a suitable slug for an entity.
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param bool|integer $currentId
|
||||
* @param bool|integer $bookId Only pass if type is not a book
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
|
||||
{
|
||||
$slug = $this->nameToSlug($name);
|
||||
while ($this->slugExists($type, $slug, $currentId, $bookId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slug already exists in the database.
|
||||
* @param string $type
|
||||
* @param string $slug
|
||||
* @param bool|integer $currentId
|
||||
* @param bool|integer $bookId
|
||||
* @return bool
|
||||
*/
|
||||
protected function slugExists($type, $slug, $currentId = false, $bookId = false)
|
||||
{
|
||||
$query = $this->entityProvider->get($type)->where('slug', '=', $slug);
|
||||
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
|
||||
$query = $query->where('book_id', '=', $bookId);
|
||||
}
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity restrictions from a request
|
||||
* @param Request $request
|
||||
* @param Entity $entity
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->get('restricted', '') === 'true';
|
||||
$entity->permissions()->delete();
|
||||
|
||||
if ($request->filled('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->permissions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new entity from request input.
|
||||
* Used for books and chapters.
|
||||
* @param string $type
|
||||
* @param array $input
|
||||
* @param bool|Book $book
|
||||
* @return Entity
|
||||
*/
|
||||
public function createFromInput($type, $input = [], $book = false)
|
||||
{
|
||||
$isChapter = strtolower($type) === 'chapter';
|
||||
$entityModel = $this->entityProvider->get($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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity details from request input.
|
||||
* Used for books and chapters
|
||||
* @param string $type
|
||||
* @param Entity $entityModel
|
||||
* @param array $input
|
||||
* @return Entity
|
||||
*/
|
||||
public function updateFromInput($type, Entity $entityModel, $input = [])
|
||||
{
|
||||
if ($entityModel->name !== $input['name']) {
|
||||
$entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
|
||||
}
|
||||
$entityModel->fill($input);
|
||||
$entityModel->updated_by = user()->id;
|
||||
$entityModel->save();
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the books assigned to a shelf from a comma-separated list
|
||||
* of book IDs.
|
||||
* @param Bookshelf $shelf
|
||||
* @param string $books
|
||||
*/
|
||||
public function updateShelfBooks(Bookshelf $shelf, string $books)
|
||||
{
|
||||
$ids = explode(',', $books);
|
||||
|
||||
// Check books exist and match ordering
|
||||
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
|
||||
$syncData = [];
|
||||
foreach ($ids as $index => $id) {
|
||||
if ($bookIds->contains($id)) {
|
||||
$syncData[$id] = ['order' => $index];
|
||||
}
|
||||
}
|
||||
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a Book to a BookShelf.
|
||||
* @param Bookshelf $shelf
|
||||
* @param Book $book
|
||||
*/
|
||||
public function appendBookToShelf(Bookshelf $shelf, Book $book)
|
||||
{
|
||||
if ($shelf->contains($book)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$maxOrder = $shelf->books()->max('order');
|
||||
$shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the book that an entity belongs to.
|
||||
* @param string $type
|
||||
* @param integer $newBookId
|
||||
* @param Entity $entity
|
||||
* @param bool $rebuildPermissions
|
||||
* @return Entity
|
||||
*/
|
||||
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
|
||||
{
|
||||
$entity->book_id = $newBookId;
|
||||
// Update related activity
|
||||
foreach ($entity->activity as $activity) {
|
||||
$activity->book_id = $newBookId;
|
||||
$activity->save();
|
||||
}
|
||||
$entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
|
||||
$entity->save();
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if (strtolower($type) === 'chapter') {
|
||||
foreach ($entity->pages as $page) {
|
||||
$this->changeBook('page', $newBookId, $page, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update permissions if applicable
|
||||
if ($rebuildPermissions) {
|
||||
$entity->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity->book);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias method to update the book jointPermissions in the PermissionService.
|
||||
* @param Book $book
|
||||
*/
|
||||
public function buildJointPermissionsForBook(Book $book)
|
||||
{
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a name as a url slug.
|
||||
* @param $name
|
||||
* @return string
|
||||
*/
|
||||
protected function nameToSlug($name)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the page for viewing
|
||||
* @param Page $page
|
||||
* @param bool $blankIncludes
|
||||
* @return string
|
||||
*/
|
||||
public function renderPage(Page $page, bool $blankIncludes = false) : string
|
||||
{
|
||||
$content = $page->html;
|
||||
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($content);
|
||||
}
|
||||
|
||||
if ($blankIncludes) {
|
||||
$content = $this->blankPageIncludes($content);
|
||||
} else {
|
||||
$content = $this->parsePageIncludes($content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any page include tags within the given HTML.
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
protected function blankPageIncludes(string $html) : string
|
||||
{
|
||||
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
|
||||
* @param string $html
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function parsePageIncludes(string $html) : string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
|
||||
|
||||
$topLevelTags = ['table', 'ul', 'ol'];
|
||||
foreach ($matches[1] as $index => $includeId) {
|
||||
$splitInclude = explode('#', $includeId, 2);
|
||||
$pageId = intval($splitInclude[0]);
|
||||
if (is_nan($pageId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedPage = $this->getById('page', $pageId);
|
||||
if ($matchedPage === null) {
|
||||
$html = str_replace($matches[0][$index], '', $html);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($splitInclude) === 1) {
|
||||
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||
if ($matchingElem === null) {
|
||||
$html = str_replace($matches[0][$index], '', $html);
|
||||
continue;
|
||||
}
|
||||
$innerContent = '';
|
||||
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
|
||||
if ($isTopLevel) {
|
||||
$innerContent .= $doc->saveHTML($matchingElem);
|
||||
} else {
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
}
|
||||
}
|
||||
libxml_clear_errors();
|
||||
$html = str_replace($matches[0][$index], trim($innerContent), $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape script tags within HTML content.
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeScripts(string $html) : string
|
||||
{
|
||||
if ($html == '') {
|
||||
return $html;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
// Remove standard script tags
|
||||
$scriptElems = $xPath->query('//script');
|
||||
foreach ($scriptElems as $scriptElem) {
|
||||
$scriptElem->parentNode->removeChild($scriptElem);
|
||||
}
|
||||
|
||||
// Remove data or JavaScript iFrames
|
||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||
foreach ($badIframes as $badIframe) {
|
||||
$badIframe->parentNode->removeChild($badIframe);
|
||||
}
|
||||
|
||||
// Remove 'on*' attributes
|
||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
||||
foreach ($onAttributes as $attr) {
|
||||
/** @var \DOMAttr $attr*/
|
||||
$attrName = $attr->nodeName;
|
||||
$attr->parentNode->removeAttribute($attrName);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||
foreach ($topElems as $child) {
|
||||
$html .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for image usage within page content.
|
||||
* @param $imageString
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
$page->text = '';
|
||||
}
|
||||
return count($pages) > 0 ? $pages : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a bookshelf instance
|
||||
* @param Bookshelf $shelf
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyBookshelf(Bookshelf $shelf)
|
||||
{
|
||||
$this->destroyEntityCommonRelations($shelf);
|
||||
$shelf->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the provided book and all its child entities.
|
||||
* @param Book $book
|
||||
* @throws NotifyException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyBook(Book $book)
|
||||
{
|
||||
foreach ($book->pages as $page) {
|
||||
$this->destroyPage($page);
|
||||
}
|
||||
foreach ($book->chapters as $chapter) {
|
||||
$this->destroyChapter($chapter);
|
||||
}
|
||||
$this->destroyEntityCommonRelations($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a chapter and its relations.
|
||||
* @param Chapter $chapter
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyChapter(Chapter $chapter)
|
||||
{
|
||||
if (count($chapter->pages) > 0) {
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->save();
|
||||
}
|
||||
}
|
||||
$this->destroyEntityCommonRelations($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param Page $page
|
||||
* @throws NotifyException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
// Check if set as custom homepage & remove setting if not used or throw error if active
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
if (setting('app-homepage-type') === 'page') {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
}
|
||||
setting()->remove('app-homepage');
|
||||
}
|
||||
|
||||
$this->destroyEntityCommonRelations($page);
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
$attachmentService->deleteFile($attachment);
|
||||
}
|
||||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy or handle the common relations connected to an entity.
|
||||
* @param Entity $entity
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function destroyEntityCommonRelations(Entity $entity)
|
||||
{
|
||||
Activity::removeEntity($entity);
|
||||
$entity->views()->delete();
|
||||
$entity->permissions()->delete();
|
||||
$entity->tags()->delete();
|
||||
$entity->comments()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($entity);
|
||||
$this->searchService->deleteEntityTerms($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permissions of a bookshelf to all child books.
|
||||
* Returns the number of books that had permissions updated.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @return int
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function copyBookshelfPermissions(Bookshelf $bookshelf)
|
||||
{
|
||||
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$shelfBooks = $bookshelf->books()->get();
|
||||
$updatedBookCount = 0;
|
||||
|
||||
foreach ($shelfBooks as $book) {
|
||||
if (!userCan('restrictions-manage', $book)) {
|
||||
continue;
|
||||
}
|
||||
$book->permissions()->delete();
|
||||
$book->restricted = $bookshelf->restricted;
|
||||
$book->permissions()->createMany($shelfPermissions);
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
$updatedBookCount++;
|
||||
}
|
||||
|
||||
return $updatedBookCount;
|
||||
}
|
||||
}
|
||||
561
app/Entities/Repos/PageRepo.php
Normal file
561
app/Entities/Repos/PageRepo.php
Normal file
@@ -0,0 +1,561 @@
|
||||
<?php namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Entities\PageRevision;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* Get page by slug.
|
||||
* @param string $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @return Page
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function getPageBySlug(string $pageSlug, string $bookSlug)
|
||||
{
|
||||
return $this->getBySlug('page', $pageSlug, $bookSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through page revisions and retrieve the last page in the
|
||||
* current book that has a slug equal to the one given.
|
||||
* @param string $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @return null|Page
|
||||
*/
|
||||
public function getPageByOldSlug(string $pageSlug, string $bookSlug)
|
||||
{
|
||||
$revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->permissionService->enforceEntityRestrictions('page', $query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)
|
||||
->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param array $input
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updatePage(Page $page, int $book_id, array $input)
|
||||
{
|
||||
// Hold the old details to compare later
|
||||
$oldHtml = $page->html;
|
||||
$oldName = $page->name;
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
|
||||
}
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
|
||||
}
|
||||
|
||||
if (isset($input['template']) && userCan('templates-manage')) {
|
||||
$page->template = ($input['template'] === 'true');
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
$page->updated_by = $userId;
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->userUpdatePageDraftsQuery($page, $userId)->delete();
|
||||
|
||||
// Save a revision after updating
|
||||
$summary = $input['summary'] ?? null;
|
||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
|
||||
$this->savePageRevision($page, $summary);
|
||||
}
|
||||
|
||||
$this->searchService->indexEntity($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @param null|string $summary
|
||||
* @return PageRevision
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function savePageRevision(Page $page, string $summary = null)
|
||||
{
|
||||
$revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$revision->markdown = '';
|
||||
}
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$revision->created_by = user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->revision_number = $page->revision_count;
|
||||
$revision->save();
|
||||
|
||||
$revisionLimit = config('app.revision_limit');
|
||||
if ($revisionLimit !== false) {
|
||||
$revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
|
||||
if ($revisionsToDelete->count() > 0) {
|
||||
$this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly within the system.
|
||||
* @param string $htmlText
|
||||
* @return string
|
||||
*/
|
||||
protected function formatHtml(string $htmlText)
|
||||
{
|
||||
if ($htmlText == '') {
|
||||
return $htmlText;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
|
||||
// Set ids on top-level nodes
|
||||
$idMap = [];
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
$this->setUniqueId($childNode, $idMap);
|
||||
}
|
||||
|
||||
// Ensure no duplicate ids within child items
|
||||
$xPath = new DOMXPath($doc);
|
||||
$idElems = $xPath->query('//body//*//*[@id]');
|
||||
foreach ($idElems as $domElem) {
|
||||
$this->setUniqueId($domElem, $idMap);
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a unique id on the given DOMElement.
|
||||
* A map for existing ID's should be passed in to check for current existence.
|
||||
* @param DOMElement $element
|
||||
* @param array $idMap
|
||||
*/
|
||||
protected function setUniqueId($element, array &$idMap)
|
||||
{
|
||||
if (get_class($element) !== 'DOMElement') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Overwrite id if not a BookStack custom id
|
||||
$existingId = $element->getAttribute('id');
|
||||
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
||||
$idMap[$existingId] = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an unique id for the element
|
||||
// Uses the content as a basis to ensure output is the same every time
|
||||
// the same content is passed through.
|
||||
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
|
||||
$newId = urlencode($contentId);
|
||||
$loopIndex = 0;
|
||||
|
||||
while (isset($idMap[$newId])) {
|
||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
||||
$loopIndex++;
|
||||
}
|
||||
|
||||
$element->setAttribute('id', $newId);
|
||||
$idMap[$newId] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain text version of a page's content.
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @return string
|
||||
*/
|
||||
protected function pageToPlainText(Page $page) : string
|
||||
{
|
||||
$html = $this->renderPage($page, true);
|
||||
return strip_tags($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|null $chapter
|
||||
* @return \BookStack\Entities\Page
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function getDraftPage(Book $book, Chapter $chapter = null)
|
||||
{
|
||||
$page = $this->entityProvider->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->entityProvider->page->find($page->id);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return PageRevision|Page
|
||||
*/
|
||||
public function updatePageDraft(Page $page, array $data = [])
|
||||
{
|
||||
// If the page itself is a draft simply update that
|
||||
if ($page->draft) {
|
||||
$page->fill($data);
|
||||
if (isset($data['html'])) {
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
}
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
// Otherwise save the data to a revision
|
||||
$userId = user()->id;
|
||||
$drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
$draft = $drafts->first();
|
||||
} else {
|
||||
$draft = $this->entityProvider->pageRevision->newInstance();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = $userId;
|
||||
$draft->type = 'update_draft';
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$draft->markdown = '';
|
||||
}
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a draft page to make it a normal page.
|
||||
* Sets the slug and updates the content.
|
||||
* @param Page $draftPage
|
||||
* @param array $input
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function publishPageDraft(Page $draftPage, array $input)
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
|
||||
}
|
||||
|
||||
if (isset($input['template']) && userCan('templates-manage')) {
|
||||
$draftPage->template = ($input['template'] === 'true');
|
||||
}
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = $this->pageToPlainText($draftPage);
|
||||
$draftPage->draft = false;
|
||||
$draftPage->revision_count = 1;
|
||||
|
||||
$draftPage->save();
|
||||
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
|
||||
$this->searchService->indexEntity($draftPage);
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for getting user update drafts.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
protected function userUpdatePageDraftsQuery(Page $page, int $userId)
|
||||
{
|
||||
return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest updated draft revision for a particular page and user.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function getUserPageDraft(Page $page, int $userId)
|
||||
{
|
||||
return $this->userUpdatePageDraftsQuery($page, $userId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification message that informs the user that they are editing a draft page.
|
||||
* @param PageRevision $draft
|
||||
* @return string
|
||||
*/
|
||||
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;
|
||||
}
|
||||
return $message . "\n" . trans('entities.pages_draft_edited_notification');
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to check for active update drafts on a particular page.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return mixed
|
||||
*/
|
||||
protected function activePageEditingQuery(Page $page, int $minRange = null)
|
||||
{
|
||||
$query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is being actively editing.
|
||||
* Checks for edits since last page updated.
|
||||
* Passing in a minuted range will check for edits
|
||||
* within the last x minutes.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return bool
|
||||
*/
|
||||
public function isPageEditingActive(Page $page, int $minRange = null)
|
||||
{
|
||||
$draftSearch = $this->activePageEditingQuery($page, $minRange);
|
||||
return $draftSearch->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification message concerning the editing activity on a particular page.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return string
|
||||
*/
|
||||
public function getPageEditingActiveMessage(Page $page, int $minRange = null)
|
||||
{
|
||||
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
|
||||
|
||||
$userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
|
||||
$timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
|
||||
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the headers on the page to get a navigation menu
|
||||
* @param string $pageContent
|
||||
* @return array
|
||||
*/
|
||||
public function getPageNav(string $pageContent)
|
||||
{
|
||||
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 [];
|
||||
}
|
||||
|
||||
$tree = collect($headers)->map(function($header) {
|
||||
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
|
||||
$text = mb_substr($text, 0, 100);
|
||||
|
||||
return [
|
||||
'nodeName' => strtolower($header->nodeName),
|
||||
'level' => intval(str_replace('h', '', $header->nodeName)),
|
||||
'link' => '#' . $header->getAttribute('id'),
|
||||
'text' => $text,
|
||||
];
|
||||
})->filter(function($header) {
|
||||
return mb_strlen($header['text']) > 0;
|
||||
});
|
||||
|
||||
// Shift headers if only smaller headers have been used
|
||||
$levelChange = ($tree->pluck('level')->min() - 1);
|
||||
$tree = $tree->map(function ($header) use ($levelChange) {
|
||||
$header['level'] -= ($levelChange);
|
||||
return $header;
|
||||
});
|
||||
|
||||
return $tree->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a revision's content back into a page.
|
||||
* @param Page $page
|
||||
* @param Book $book
|
||||
* @param int $revisionId
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function restorePageRevision(Page $page, Book $book, int $revisionId)
|
||||
{
|
||||
$page->revision_count++;
|
||||
$this->savePageRevision($page);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
$this->searchService->indexEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
* @param Page $page
|
||||
* @param Entity $parent
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function changePageParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = $parent->isA('book') ? $parent : $parent->book;
|
||||
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
|
||||
$page->save();
|
||||
if ($page->book->id !== $book->id) {
|
||||
$page = $this->changeBook('page', $book->id, $page);
|
||||
}
|
||||
$page->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of a page in a new location with a new name.
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @param \BookStack\Entities\Entity $newParent
|
||||
* @param string $newName
|
||||
* @return \BookStack\Entities\Page
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function copyPage(Page $page, Entity $newParent, string $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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pages that have been marked as templates.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param string $search
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getPageTemplates(int $count = 10, int $page = 1, string $search = '')
|
||||
{
|
||||
$query = $this->entityQuery('page')
|
||||
->where('template', '=', true)
|
||||
->orderBy('name', 'asc')
|
||||
->skip( ($page - 1) * $count)
|
||||
->take($count);
|
||||
|
||||
if ($search) {
|
||||
$query->where('name', 'like', '%' . $search . '%');
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($count, ['*'], 'page', $page);
|
||||
$paginator->withPath('/templates');
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,34 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
/**
|
||||
* @var SearchTerm
|
||||
*/
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
@@ -29,24 +39,15 @@ class SearchService
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param EntityProvider $entityProvider
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->entityProvider = $entityProvider;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
@@ -64,15 +65,15 @@ class SearchService
|
||||
* @param string $searchString
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count
|
||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
||||
* @param string $action
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypes = array_keys($this->entityProvider->all());
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
$results = collect();
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
$entityTypesToSearch = $entityType;
|
||||
@@ -80,18 +81,27 @@ 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),
|
||||
'has_more' => $hasMore,
|
||||
'results' => $results->sortByDesc('score')->values()
|
||||
];
|
||||
}
|
||||
@@ -111,7 +121,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 +149,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,23 +168,24 @@ class SearchService
|
||||
* Create a search query for an entity
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
* @param string $action
|
||||
* @return EloquentBuilder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page')
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entity = $this->entityProvider->get($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where(function(Builder $query) use ($terms) {
|
||||
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
||||
$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,9 +193,9 @@ 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 (EloquentBuilder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
@@ -195,10 +211,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 +252,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
|
||||
@@ -263,19 +283,22 @@ class SearchService
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param EloquentBuilder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
|
||||
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
|
||||
{
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$query->whereHas('tags', function (EloquentBuilder $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
|
||||
@@ -292,16 +315,6 @@ class SearchService
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
@@ -309,8 +322,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();
|
||||
@@ -321,13 +334,14 @@ class SearchService
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities) {
|
||||
protected function indexEntities($entities)
|
||||
{
|
||||
$terms = [];
|
||||
foreach ($entities as $entity) {
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
|
||||
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
|
||||
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
|
||||
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
|
||||
$term['entity_id'] = $entity->id;
|
||||
$term['entity_type'] = $entity->getMorphClass();
|
||||
@@ -348,20 +362,12 @@ class SearchService
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
foreach ($this->entityProvider->all() as $entityModel) {
|
||||
$selectFields = ['id', 'name', $entityModel->textField];
|
||||
$entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
|
||||
$this->indexEntities($entities);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,11 +388,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 = [];
|
||||
@@ -406,77 +416,121 @@ class SearchService
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterUpdatedAfter(EloquentBuilder $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)
|
||||
protected function filterUpdatedBefore(EloquentBuilder $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)
|
||||
protected function filterCreatedAfter(EloquentBuilder $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)
|
||||
protected function filterCreatedBefore(EloquentBuilder $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)
|
||||
protected function filterCreatedBy(EloquentBuilder $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)
|
||||
protected function filterUpdatedBy(EloquentBuilder $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);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterInName(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
|
||||
protected function filterInTitle(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$this->filterInName($query, $model, $input);
|
||||
}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterInBody(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterViewedByMe(EloquentBuilder $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)
|
||||
protected function filterNotViewedByMe(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function($query) {
|
||||
$query->whereDoesntHave('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
protected function filterSortBy(EloquentBuilder $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(EloquentBuilder $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');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
@@ -14,5 +16,4 @@ class SearchTerm extends Model
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -26,10 +27,11 @@ class Handler extends ExceptionHandler
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||
*
|
||||
* @param \Exception $e
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
*/
|
||||
public function report(Exception $e)
|
||||
{
|
||||
@@ -60,6 +62,11 @@ class Handler extends ExceptionHandler
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
// Handle 404 errors with a loaded session to enable showing user-specific information
|
||||
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
|
||||
return \Route::respondWithRoute('fallback');
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
@@ -69,9 +76,12 @@ class Handler extends ExceptionHandler
|
||||
* @param $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, $type) {
|
||||
protected function isExceptionType(Exception $e, $type)
|
||||
{
|
||||
do {
|
||||
if (is_a($e, $type)) return true;
|
||||
if (is_a($e, $type)) {
|
||||
return true;
|
||||
}
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
@@ -81,7 +91,8 @@ class Handler extends ExceptionHandler
|
||||
* @param Exception $e
|
||||
* @return string
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e) {
|
||||
protected function getOriginalMessage(Exception $e)
|
||||
{
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
@@ -103,4 +114,16 @@ class Handler extends ExceptionHandler
|
||||
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a validation exception into a JSON response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Validation\ValidationException $exception
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
protected function invalidJson($request, ValidationException $exception)
|
||||
{
|
||||
return response()->json($exception->errors(), $exception->status);
|
||||
}
|
||||
}
|
||||
|
||||
7
app/Exceptions/HttpFetchException.php
Normal file
7
app/Exceptions/HttpFetchException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class HttpFetchException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -12,10 +11,10 @@ class NotifyException extends \Exception
|
||||
* @param string $message
|
||||
* @param string $redirectLocation
|
||||
*/
|
||||
public function __construct($message, $redirectLocation)
|
||||
public function __construct(string $message, string $redirectLocation = "/")
|
||||
{
|
||||
$this->message = $message;
|
||||
$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 {}
|
||||
}
|
||||
|
||||
6
app/Exceptions/SocialSignInAccountNotUsed.php
Normal file
6
app/Exceptions/SocialSignInAccountNotUsed.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class SocialSignInAccountNotUsed extends SocialSignInException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
19
app/Exceptions/UserTokenExpiredException.php
Normal file
19
app/Exceptions/UserTokenExpiredException.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserTokenExpiredException extends \Exception {
|
||||
|
||||
public $userId;
|
||||
|
||||
/**
|
||||
* UserTokenExpiredException constructor.
|
||||
* @param string $message
|
||||
* @param int $userId
|
||||
*/
|
||||
public function __construct(string $message, int $userId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
3
app/Exceptions/UserTokenNotFoundException.php
Normal file
3
app/Exceptions/UserTokenNotFoundException.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserTokenNotFoundException extends \Exception {}
|
||||
5
app/Exceptions/UserUpdateException.php
Normal file
5
app/Exceptions/UserUpdateException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserUpdateException extends NotifyException
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -10,5 +9,8 @@ class Activity extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'activity'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'activity';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -10,5 +9,8 @@ class Images extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'images'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'images';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -10,5 +9,8 @@ class Setting extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'setting'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'setting';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -9,5 +9,8 @@ class Views extends Facade
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor() { return 'views'; }
|
||||
}
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'views';
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
@@ -14,7 +15,7 @@ class AttachmentController extends Controller
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
* @param AttachmentService $attachmentService
|
||||
* @param \BookStack\Uploads\AttachmentService $attachmentService
|
||||
* @param Attachment $attachment
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
@@ -102,7 +103,7 @@ class AttachmentController extends Controller
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'url|min:1|max:255'
|
||||
'link' => 'string|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
@@ -130,7 +131,7 @@ class AttachmentController extends Controller
|
||||
$this->validate($request, [
|
||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||
'name' => 'required|string|min:1|max:255',
|
||||
'link' => 'required|url|min:1|max:255'
|
||||
'link' => 'required|string|min:1|max:255'
|
||||
]);
|
||||
|
||||
$pageId = $request->get('uploaded_to');
|
||||
@@ -182,11 +183,17 @@ class AttachmentController extends Controller
|
||||
* Get an attachment from storage.
|
||||
* @param $attachmentId
|
||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function get($attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
||||
if ($page === null) {
|
||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
||||
}
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
if ($attachment->external) {
|
||||
@@ -194,16 +201,14 @@ class AttachmentController extends Controller
|
||||
}
|
||||
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return response($attachmentContents, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||
]);
|
||||
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific attachment in the system.
|
||||
* @param $attachmentId
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete($attachmentId)
|
||||
{
|
||||
|
||||
118
app/Http/Controllers/Auth/ConfirmEmailController.php
Normal file
118
app/Http/Controllers/Auth/ConfirmEmailController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\EmailConfirmationService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ConfirmEmailController extends Controller
|
||||
{
|
||||
protected $emailConfirmationService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->emailConfirmationService = $emailConfirmationService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the page to tell the user to check their email
|
||||
* and confirm their address.
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
return view('auth.register-confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notice that a user's email address has not been confirmed,
|
||||
* Also has the option to re-send the confirmation email.
|
||||
* @return View
|
||||
*/
|
||||
public function showAwaiting()
|
||||
{
|
||||
return view('auth.user-unconfirmed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms an email via a token and logs the user into the system.
|
||||
* @param $token
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws ConfirmationEmailException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function confirm($token)
|
||||
{
|
||||
try {
|
||||
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
|
||||
} catch (Exception $exception) {
|
||||
|
||||
if ($exception instanceof UserTokenNotFoundException) {
|
||||
session()->flash('error', trans('errors.email_confirmation_invalid'));
|
||||
return redirect('/register');
|
||||
}
|
||||
|
||||
if ($exception instanceof UserTokenExpiredException) {
|
||||
$user = $this->userRepo->getById($exception->userId);
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
session()->flash('error', trans('errors.email_confirmation_expired'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
session()->flash('success', trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteByUser($user);
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resend the confirmation email
|
||||
* @param Request $request
|
||||
* @return View
|
||||
*/
|
||||
public function resend(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|exists:users,email'
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -64,5 +64,4 @@ class ForgotPasswordController extends Controller
|
||||
['email' => trans($response)]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -36,21 +37,24 @@ class LoginController extends Controller
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $ldapService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService
|
||||
* @param LdapService $ldapService
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->ldapService = $ldapService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectAfterLogout = baseUrl('/login');
|
||||
$this->redirectPath = url('/');
|
||||
$this->redirectAfterLogout = url('/login');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -66,24 +70,26 @@ class LoginController extends Controller
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
* @throws \BookStack\Exceptions\LdapException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
// Explicitly log them out for now if they do no exist.
|
||||
if (!$user->exists) auth()->logout($user);
|
||||
if (!$user->exists) {
|
||||
auth()->logout($user);
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||
if (!$user->exists && $user->email === null && !$request->filled('email')) {
|
||||
$request->flash();
|
||||
session()->flash('request-email', true);
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||
if (!$user->exists && $user->email === null && $request->filled('email')) {
|
||||
$user->email = $request->get('email');
|
||||
}
|
||||
|
||||
if (!$user->exists) {
|
||||
|
||||
// Check for users with same email already
|
||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||
if ($alreadyUser) {
|
||||
@@ -95,30 +101,43 @@ class LoginController extends Controller
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
// Sync LDAP groups if required
|
||||
if ($this->ldapService->shouldSyncGroups()) {
|
||||
$this->ldapService->syncGroups($user, $request->get($this->username()));
|
||||
}
|
||||
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
session()->put('social-callback', 'login');
|
||||
return $this->socialAuthService->startLogIn($socialDriver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,25 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Auth\Access\EmailConfirmationService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use Validator;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
@@ -53,12 +58,12 @@ 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;
|
||||
$this->redirectTo = baseUrl('/');
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectTo = url('/');
|
||||
$this->redirectPath = url('/');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -71,7 +76,7 @@ class RegisterController extends Controller
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|max:255',
|
||||
'name' => 'required|min:2|max:255',
|
||||
'email' => 'required|email|max:255|unique:users',
|
||||
'password' => 'required|min:6',
|
||||
]);
|
||||
@@ -91,6 +96,7 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Show the application registration form.
|
||||
* @return Response
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function getRegister()
|
||||
{
|
||||
@@ -101,20 +107,27 @@ class RegisterController extends Controller
|
||||
|
||||
/**
|
||||
* Handle a registration request for the application.
|
||||
* @param Request|\Illuminate\Http\Request $request
|
||||
* @return Response
|
||||
* @param Request|Request $request
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function postRegister(Request $request)
|
||||
{
|
||||
$this->checkRegistrationAllowed();
|
||||
$validator = $this->validator($request->all());
|
||||
$this->validator($request->all())->validate();
|
||||
|
||||
if ($validator->fails()) {
|
||||
$this->throwValidationException(
|
||||
$request, $validator
|
||||
);
|
||||
$captcha = $request->get('g-recaptcha-response');
|
||||
$resp = (new Client())->post('https://www.google.com/recaptcha/api/siteverify', [
|
||||
'form_params' => [
|
||||
'response' => $captcha,
|
||||
'secret' => '%%secret_key%%',
|
||||
]
|
||||
]);
|
||||
$respBody = json_decode($resp->getBody());
|
||||
if (!$respBody->success) {
|
||||
return redirect()->back()->withInput()->withErrors([
|
||||
'g-recaptcha-response' => 'Did not pass captcha',
|
||||
]);
|
||||
}
|
||||
|
||||
$userData = $request->all();
|
||||
@@ -139,26 +152,28 @@ class RegisterController extends Controller
|
||||
* The registrations flow for all users.
|
||||
* @param array $userData
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @param bool $emailVerified
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
|
||||
{
|
||||
if (setting('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
$registrationRestrict = setting('registration-restrict');
|
||||
|
||||
if ($registrationRestrict) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||
}
|
||||
}
|
||||
|
||||
$newUser = $this->userRepo->registerNew($userData);
|
||||
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
|
||||
if ($socialAccount) {
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
|
||||
$newUser->save();
|
||||
|
||||
try {
|
||||
@@ -175,70 +190,12 @@ class RegisterController extends Controller
|
||||
return redirect($this->redirectPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page to tell the user to check their email
|
||||
* and confirm their address.
|
||||
*/
|
||||
public function getRegisterConfirmation()
|
||||
{
|
||||
return view('auth/register-confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms an email via a token and logs the user into the system.
|
||||
* @param $token
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function confirmEmail($token)
|
||||
{
|
||||
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
||||
$user = $confirmation->user;
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
auth()->login($user);
|
||||
session()->flash('success', trans('auth.email_confirm_success'));
|
||||
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
||||
return redirect($this->redirectPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notice that a user's email address has not been confirmed,
|
||||
* Also has the option to re-send the confirmation email.
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function showAwaitingConfirmation()
|
||||
{
|
||||
return view('auth/user-unconfirmed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the confirmation email
|
||||
* @param Request $request
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function resendConfirmation(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|exists:users,email'
|
||||
]);
|
||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($user);
|
||||
} catch (Exception $e) {
|
||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the social site for authentication intended to register.
|
||||
* @param $socialDriver
|
||||
* @return mixed
|
||||
* @throws UserRegistrationException
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function socialRegister($socialDriver)
|
||||
{
|
||||
@@ -250,25 +207,52 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* The callback for social login services.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws SocialSignInException
|
||||
* @throws UserRegistrationException
|
||||
* @throws SocialDriverNotConfigured
|
||||
*/
|
||||
public function socialCallback($socialDriver)
|
||||
public function socialCallback($socialDriver, Request $request)
|
||||
{
|
||||
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);
|
||||
|
||||
// Attempt login or fall-back to register if allowed.
|
||||
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
||||
if ($action == 'login') {
|
||||
try {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||
} catch (SocialSignInAccountNotUsed $exception) {
|
||||
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
|
||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a social account from a user.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @return RedirectResponse|Redirector
|
||||
*/
|
||||
public function detachSocialAccount($socialDriver)
|
||||
{
|
||||
@@ -277,14 +261,16 @@ class RegisterController extends Controller
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
@@ -292,7 +278,6 @@ class RegisterController extends Controller
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
return $this->registerUser($userData, $socialAccount, $emailVerified);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +46,4 @@ class ResetPasswordController extends Controller
|
||||
return redirect($this->redirectPath())
|
||||
->with('status', trans($response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
app/Http/Controllers/Auth/UserInviteController.php
Normal file
106
app/Http/Controllers/Auth/UserInviteController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\UserInviteService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserTokenExpiredException;
|
||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserInviteController extends Controller
|
||||
{
|
||||
protected $inviteService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param UserInviteService $inviteService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
|
||||
{
|
||||
$this->inviteService = $inviteService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->middleware('guest');
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the page for the user to set the password for their account.
|
||||
* @param string $token
|
||||
* @return Factory|View|RedirectResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function showSetPassword(string $token)
|
||||
{
|
||||
try {
|
||||
$this->inviteService->checkTokenAndGetUserId($token);
|
||||
} catch (Exception $exception) {
|
||||
return $this->handleTokenException($exception);
|
||||
}
|
||||
|
||||
return view('auth.invite-set-password', [
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password for an invited user and then grants them access.
|
||||
* @param string $token
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setPassword(string $token, Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'password' => 'required|min:6'
|
||||
]);
|
||||
|
||||
try {
|
||||
$userId = $this->inviteService->checkTokenAndGetUserId($token);
|
||||
} catch (Exception $exception) {
|
||||
return $this->handleTokenException($exception);
|
||||
}
|
||||
|
||||
$user = $this->userRepo->getById($userId);
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
auth()->login($user);
|
||||
session()->flash('success', trans('auth.user_invite_success', ['appName' => setting('app-name')]));
|
||||
$this->inviteService->deleteByUser($user);
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and validate the exception thrown when checking an invite token.
|
||||
* @param Exception $exception
|
||||
* @return RedirectResponse|Redirector
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function handleTokenException(Exception $exception)
|
||||
{
|
||||
if ($exception instanceof UserTokenNotFoundException) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
if ($exception instanceof UserTokenExpiredException) {
|
||||
session()->flash('error', trans('errors.invite_token_expired'));
|
||||
return redirect('/password/email');
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user